GISBI API Docs

Full API and Socket Documentation

This guide is generated directly from the backend contract. It includes every request key, response field, and socket event so frontend agents can build without guessing.

Base URL

https://api.ockde.com

All REST requests are relative to this base.

WebSocket

wss://api.ockde.com/ws

Single socket endpoint for all realtime events.

OpenAPI

/docs.json

Machine-readable schema for clients.

Swagger UI

/docs/

Interactive API explorer.

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>
Tokens are issued by POST /auth/social-login.

REST API

POST /auth/social-login

Social login for Google or Apple. Returns JWT tokens and a user profile.

Request Body
FieldTypeRequiredDescription
providerstringyesProvider name. Expected: google or apple.
idTokenstringyesProvider ID token.
deviceTokenstringnoPush device token (optional).
devicePlatformstringnoDevice platform (optional).
Response 200
FieldTypeNullableDescription
accessTokenstringnoJWT access token.
refreshTokenstringnoJWT refresh token.
firebaseTokenstringyesFirebase custom token when Firebase is enabled.
userUsernoUser 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).

Request Body
FieldTypeRequiredDescription
deviceTokenstringnoPush device token to deactivate.
refreshTokenstringnoRefresh token to blacklist.
Response 200
{ "status": "ok" }

POST /auth/delete-account

Soft delete the authenticated account.

Request Body
FieldTypeRequiredDescription
reasonstringyesReason for deletion.
Response 200
{ "status": "ok" }

POST /devices/register

Register or refresh an FCM/APNS token for push notifications.

Request Body
{
  "token": "fcm_or_apns_token",
  "platform": "android"
}
Response 200
{
  "status": "ok",
  "device": {
    "token": "fcm_or_apns_token",
    "platform": "android",
    "isActive": true
  }
}

PATCH /users/profile

Update profile fields for the authenticated user.

Request Body
FieldTypeRequiredDescription
namestringnoDisplay name.
avatarstring (URL) or nullnoProfile image URL. Empty string clears to null.
ageintegernoAge (0+). Use null to clear.
genderstring or nullnoGender label. Use null to clear.
Response 200
{
  "id": "user_123",
  "name": "Ada Lovelace",
  "age": 28,
  "avatar": "https://cdn.example.com/ada.png",
  "gender": "female",
  "vibe": null,
  "vibeExpiresAt": null
}
A matching socket event user.profile.updated is emitted immediately after a successful update.

POST /connections/:connectionId/proximity/token

Issue a short-lived BLE token for the authenticated participant in an active connection.

Response 200
{
  "token": "a3f9c2e1",
  "expiresAt": "2026-04-17T14:32:00Z",
  "connectionId": "conn_abc"
}
Tokens are tied to a connection participant, expire after about 90 seconds, and issuing a new token invalidates the previous active token for that participant.
This token flow is the proximity proof used by the platform to power connection-duration milestones for sponsored gifts. The token itself does not unlock a gift; it enables verified together-time tracking for the connection.
On reconnect, the current active BLE token may also be returned inside session.bootstrap so the client can resume advertising without calling this endpoint again immediately.

GET /connections/:connectionId/proximity/summary

Return accumulated proximity progress, milestone state, and recent session history for an active connection.

Response 200
{
  "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

FieldTypeNullableDescription
idstringnoUser ID (e.g. user_...).
namestringnoDisplay name.
ageintegeryesAge.
avatarstring (URL)yesAvatar URL.
genderstringyesGender label.
vibestringyesCurrent vibe text.
vibeExpiresAtstring (ISO)yesVibe expiration timestamp.

Presence

FieldTypeNullableDescription
isLivebooleannoLive status.
locationobjectyesLocation object or null.
Location FieldTypeDescription
latnumberLatitude.
lngnumberLongitude.
accuracyMetersnumberOptional GPS accuracy in meters.
headingnumberOptional device heading in degrees.
speednumberOptional device speed.
updatedAtstring (ISO)Last update timestamp.

NearbyUser

FieldTypeDescription
userUserUser summary.
distancenumberDistance in meters.
anglenumberBearing angle (0-360).
isLivebooleanWhether the nearby user is currently live.
sharedInterestsarray[string]Shared interests between the viewer and the nearby user. In the current backend this is derived from overlapping vibe tags/text.
locationLocationLatest nearby user location snapshot, including freshness metadata.

ConnectionRequest

FieldTypeDescription
idstringRequest ID alias for list rendering.
requestIdstringRequest ID.
fromUserIdstringSender user ID.
toUserIdstringRecipient user ID.
senderIdstringAlias for the request sender user ID.
receiverIdstringAlias for the request receiver user ID.
userUserOther user.
userIdstringOther user ID.
namestringOther user display name.
ageintegerOther user age.
avatarstring (URL) or nullOther user avatar, always present even when null.
snippetstringShort request summary for list UIs.
directionstringincoming | outgoing
statusstringpending | accepted | declined | missed | closed
legacyStatusstringOriginal server-side status value for backwards compatibility.
closureReasonstring or nullOptional reason when the request was closed before activation.
createdAtstring (ISO)Created timestamp.
resolvedAtstring (ISO) or nullWhen the request stopped being pending.
expiresAtstring (ISO) or nullPending request expiry timestamp. Null once the request is no longer pending.
Pending connection requests expire after 10 minutes. When that happens, the server marks them as missed and emits connection.request.missed to both users.

Connection

FieldTypeDescription
connectionIdstringConnection ID.
userUserOther user.
isOnlinebooleanWhether the other participant currently has an active GISBI socket session.
accumulatedMinutesintegerVerified together-time accumulated on the active connection.
nextMilestoneMinutesinteger or nullNext proximity milestone for the connection, when available.
directionstringincoming | outgoing, based on who initiated the connection request.
typestringAlways connection for closed/active connection history items.
statusstringaccepted | closed
legacyStatusstringUnderlying connection status value (active | closed).
senderIdstringUser who originally sent the request that became this connection.
receiverIdstringUser who originally received the request that became this connection.
namestringOther user display name.
ageintegerOther user age.
avatarstring (URL) or nullOther user avatar, always present even when null.
snippetstringOther user vibe text.
closureReasonstring or nullOptional reason provided when the connection was closed.
closedByUserIdstring or nullUser who closed the connection, when known.
connectedLocationobject or nullSnapshot of the pair connection point when live locations were available at acceptance time.
createdAtstring (ISO)Created timestamp.
resolvedAtstring (ISO)When the request became an accepted connection.
closedAtstring (ISO) or nullClosed timestamp.

ProximitySession

FieldTypeDescription
connectionIdstringActive connection being tracked.
accumulatedSecondsintegerTotal verified together-time accumulated across all valid runs.
statusstringactive | paused | ended
currentSessionSecondsintegerLength of the current continuous verified run.
unlockedMilestonesinteger[]Together-time milestones already reached, in minutes.
bleTokenstringThe participant's current BLE advertising token when available in bootstrap state.
bleTokenExpiresAtstring (ISO)When the current BLE token expires.
This model is the backend source of truth for future gift milestone logic. Frontend should treat it as server-owned progression, not a client-computed timer.

Message

FieldTypeDescription
messageIdstringMessage ID.
senderIdstringSender user ID.
recipientIdstringRecipient user ID.
senderUserSender profile.
recipientUserRecipient profile.
contentstringMessage body.
createdAtstring (ISO)Created timestamp.
readAtstring (ISO) or nullRead timestamp.

SponsoredGift

FieldTypeDescription
giftIdstringSponsored grant ID.
giftTypestringAlways sponsoredPairReward.
connectionIdstringPair connection that earned the reward.
campaignIdstringSponsor campaign ID.
brandobjectSponsor metadata.
userAIdstringFirst participant in the pair.
userBIdstringSecond participant in the pair.
partnerUserIdstring or nullThe other user in the pair for the authenticated viewer.
partnerNamestring or nullDisplay name for the other user in the pair.
partnerAvatarUrlstring (URL) or nullProfile image URL for the other user in the pair.
partnerClaimableNowboolean or nullPair-level readiness flag mirrored for the partner-focused gift card UI.
triggerTypestringconnectionAccepted or proximityMilestone.
milestoneMinutesinteger or nullMilestone that triggered the reward, when applicable.
minTotalTogetherMinutesintegerMinimum cumulative verified together-time required for this campaign or grant.
minDailyTogetherMinutesinteger or nullOptional per-day threshold. When set, only days that meet this verified time count toward the cumulative total.
minDistinctQualifyingDaysintegerMinimum number of qualifying days required when daily proximity thresholds are part of the rule.
requireActiveProximityForClaimbooleanWhen true, the pair must still be in an active verified proximity session when the reward is verified or redeemed.
qualifyingTogetherMinutesinteger or nullMinutes currently counted toward this campaign after daily-threshold filtering.
requiredTogetherMinutesinteger or nullTotal minutes the pair must satisfy for this campaign to be claimable.
qualifyingDaysCountinteger or nullHow many calendar days currently meet the campaign's daily together-time rule.
requiredQualifyingDaysinteger or nullHow many qualifying days the campaign requires.
claimableNowboolean or nullWhether the pair currently satisfies the campaign proximity rules for claim.
claimStagestringgranted, verified, redeemed, expired, or cancelled.
historyStatusstringUser-facing history label. Current values include pending, available, verified, claimed, and disqualified.
sponsorConfidenceScoreinteger0-100 confidence score derived from the pair's verified together-time, qualifying days, and live proximity rule fit.
verificationContextobjectSponsor-facing proof context including current and historical qualifying minutes/days.
riskSignalsobjectBoolean risk flags that explain why a reward is or is not safely claimable right now.
titlestringReward title snapshot.
descriptionstringWhat the pair receives from the sponsor.
termsstringReward terms snapshot.
rewardImageUrlstring (URL) or nullOptional reward image for the reveal card.
brandTaglinestring or nullOptional sponsor/campaign line to show warmth on the card.
campaignMessagestring or nullAlias of the campaign tagline/message.
venueNamestring or nullWhere the reward is redeemed. Falls back to the brand name when no venue name is configured.
venueAddressstring or nullVenue address or city for redemption guidance.
statusstringRaw backend lifecycle status: active | redeemed | expired | cancelled
legacyStatusstringAlias of the raw backend lifecycle status for compatibility.
createdAtstring (ISO)Granted timestamp.
expiresAtstring (ISO) or nullOverall reward expiry timestamp.
codeExpiresAtstring (ISO) or nullRedemption code expiry timestamp, tracked separately from the broader reward lifetime.

SafetyReport

FieldTypeDescription
reportIdstringReport ID.
reportedUserIdstringReported user ID.
reasonstringReport reason.
statusstringopen | reviewing | resolved
createdAtstring (ISO)Created timestamp.

WebSocket Protocol

All socket messages are JSON with a common envelope.

Client to Server
{
  "type": "session.start",
  "payload": {},
  "requestId": "req_123",
  "correlationId": "corr_123"
}
Server to Client
{
  "type": "session.bootstrap",
  "payload": { "currentUser": { "...": "..." } },
  "timestamp": "2026-04-14T14:12:20Z",
  "requestId": "req_123",
  "correlationId": "corr_123"
}
If 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.

Request Payload
{ "lastSeenAt": "2026-04-19T08:00:00Z" }
Responses
{ "type": "session.connected", "payload": {}, "timestamp": "..." }
{ "type": "session.bootstrap", "payload": { ... }, "timestamp": "..." }
{ "type": "chat.message.created", "payload": { ...Message }, "timestamp": "..." }
If lastSeenAt is provided, the server replays any incoming messages created after that timestamp.

presence.location_update

Send live location updates and receive nearby users.

Request Payload
{
  "lat": 6.5244,
  "lng": 3.3792,
  "accuracyMeters": 6.2,
  "heading": 120.0,
  "speed": 1.7
}
Responses
{
  "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": "..."
}
Nearby users are recomputed from live coordinates on every 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.

Request Payload
{ "isOnRadar": false }
Responses
{ "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.

Request Payload
{ "userId": "user_456" }
Response to Sender
{ "type": "connection.request.send", "payload": { "request": { ...ConnectionRequest } }, "timestamp": "..." }
Response to Recipient
{ "type": "connection.request.send", "payload": { "request": { ...ConnectionRequest } }, "timestamp": "..." }
If an older pending request between the same users has already timed out, the server first emits connection.request.missed for the stale request and then emits the new connection.request.send.
The nested 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.

Request Payload
{ "requestId": "req_123" }
Response (both users)
{
  "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": "..."
}
If the request has already exceeded the 10 minute timeout, the server marks it as missed, emits connection.request.missed to both users, and returns an error payload with code request_expired.
When both users have live location at accept time, the server stores a connection location snapshot and uses it later to evaluate sponsor campaign eligibility.

connection.request.reject

Reject an incoming connection request.

Request Payload
{ "requestId": "req_123" }
Response (both users)
{ "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.

Response (both users)
{ "type": "connection.request.missed", "payload": { "requestId": "req_123", "fromUserId": "user_123", "toUserId": "user_456", "direction": "incoming", "status": "missed", "resolvedAt": "...", "request": { ...ConnectionRequest } }, "timestamp": "..." }
This can be emitted by scheduled expiry processing or immediately when the server discovers an already expired request during a client action.

connection.close

Close an active connection.

Request Payload
{ "connectionId": "conn_123", "closureReason": "user_left" }
Response (both users)
{ "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.

Request Payload
{}
Response
{
  "type": "connection.history.fetch",
  "payload": {
    "activeConnections": [ ...Connection ],
    "closedConnections": [ ...Connection ]
  },
  "timestamp": "..."
}

connection.ring

Ring another user.

Request Payload
{ "userId": "user_456" }
Response to Recipient
{ "type": "connection.ring", "payload": { "fromUserId": "user_123", "recipientId": "user_456", "fromUser": { ...User } }, "timestamp": "..." }
Response to Sender
{ "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.

Request Payload
{ "connectionId": "conn_abc", "token": "a3f9c2e1", "rssi": -68 }
Direct ACK to Sender
{
  "type": "proximity.session.start",
  "payload": {
    "sessionActive": false,
    "yourTokenReceived": true,
    "waitingForPartner": true
  },
  "timestamp": "..."
}
Targeted Push to Partner
{
  "type": "proximity.partner.confirmed",
  "payload": { "connectionId": "conn_abc" },
  "timestamp": "..."
}
Response (both users)
{
  "type": "proximity.session.updated",
  "payload": {
    "connectionId": "conn_abc",
    "accumulatedSeconds": 420,
    "sessionActive": true,
    "currentSessionSeconds": 120,
    "unlockedMilestones": [5, 15],
    "nextMilestoneMinutes": 30
  },
  "timestamp": "..."
}
A valid start does not mean a gift is guaranteed. It starts or resumes a verified proximity run whose accumulated time can later unlock gift milestones, subject to availability and allocation rules.
Repeating 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.

Request Payload
{ "connectionId": "conn_abc", "token": "a3f9c2e1", "rssi": -68 }
Direct ACK to Sender
{
  "type": "proximity.heartbeat",
  "payload": {
    "sessionActive": true,
    "accumulatedSeconds": 420
  },
  "timestamp": "..."
}
Server Pushes
{
  "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": "..." }
The server advances time only when both participants submit valid proximity events within the rolling heartbeat window. Heartbeats with RSSI above -40 dBm are rejected.
Milestones are only unlocked after verified together-time passes the configured minimum threshold. The emitted proximity.milestone.reached event is the signal gift systems should listen to for milestone-based rewards.

proximity.session.end

Explicitly end or pause a proximity run when BLE detection stops on the client.

Request Payload
{ "connectionId": "conn_abc" }
Response (both users)
{
  "type": "proximity.session.updated",
  "payload": {
    "connectionId": "conn_abc",
    "sessionActive": false,
    "accumulatedSeconds": 142,
    "currentSessionSeconds": 0,
    "unlockedMilestones": [],
    "nextMilestoneMinutes": 5,
    "gracePeriodSeconds": 30
  },
  "timestamp": "..."
}
Even without this event, the server pauses or ends the session when heartbeats are missed long enough. Sending it helps the UI sync faster when users separate.

chat.message.created

Send a chat message to another user.

Request Payload
{ "recipientId": "user_456", "content": "Hello there" }
Response (both users)
{ "type": "chat.message.created", "payload": { ...Message }, "timestamp": "..." }
If the recipient socket is offline, the server also attempts a push notification.

vibe.expired

Server-only event emitted when a user's vibe expires during background processing.

Payload
{ "expiredAt": "2026-04-19T08:00:00Z" }

chat.history.fetch

Fetch recent message history for the authenticated user.

Request Payload
{}
Response
{
  "type": "chat.history.fetch",
  "payload": {
    "messages": [ ...Message ],
    "conversations": {
      "user_456": { "lastMessage": { ...Message }, "unreadCount": 3 }
    },
    "unreadMessageCount": { "user_456": 3 },
    "totalUnreadMessageCount": 3
  },
  "timestamp": "..."
}
This returns the same recent message set used in the bootstrap payload, currently capped at the latest 50 messages.
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.

Request Payload
{ "messageId": "msg_123" }
Response (both users)
{ "type": "chat.message.read", "payload": { "messageId": "msg_123", "senderId": "user_123", "recipientId": "user_456", "readAt": "..." }, "timestamp": "..." }

chat.typing

Send typing indicator.

Request Payload
{ "recipientId": "user_456", "isTyping": true }
Response to Recipient
{ "type": "chat.typing", "payload": { "fromUserId": "user_123", "recipientId": "user_456", "isTyping": true }, "timestamp": "..." }
Response to Sender
{ "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.

Request Payload
{
  "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"
  }
}
Response to Recipient
{
  "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": "..."
}
Response to Sender
{ "type": "chat.location.share.start", "payload": { "status": "sent", "fromUserId": "user_123", "recipientId": "user_456", "location": { "...": "..." } }, "timestamp": "..." }
The server derives fromUserId from the authenticated socket user and does not trust the client-provided value.
Possible socket errors: 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.

Request Payload
{
  "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"
  }
}
Response to Recipient
{
  "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": "..."
}
Response to Sender
{ "type": "chat.location.updated", "payload": { "status": "sent", "fromUserId": "user_123", "recipientId": "user_456", "location": { "...": "..." } }, "timestamp": "..." }
Possible socket errors: 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.

Request Payload
{
  "recipientId": "user_456",
  "fromUserId": "user_123"
}
Response to Recipient
{
  "type": "chat.location.share.stop",
  "payload": {
    "fromUserId": "user_123",
    "recipientId": "user_456"
  },
  "timestamp": "..."
}
Response to Sender
{ "type": "chat.location.share.stop", "payload": { "status": "sent", "fromUserId": "user_123", "recipientId": "user_456" }, "timestamp": "..." }
Possible socket errors: 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.

Request Payload
{
  "limit": 20,
  "cursor": "opaque_cursor_from_previous_response"
}
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": "..."
}
Use historyStatus for user-facing history labels. The raw backend lifecycle remains in status and legacyStatus.
Use 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.

Response (both users)
{
  "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": "..."
}
Sponsored rewards are pair-based, not guaranteed, and are allocated only when an active sponsor campaign passes availability, pair-limit, and location checks.
This is a server-driven event only. Clients do not create sponsored gifts directly; they receive them after connection acceptance or a qualifying proximity milestone.
Campaigns can now require cumulative verified together-time. If 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.

Request Payload
{ "reportedUserId": "user_456", "reason": "Harassment" }
Response
{
  "type": "safety.report",
  "payload": {
    "reportId": "sr_123",
    "reportedUserId": "user_456",
    "reason": "Harassment",
    "status": "open",
    "createdAt": "..."
  },
  "timestamp": "..."
}

safety.block

Block another user.

Request Payload
{ "blockedUserId": "user_456" }
Response
{ "type": "safety.block", "payload": { "blockedUserId": "user_456" }, "timestamp": "..." }

user.profile.updated

Server-only event emitted after profile updates (REST or system changes).

Payload
{ "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.
A unified 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.
Bootstrap includes the current proximity milestone snapshot so the app can restore in-progress together-time state after reconnect. This same state will be reused by milestone-driven gift features.
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" }
Common error codes include: missing_userId missing_recipientId user_not_found invalid_location missing_connectionId missing_token missing_rssi invalid_token invalid_rssi not_allowed active_connection_exists active_connection_required missing_requestId missing_giftId distance_exceeded not_ready