Authentication
All authenticated REST requests require a JWT access token:
Authorization: Bearer <accessToken>
WebSocket authentication uses the same header during the connection handshake.
wss://api.ockde.com/ws
Authorization: Bearer <accessToken>
REST API
POST /auth/social-login
Social login for Google or Apple. Returns JWT tokens and a user profile.
| Field | Type | Required | Description |
|---|---|---|---|
| provider | string | yes | Provider name. Expected: google or apple. |
| idToken | string | yes | Provider ID token. |
| deviceToken | string | no | Push device token (optional). |
| devicePlatform | string | no | Device platform (optional). |
| Field | Type | Nullable | Description |
|---|---|---|---|
| accessToken | string | no | JWT access token. |
| refreshToken | string | no | JWT refresh token. |
| firebaseToken | string | yes | Firebase custom token when Firebase is enabled. |
| user | User | no | User profile. |
{
"provider": "google",
"idToken": "eyJhbGciOi..."
}
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"firebaseToken": null,
"user": {
"id": "user_123",
"name": "Ada Lovelace",
"age": 28,
"avatar": "https://cdn.example.com/ada.png",
"gender": "female",
"vibe": null,
"vibeExpiresAt": null
}
}
POST /auth/logout
Deactivate a device token and blacklist refresh token (if provided).
| Field | Type | Required | Description |
|---|---|---|---|
| deviceToken | string | no | Push device token to deactivate. |
| refreshToken | string | no | Refresh token to blacklist. |
{ "status": "ok" }
POST /auth/delete-account
Soft delete the authenticated account.
| Field | Type | Required | Description |
|---|---|---|---|
| reason | string | yes | Reason for deletion. |
{ "status": "ok" }
POST /devices/register
Register or refresh an FCM/APNS token for push notifications.
{
"token": "fcm_or_apns_token",
"platform": "android"
}
{
"status": "ok",
"device": {
"token": "fcm_or_apns_token",
"platform": "android",
"isActive": true
}
}
PATCH /users/profile
Update profile fields for the authenticated user.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | no | Display name. |
| avatar | string (URL) or null | no | Profile image URL. Empty string clears to null. |
| age | integer | no | Age (0+). Use null to clear. |
| gender | string or null | no | Gender label. Use null to clear. |
{
"id": "user_123",
"name": "Ada Lovelace",
"age": 28,
"avatar": "https://cdn.example.com/ada.png",
"gender": "female",
"vibe": null,
"vibeExpiresAt": null
}
POST /connections/:connectionId/proximity/token
Issue a short-lived BLE token for the authenticated participant in an active connection.
{
"token": "a3f9c2e1",
"expiresAt": "2026-04-17T14:32:00Z",
"connectionId": "conn_abc"
}
GET /connections/:connectionId/proximity/summary
Return accumulated proximity progress, milestone state, and recent session history for an active connection.
{
"connectionId": "conn_abc",
"accumulatedSeconds": 1020,
"accumulatedMinutes": 17,
"unlockedMilestones": [5, 15],
"nextMilestone": 30,
"secondsUntilNext": 780,
"sessions": [
{ "startedAt": "...", "durationSeconds": 600, "status": "ended" },
{ "startedAt": "...", "durationSeconds": 420, "status": "active" }
]
}
unlockedMilestones represents together-time milestones already reached by the pair. This milestone state is intended to feed gift eligibility and later sponsor reward allocation.
Data Models
User
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | no | User ID (e.g. user_...). |
| name | string | no | Display name. |
| age | integer | yes | Age. |
| avatar | string (URL) | yes | Avatar URL. |
| gender | string | yes | Gender label. |
| vibe | string | yes | Current vibe text. |
| vibeExpiresAt | string (ISO) | yes | Vibe expiration timestamp. |
Presence
| Field | Type | Nullable | Description |
|---|---|---|---|
| isLive | boolean | no | Live status. |
| location | object | yes | Location object or null. |
| Location Field | Type | Description |
|---|---|---|
| lat | number | Latitude. |
| lng | number | Longitude. |
| accuracyMeters | number | Optional GPS accuracy in meters. |
| heading | number | Optional device heading in degrees. |
| speed | number | Optional device speed. |
| updatedAt | string (ISO) | Last update timestamp. |
NearbyUser
| Field | Type | Description |
|---|---|---|
| user | User | User summary. |
| distance | number | Distance in meters. |
| angle | number | Bearing angle (0-360). |
| isLive | boolean | Whether the nearby user is currently live. |
| sharedInterests | array[string] | Shared interests between the viewer and the nearby user. In the current backend this is derived from overlapping vibe tags/text. |
| location | Location | Latest nearby user location snapshot, including freshness metadata. |
ConnectionRequest
| Field | Type | Description |
|---|---|---|
| id | string | Request ID alias for list rendering. |
| requestId | string | Request ID. |
| fromUserId | string | Sender user ID. |
| toUserId | string | Recipient user ID. |
| senderId | string | Alias for the request sender user ID. |
| receiverId | string | Alias for the request receiver user ID. |
| user | User | Other user. |
| userId | string | Other user ID. |
| name | string | Other user display name. |
| age | integer | Other user age. |
| avatar | string (URL) or null | Other user avatar, always present even when null. |
| snippet | string | Short request summary for list UIs. |
| direction | string | incoming | outgoing |
| status | string | pending | accepted | declined | missed | closed |
| legacyStatus | string | Original server-side status value for backwards compatibility. |
| closureReason | string or null | Optional reason when the request was closed before activation. |
| createdAt | string (ISO) | Created timestamp. |
| resolvedAt | string (ISO) or null | When the request stopped being pending. |
| expiresAt | string (ISO) or null | Pending request expiry timestamp. Null once the request is no longer pending. |
missed and emits connection.request.missed to both users.
Connection
| Field | Type | Description |
|---|---|---|
| connectionId | string | Connection ID. |
| user | User | Other user. |
| isOnline | boolean | Whether the other participant currently has an active GISBI socket session. |
| accumulatedMinutes | integer | Verified together-time accumulated on the active connection. |
| nextMilestoneMinutes | integer or null | Next proximity milestone for the connection, when available. |
| direction | string | incoming | outgoing, based on who initiated the connection request. |
| type | string | Always connection for closed/active connection history items. |
| status | string | accepted | closed |
| legacyStatus | string | Underlying connection status value (active | closed). |
| senderId | string | User who originally sent the request that became this connection. |
| receiverId | string | User who originally received the request that became this connection. |
| name | string | Other user display name. |
| age | integer | Other user age. |
| avatar | string (URL) or null | Other user avatar, always present even when null. |
| snippet | string | Other user vibe text. |
| closureReason | string or null | Optional reason provided when the connection was closed. |
| closedByUserId | string or null | User who closed the connection, when known. |
| connectedLocation | object or null | Snapshot of the pair connection point when live locations were available at acceptance time. |
| createdAt | string (ISO) | Created timestamp. |
| resolvedAt | string (ISO) | When the request became an accepted connection. |
| closedAt | string (ISO) or null | Closed timestamp. |
ProximitySession
| Field | Type | Description |
|---|---|---|
| connectionId | string | Active connection being tracked. |
| accumulatedSeconds | integer | Total verified together-time accumulated across all valid runs. |
| status | string | active | paused | ended |
| currentSessionSeconds | integer | Length of the current continuous verified run. |
| unlockedMilestones | integer[] | Together-time milestones already reached, in minutes. |
| bleToken | string | The participant's current BLE advertising token when available in bootstrap state. |
| bleTokenExpiresAt | string (ISO) | When the current BLE token expires. |
Message
| Field | Type | Description |
|---|---|---|
| messageId | string | Message ID. |
| senderId | string | Sender user ID. |
| recipientId | string | Recipient user ID. |
| sender | User | Sender profile. |
| recipient | User | Recipient profile. |
| content | string | Message body. |
| createdAt | string (ISO) | Created timestamp. |
| readAt | string (ISO) or null | Read timestamp. |
SponsoredGift
| Field | Type | Description |
|---|---|---|
| giftId | string | Sponsored grant ID. |
| giftType | string | Always sponsoredPairReward. |
| connectionId | string | Pair connection that earned the reward. |
| campaignId | string | Sponsor campaign ID. |
| brand | object | Sponsor metadata. |
| userAId | string | First participant in the pair. |
| userBId | string | Second participant in the pair. |
| partnerUserId | string or null | The other user in the pair for the authenticated viewer. |
| partnerName | string or null | Display name for the other user in the pair. |
| partnerAvatarUrl | string (URL) or null | Profile image URL for the other user in the pair. |
| partnerClaimableNow | boolean or null | Pair-level readiness flag mirrored for the partner-focused gift card UI. |
| triggerType | string | connectionAccepted or proximityMilestone. |
| milestoneMinutes | integer or null | Milestone that triggered the reward, when applicable. |
| minTotalTogetherMinutes | integer | Minimum cumulative verified together-time required for this campaign or grant. |
| minDailyTogetherMinutes | integer or null | Optional per-day threshold. When set, only days that meet this verified time count toward the cumulative total. |
| minDistinctQualifyingDays | integer | Minimum number of qualifying days required when daily proximity thresholds are part of the rule. |
| requireActiveProximityForClaim | boolean | When true, the pair must still be in an active verified proximity session when the reward is verified or redeemed. |
| qualifyingTogetherMinutes | integer or null | Minutes currently counted toward this campaign after daily-threshold filtering. |
| requiredTogetherMinutes | integer or null | Total minutes the pair must satisfy for this campaign to be claimable. |
| qualifyingDaysCount | integer or null | How many calendar days currently meet the campaign's daily together-time rule. |
| requiredQualifyingDays | integer or null | How many qualifying days the campaign requires. |
| claimableNow | boolean or null | Whether the pair currently satisfies the campaign proximity rules for claim. |
| claimStage | string | granted, verified, redeemed, expired, or cancelled. |
| historyStatus | string | User-facing history label. Current values include pending, available, verified, claimed, and disqualified. |
| sponsorConfidenceScore | integer | 0-100 confidence score derived from the pair's verified together-time, qualifying days, and live proximity rule fit. |
| verificationContext | object | Sponsor-facing proof context including current and historical qualifying minutes/days. |
| riskSignals | object | Boolean risk flags that explain why a reward is or is not safely claimable right now. |
| title | string | Reward title snapshot. |
| description | string | What the pair receives from the sponsor. |
| terms | string | Reward terms snapshot. |
| rewardImageUrl | string (URL) or null | Optional reward image for the reveal card. |
| brandTagline | string or null | Optional sponsor/campaign line to show warmth on the card. |
| campaignMessage | string or null | Alias of the campaign tagline/message. |
| venueName | string or null | Where the reward is redeemed. Falls back to the brand name when no venue name is configured. |
| venueAddress | string or null | Venue address or city for redemption guidance. |
| status | string | Raw backend lifecycle status: active | redeemed | expired | cancelled |
| legacyStatus | string | Alias of the raw backend lifecycle status for compatibility. |
| createdAt | string (ISO) | Granted timestamp. |
| expiresAt | string (ISO) or null | Overall reward expiry timestamp. |
| codeExpiresAt | string (ISO) or null | Redemption code expiry timestamp, tracked separately from the broader reward lifetime. |
SafetyReport
| Field | Type | Description |
|---|---|---|
| reportId | string | Report ID. |
| reportedUserId | string | Reported user ID. |
| reason | string | Report reason. |
| status | string | open | reviewing | resolved |
| createdAt | string (ISO) | Created timestamp. |
WebSocket Protocol
All socket messages are JSON with a common envelope.
{
"type": "session.start",
"payload": {},
"requestId": "req_123",
"correlationId": "corr_123"
}
{
"type": "session.bootstrap",
"payload": { "currentUser": { "...": "..." } },
"timestamp": "2026-04-14T14:12:20Z",
"requestId": "req_123",
"correlationId": "corr_123"
}
requestId or correlationId are provided by the client,
the server echoes them back in the response envelope.
session.start
Initialize a socket session and fetch bootstrap data.
{ "lastSeenAt": "2026-04-19T08:00:00Z" }
{ "type": "session.connected", "payload": {}, "timestamp": "..." }
{ "type": "session.bootstrap", "payload": { ... }, "timestamp": "..." }
{ "type": "chat.message.created", "payload": { ...Message }, "timestamp": "..." }
lastSeenAt is provided, the server replays any incoming messages created after that timestamp.
presence.location_update
Send live location updates and receive nearby users.
{
"lat": 6.5244,
"lng": 3.3792,
"accuracyMeters": 6.2,
"heading": 120.0,
"speed": 1.7
}
{
"type": "presence.updated",
"payload": {
"userId": "user_123",
"presence": {
"isLive": true,
"isOnRadar": true,
"location": {
"lat": 6.5244,
"lng": 3.3792,
"accuracyMeters": 6.2,
"heading": 120.0,
"speed": 1.7,
"updatedAt": "..."
}
}
},
"timestamp": "..."
}
presence.isOnRadar is tracked separately from socket session state. When false, the user is hidden from other users' radar results immediately.
{
"type": "radar.nearby.updated",
"payload": {
"nearbyUsers": [
{
"user": { "...": "..." },
"distance": 12.4,
"angle": 210.3,
"isLive": true,
"location": {
"lat": 6.5245,
"lng": 3.3794,
"updatedAt": "...",
"accuracyMeters": 4.8
}
}
]
},
"timestamp": "..."
}
presence.location_update. Users
who are not currently live are excluded from the radar payload.
radar.set_visible
Show or hide the authenticated user from radar without disconnecting the session.
{ "isOnRadar": false }
{ "type": "presence.updated", "payload": { "userId": "user_123", "presence": { "isLive": true, "isOnRadar": false, "location": { "...": "..." } } }, "timestamp": "..." }
{ "type": "radar.nearby.updated", "payload": { "nearbyUsers": [ ...NearbyUser ] }, "timestamp": "..." }
connection.request.send
Send a connection request to another user. Pending requests remain valid for 10 minutes.
{ "userId": "user_456" }
{ "type": "connection.request.send", "payload": { "request": { ...ConnectionRequest } }, "timestamp": "..." }
{ "type": "connection.request.send", "payload": { "request": { ...ConnectionRequest } }, "timestamp": "..." }
request now includes direction and resolvedAt so clients can keep a unified requests list in sync without a fresh bootstrap.
connection.request.accept
Accept an incoming connection request.
{ "requestId": "req_123" }
{
"type": "connection.request.accept",
"payload": {
"requestId": "req_123",
"fromUserId": "user_123",
"toUserId": "user_456",
"direction": "incoming",
"resolvedAt": "...",
"request": { ...ConnectionRequest },
"connection": {
"connectionId": "conn_abc",
"user": { ...User },
"type": "connection",
"status": "active",
"connectedLocation": { "lat": 6.5244, "lng": 3.3792 },
"createdAt": "...",
"closedAt": null
}
},
"timestamp": "..."
}
missed, emits connection.request.missed to both users, and
returns an error payload with code request_expired.
connection.request.reject
Reject an incoming connection request.
{ "requestId": "req_123" }
{ "type": "connection.request.reject", "payload": { "requestId": "req_123", "fromUserId": "user_123", "toUserId": "user_456", "direction": "incoming", "status": "rejected", "resolvedAt": "...", "request": { ...ConnectionRequest } }, "timestamp": "..." }
connection.request.missed
Broadcast when a pending connection request times out after 10 minutes.
{ "type": "connection.request.missed", "payload": { "requestId": "req_123", "fromUserId": "user_123", "toUserId": "user_456", "direction": "incoming", "status": "missed", "resolvedAt": "...", "request": { ...ConnectionRequest } }, "timestamp": "..." }
connection.close
Close an active connection.
{ "connectionId": "conn_123", "closureReason": "user_left" }
{ "type": "connection.close", "payload": { "connectionId": "conn_123", "type": "connection", "status": "closed", "closedByUserId": "user_123", "closureReason": "user_left", "closedAt": "...", "recipientId": "user_456" }, "timestamp": "..." }
connection.history.fetch
Fetch the current connection history for the authenticated user.
{}
{
"type": "connection.history.fetch",
"payload": {
"activeConnections": [ ...Connection ],
"closedConnections": [ ...Connection ]
},
"timestamp": "..."
}
connection.ring
Ring another user.
{ "userId": "user_456" }
{ "type": "connection.ring", "payload": { "fromUserId": "user_123", "recipientId": "user_456", "fromUser": { ...User } }, "timestamp": "..." }
{ "type": "connection.ring", "payload": { "status": "sent", "fromUserId": "user_123", "recipientId": "user_456", "fromUser": { ...User } }, "timestamp": "..." }
proximity.session.start
Begin or resume a proximity run after scanning the partner BLE token.
{ "connectionId": "conn_abc", "token": "a3f9c2e1", "rssi": -68 }
{
"type": "proximity.session.start",
"payload": {
"sessionActive": false,
"yourTokenReceived": true,
"waitingForPartner": true
},
"timestamp": "..."
}
{
"type": "proximity.partner.confirmed",
"payload": { "connectionId": "conn_abc" },
"timestamp": "..."
}
{
"type": "proximity.session.updated",
"payload": {
"connectionId": "conn_abc",
"accumulatedSeconds": 420,
"sessionActive": true,
"currentSessionSeconds": 120,
"unlockedMilestones": [5, 15],
"nextMilestoneMinutes": 30
},
"timestamp": "..."
}
proximity.session.start with the same valid (connectionId, token) pair is idempotent. The server returns the same ACK shape without duplicating or resetting the session.
proximity.heartbeat
Keep a proximity session alive while both devices continue seeing each other.
{ "connectionId": "conn_abc", "token": "a3f9c2e1", "rssi": -68 }
{
"type": "proximity.heartbeat",
"payload": {
"sessionActive": true,
"accumulatedSeconds": 420
},
"timestamp": "..."
}
{
"type": "proximity.session.updated",
"payload": {
"connectionId": "conn_abc",
"accumulatedSeconds": 420,
"sessionActive": true,
"currentSessionSeconds": 120,
"unlockedMilestones": [5, 15],
"nextMilestoneMinutes": 30
},
"timestamp": "..."
}
{ "type": "proximity.milestone.reached", "payload": { "connectionId": "conn_abc", "milestone": 15, "giftTier": 2, "message": "You've been together 15 minutes - a new gift is ready." }, "timestamp": "..." }
{ "type": "proximity.token.rotate", "payload": { "connectionId": "conn_abc", "token": "b7d1a4f2", "expiresAt": "2026-04-17T14:33:30Z" }, "timestamp": "..." }
-40 dBm are rejected.
proximity.session.end
Explicitly end or pause a proximity run when BLE detection stops on the client.
{ "connectionId": "conn_abc" }
{
"type": "proximity.session.updated",
"payload": {
"connectionId": "conn_abc",
"sessionActive": false,
"accumulatedSeconds": 142,
"currentSessionSeconds": 0,
"unlockedMilestones": [],
"nextMilestoneMinutes": 5,
"gracePeriodSeconds": 30
},
"timestamp": "..."
}
chat.message.created
Send a chat message to another user.
{ "recipientId": "user_456", "content": "Hello there" }
{ "type": "chat.message.created", "payload": { ...Message }, "timestamp": "..." }
vibe.expired
Server-only event emitted when a user's vibe expires during background processing.
{ "expiredAt": "2026-04-19T08:00:00Z" }
chat.history.fetch
Fetch recent message history for the authenticated user.
{}
{
"type": "chat.history.fetch",
"payload": {
"messages": [ ...Message ],
"conversations": {
"user_456": { "lastMessage": { ...Message }, "unreadCount": 3 }
},
"unreadMessageCount": { "user_456": 3 },
"totalUnreadMessageCount": 3
},
"timestamp": "..."
}
conversations is keyed by the other user ID and contains the latest known message preview plus
unread count for that conversation.
chat.message.read
Mark a message as read.
{ "messageId": "msg_123" }
{ "type": "chat.message.read", "payload": { "messageId": "msg_123", "senderId": "user_123", "recipientId": "user_456", "readAt": "..." }, "timestamp": "..." }
chat.typing
Send typing indicator.
{ "recipientId": "user_456", "isTyping": true }
{ "type": "chat.typing", "payload": { "fromUserId": "user_123", "recipientId": "user_456", "isTyping": true }, "timestamp": "..." }
{ "type": "chat.typing", "payload": { "status": "sent", "fromUserId": "user_123", "recipientId": "user_456", "isTyping": true }, "timestamp": "..." }
chat.location.share.start
Start relaying your live location to another user in chat. This requires an active connection with the recipient.
{
"recipientId": "user_456",
"fromUserId": "user_123",
"location": {
"lat": 6.5244,
"lng": 3.3792,
"accuracyMeters": 6.2,
"heading": 120.0,
"updatedAt": "2026-04-15T22:00:00Z"
}
}
{
"type": "chat.location.share.start",
"payload": {
"fromUserId": "user_123",
"recipientId": "user_456",
"location": {
"lat": 6.5244,
"lng": 3.3792,
"accuracyMeters": 6.2,
"heading": 120.0,
"updatedAt": "2026-04-15T22:00:00Z"
}
},
"timestamp": "..."
}
{ "type": "chat.location.share.start", "payload": { "status": "sent", "fromUserId": "user_123", "recipientId": "user_456", "location": { "...": "..." } }, "timestamp": "..." }
fromUserId from the authenticated socket user and does not trust the
client-provided value.
missing_recipientId, user_not_found,
active_connection_required, invalid_location.
chat.location.updated
Relay an updated shared location during an active chat location share. This requires an active connection with the recipient.
{
"recipientId": "user_456",
"fromUserId": "user_123",
"location": {
"lat": 6.5244,
"lng": 3.3792,
"accuracyMeters": 4.8,
"heading": 118.0,
"updatedAt": "2026-04-15T22:00:05Z"
}
}
{
"type": "chat.location.updated",
"payload": {
"fromUserId": "user_123",
"recipientId": "user_456",
"location": {
"lat": 6.5244,
"lng": 3.3792,
"accuracyMeters": 4.8,
"heading": 118.0,
"updatedAt": "2026-04-15T22:00:05Z"
}
},
"timestamp": "..."
}
{ "type": "chat.location.updated", "payload": { "status": "sent", "fromUserId": "user_123", "recipientId": "user_456", "location": { "...": "..." } }, "timestamp": "..." }
missing_recipientId, user_not_found,
active_connection_required, invalid_location.
chat.location.share.stop
Stop relaying your live location to another user in chat. This requires an active connection with the recipient.
{
"recipientId": "user_456",
"fromUserId": "user_123"
}
{
"type": "chat.location.share.stop",
"payload": {
"fromUserId": "user_123",
"recipientId": "user_456"
},
"timestamp": "..."
}
{ "type": "chat.location.share.stop", "payload": { "status": "sent", "fromUserId": "user_123", "recipientId": "user_456" }, "timestamp": "..." }
missing_recipientId, user_not_found,
active_connection_required.
Gift Events
GISBI now uses sponsored pair rewards only. Legacy peer-to-peer gift actions such as gift.create, gift.unlock.confirm, gift.unlock.execute, gift.combine.confirm, and gift.redeem are disabled and return peer_gifts_disabled.
gift.history.fetch
Fetch the authenticated user's sponsored gift participation history. This returns rewards the user has participated in as either side of the pair, including current and past rewards.
{
"limit": 20,
"cursor": "opaque_cursor_from_previous_response"
}
{
"type": "gift.history.fetch",
"payload": {
"limit": 20,
"nextCursor": "opaque_cursor_for_next_page",
"sponsoredGifts": [
{
"giftId": "sgg_123",
"partnerName": "Bola",
"partnerAvatarUrl": "/media/avatars/bola.png",
"partnerClaimableNow": true,
"venueName": "Northline Coffee, Victoria Island",
"venueAddress": "2 Adeola Odeku Street, Lagos",
"codeExpiresAt": "2026-04-20T18:00:00Z",
"historyStatus": "claimed"
},
{
"giftId": "sgg_456",
"historyStatus": "disqualified"
}
]
},
"timestamp": "..."
}
historyStatus for user-facing history labels. The raw backend lifecycle remains in status and legacyStatus.
cursor and nextCursor to page large histories instead of loading every gift at once.
gift.sponsored.granted
Broadcast when the platform grants a sponsored pair reward after connection acceptance or a verified proximity milestone.
{
"type": "gift.sponsored.granted",
"payload": {
"giftId": "sgg_123",
"giftType": "sponsoredPairReward",
"connectionId": "conn_abc",
"campaignId": "camp_123",
"brand": {
"brandId": "brand_123",
"name": "Skyline Lounge",
"slug": "skyline-lounge",
"logo": "/media/brands/skyline.png",
"category": "lounge"
},
"userAId": "user_123",
"userBId": "user_456",
"partnerUserId": "user_456",
"partnerName": "Bola",
"partnerAvatarUrl": "/media/avatars/bola.png",
"partnerClaimableNow": false,
"triggerType": "proximityMilestone",
"milestoneMinutes": 15,
"minTotalTogetherMinutes": 6000,
"minDailyTogetherMinutes": 30,
"minDistinctQualifyingDays": 20,
"requireActiveProximityForClaim": true,
"qualifyingTogetherMinutes": 4380,
"requiredTogetherMinutes": 6000,
"qualifyingDaysCount": 16,
"requiredQualifyingDays": 20,
"claimableNow": false,
"title": "Date Night Reward",
"description": "Free appetizer and two mocktails",
"terms": "Valid while stock lasts",
"venueName": "Skyline Lounge",
"venueAddress": "2 Marina Road, Lagos",
"codeExpiresAt": "...",
"status": "active",
"createdAt": "...",
"expiresAt": "..."
},
"timestamp": "..."
}
minDailyTogetherMinutes is set, the backend only counts daily proximity totals that meet that threshold when evaluating grant and claim eligibility. Distinct-day rules and active-proximity-at-claim rules can also be enforced.
safety.report
Report another user.
{ "reportedUserId": "user_456", "reason": "Harassment" }
{
"type": "safety.report",
"payload": {
"reportId": "sr_123",
"reportedUserId": "user_456",
"reason": "Harassment",
"status": "open",
"createdAt": "..."
},
"timestamp": "..."
}
safety.block
Block another user.
{ "blockedUserId": "user_456" }
{ "type": "safety.block", "payload": { "blockedUserId": "user_456" }, "timestamp": "..." }
user.profile.updated
Server-only event emitted after profile updates (REST or system changes).
{ "user": { ...User } }
Bootstrap Payload
The session.bootstrap payload includes the full client state.
incomingRequests and sentRequests include all currently pending requests for the
authenticated user. Timed-out requests are not returned there; clients should rely on
connection.request.missed for realtime removal/update when a request expires.
requests array is also included. It combines incoming and outgoing request history,
sorted with pending requests first by createdAt descending, then historical requests by
resolvedAt descending.
conversations and unreadMessageCount are keyed by the other user's ID so chat lists
can render previews and per-thread badges without scanning the flat message array on the client.
{
"currentUser": { ...User },
"presence": { ...Presence },
"nearbyUsers": [ ...NearbyUser ],
"incomingRequests": [ ...ConnectionRequest ],
"sentRequests": [ ...ConnectionRequest ],
"requests": [ ...ConnectionRequest ],
"activeConnections": [ ...Connection ],
"closedConnections": [ ...Connection ],
"messages": [ ...Message ],
"conversations": {
"user_456": { "lastMessage": { ...Message }, "unreadCount": 3 }
},
"unreadMessageCount": { "user_456": 3 },
"totalUnreadMessageCount": 3,
"gifts": [],
"sponsoredGifts": [ ...SponsoredGift ],
"sponsoredGiftsNextCursor": "opaque_cursor_or_null",
"proximitySession": {
"connectionId": "conn_abc",
"accumulatedSeconds": 420,
"status": "paused",
"unlockedMilestones": [5],
"bleToken": "a3f9c2e1",
"bleTokenExpiresAt": "2026-04-17T14:32:00Z"
},
"blockedUserIds": [ "user_123", "user_456" ],
"safetyReports": [ ...SafetyReport ]
}
presence now includes isOnRadar so the client can restore radar visibility state on reconnect.
sponsoredGifts contains the current pair rewards already granted to the authenticated user through sponsor campaigns. The legacy gifts field remains in bootstrap only for backward compatibility and is now always an empty array.
Errors
Socket errors are delivered as a dedicated event:
{
"type": "error",
"payload": {
"code": "missing_userId",
"message": "userId is required",
"domain": "connections",
"requestId": "req_123"
},
"timestamp": "..."
}
REST errors typically return:
{ "detail": "message", "code": "optional_code" }