Skip to content

Conversations & Messages ​

Conversations ​

A conversation is either direct (two friends) or group (A3).

json
{
  "id": "01JE…",
  "type": "direct",
  "title": null,
  "avatarUrl": null,
  "members": [
    { "user": { "id": "01JB…", "displayName": "Ana Souza", "username": "ana_souza", "avatarUrl": null, "isDeleted": false },
      "role": "member", "lastDeliveredSeq": 42, "lastReadSeq": 42 },
    { "user": { "id": "01JC…", "displayName": "Ben Tan", "username": "ben_tan", "avatarUrl": null, "isDeleted": false },
      "role": "member", "lastDeliveredSeq": 42, "lastReadSeq": 40 }
  ],
  "lastMessage": { "…": "Message or null" },
  "lastSeq": 42,
  "unreadCount": 0,
  "mutedUntil": null,
  "archived": false,
  "createdAt": "2026-10-22T14:05:30.000Z",
  "updatedAt": "2026-10-22T14:20:11.482Z"
}
  • For direct conversations, title and avatarUrl are null. Show the other member's name and avatar.
  • unreadCount = messages from others with seq > caller.lastReadSeq.

GET /conversations ​

Paginated, sorted by updatedAt descending. Query parameters:

ParameterDescription
updatedSinceOnly conversations changed after this time. Use for incremental sync.
includeArchivedDefault false.

POST /conversations/direct ​

json
{ "userId": "01JC…" }

Get-or-create. 200 if it exists, 201 if created. Requires friendship: 403 NOT_FRIENDS otherwise. When created, the other user receives conversation.created.

GET /conversations/{id} ​

PATCH /conversations/{id}/settings ​

Per-user: { "mutedUntil": "2026-10-23T00:00:00.000Z", "archived": true }. Muted conversations send no push notifications. 204.

Groups (A3) ​

MethodPathNotes
POST/conversations/group{ "title", "memberIds", "avatarMediaId" }. All members must be friends of the creator. Creator is admin. Max 256 members.
PATCH/conversations/{id}Admin only. title, avatarMediaId.
POST/conversations/{id}/membersAdmin only. { "userIds" }. New members must be friends of the admin.
DELETE/conversations/{id}/members/{userId}Admin removes anyone; anyone removes themself (leave). If the last admin leaves, the oldest member becomes admin.
PATCH/conversations/{id}/members/{userId}Admin only. { "role": "admin" | "member" }.

Group changes create system messages and send conversation.updated to all members. A removed member receives conversation.removed.

Messages ​

json
{
  "id": "01JF…",
  "conversationId": "01JE…",
  "seq": 42,
  "senderId": "01JB…",
  "clientMessageId": "6f1c2c1e-6b5f-4d0e-9a3b-0a8f2d1b7c55",
  "type": "text",
  "body": "See you at 8?",
  "media": null,
  "replyTo": null,
  "reactions": [],
  "editedAt": null,
  "deletedAt": null,
  "createdAt": "2026-10-22T14:20:11.482Z"
}
FieldNotes
seqAssigned by the server. Starts at 1, increases by exactly 1 per message in the conversation. The only ordering key.
clientMessageIdUUID v4 you generate. Idempotency key.
typetext, image, audio, video, document, system.
bodyUp to 4,096 characters. null when deleted. For system: a JSON string, for example {"event":"member_added","actorId":"…","targetIds":["…"]}.
replyTo(A4) { "id", "senderId", "type", "body" } preview of the quoted message, or null.

Because seq has no gaps, a jump from 40 to 43 means you missed 41 and 42. Fetch them with afterSeq=40.

Sending ​

Send over the WebSocket with message.send. If the socket is not available, use REST:

POST /conversations/{id}/messages ​

json
{
  "clientMessageId": "6f1c2c1e-6b5f-4d0e-9a3b-0a8f2d1b7c55",
  "type": "text",
  "body": "See you at 8?",
  "mediaId": null,
  "replyToId": null
}
  • 201 with the new Message.
  • 200 with the original Message if this clientMessageId was already used by you.
  • 403 NOT_FRIENDS for a direct conversation with someone who is no longer your friend.
  • 403 PROFILE_INCOMPLETE, 404 CONVERSATION_NOT_FOUND, 429 RATE_LIMITED.

History ​

GET /conversations/{id}/messages ​

ParameterReturnsUse for
(none)Latest limit messages, newest firstOpening a chat with no local data
beforeSeq=Nseq < N, newest firstScrolling up
afterSeq=Nseq > N, oldest firstCatching up after a gap or reconnect
limitDefault 50, max 100

beforeSeq and afterSeq cannot be combined. Response: { "data": [Message], "page": { "hasMore": true } }.

Messages the caller deleted "for me" are not returned. Messages deleted "for everyone" are returned with deletedAt set and body: null — show "This message was deleted".

Receipts ​

Receipts are watermarks: "I have received / read everything up to seq". Send them over the WebSocket (receipt.delivered, receipt.read) or with REST:

POST /conversations/{id}/receipts ​

json
{ "type": "read", "seq": 42 }

204. Watermarks only move forward; a lower seq is ignored. read also moves delivered. Other members receive receipt.updated.

Compute the status of your own message with seq = S:

StatusRule (direct)Rule (group, A3)
Sent ✓Has a seqHas a seq
Delivered ✓✓Other member's lastDeliveredSeq ≥ SEvery other member's lastDeliveredSeq ≥ S
Read (blue) ✓✓Other member's lastReadSeq ≥ SEvery other member's lastReadSeq ≥ S

Edit, delete, react (A4, A5) ​

MethodPathRules
PATCH/messages/{id}{ "body" }. Sender only, text only, within 15 minutes. Else 422 EDIT_WINDOW_EXPIRED.
DELETE/messages/{id}?scope=meHides the message for the caller only.
DELETE/messages/{id}?scope=everyoneSender only, within 48 hours. Sets deletedAt, clears body and media.
PUT/messages/{id}/reactions{ "emoji": "👍" }. One reaction per user; replaces the previous one.
DELETE/messages/{id}/reactionsRemoves the caller's reaction.

Edits, deletes for everyone, and reaction changes send message.updated to all members.

Search (A6) ​

GET /search/messages?q=&conversationId= ​

Full-text search over text messages the caller can see. conversationId is optional. Paginated, newest first. Minimum 2 characters.

Questions? Reply to your invitation email.