Appearance
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
directconversations,titleandavatarUrlarenull. Show the other member's name and avatar. unreadCount= messages from others withseq > caller.lastReadSeq.
GET /conversations
Paginated, sorted by updatedAt descending. Query parameters:
| Parameter | Description |
|---|---|
updatedSince | Only conversations changed after this time. Use for incremental sync. |
includeArchived | Default 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)
| Method | Path | Notes |
|---|---|---|
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}/members | Admin 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"
}| Field | Notes |
|---|---|
seq | Assigned by the server. Starts at 1, increases by exactly 1 per message in the conversation. The only ordering key. |
clientMessageId | UUID v4 you generate. Idempotency key. |
type | text, image, audio, video, document, system. |
body | Up 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
}201with the newMessage.200with the originalMessageif thisclientMessageIdwas already used by you.403 NOT_FRIENDSfor 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
| Parameter | Returns | Use for |
|---|---|---|
| (none) | Latest limit messages, newest first | Opening a chat with no local data |
beforeSeq=N | seq < N, newest first | Scrolling up |
afterSeq=N | seq > N, oldest first | Catching up after a gap or reconnect |
limit | Default 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:
| Status | Rule (direct) | Rule (group, A3) |
|---|---|---|
| Sent ✓ | Has a seq | Has a seq |
| Delivered ✓✓ | Other member's lastDeliveredSeq ≥ S | Every other member's lastDeliveredSeq ≥ S |
| Read (blue) ✓✓ | Other member's lastReadSeq ≥ S | Every other member's lastReadSeq ≥ S |
Edit, delete, react (A4, A5)
| Method | Path | Rules |
|---|---|---|
PATCH | /messages/{id} | { "body" }. Sender only, text only, within 15 minutes. Else 422 EDIT_WINDOW_EXPIRED. |
DELETE | /messages/{id}?scope=me | Hides the message for the caller only. |
DELETE | /messages/{id}?scope=everyone | Sender 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}/reactions | Removes 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.