Chats
Direct (1:1) and group chats with end-to-end encrypted metadata; participant roles carry the per-user key chain and preferences.
Service: messages.chats
A chat is a conversation container. kind: "direct" is a 1:1 conversation (no name — the client builds the title from the other participant); kind: "group" is multi-party with an encrypted name/icon/description. Membership lives in the inline chat_user_role collection: every participant (including the creator) has a row carrying their wrapped copy of the chat content key plus their per-chat preferences (pin, mute, read cursor).
All methods use the JSON-RPC 2.0 protocol over WebSocket. See Enbox API for transport details, error codes, query operators, and encryption model.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
— | Primary key (snowflake). Read-only, auto-generated |
owner_id |
string |
— | Chat creator’s user id. Read-only, auto-set on create |
owner_wrapped_key |
string |
✅ | Chat content key wrapped by the creator’s account key (base64). Required on create |
kind |
string |
✅ | direct (1:1) or group (multi-party). Immutable after creation |
enc_name |
string? |
Encrypted group display name. Plain string after decryption. null for direct chats |
|
enc_icon |
string? |
Encrypted group icon (Bootstrap Icons name). null for direct chats |
|
enc_description |
string? |
Encrypted group description. null for direct chats |
|
member_count |
integer |
— | Number of participants. Read-only, computed |
unread_count |
integer |
— | Messages newer than your read cursor, excluding your own. Read-only, computed per caller |
last_message_id |
string? |
— | Snowflake id of the newest message. Read-only, computed. String-serialized (values exceed 2^53) |
created_at |
string |
— | ISO 8601 creation timestamp. Read-only |
updated_at |
string |
— | ISO 8601 last update timestamp. Read-only |
chat_user_role |
ChatUserRole[] | ✅ | Participant rows. Seeded at creation (see below) |
Filterable: kind
Note: On create,
chat_user_rolemust list every participant (the encryption middleware prepends the creator’s ownadminrow automatically). Each non-creator entry is{ user_id, role: "member", account_public_key }— the middleware wraps the chat content key for each recipient via ECDH intowrapped_content_keyand stripsaccount_public_keybefore the request leaves the client. Direct chats: exactly one other participant. The whole create runs in a single transaction.
ChatUserRole
Per-participant membership row. Reads flow inline in the chat payload; per-user preference writes (pin / mute) go through the dedicated chat_user_roles endpoint — the inline collection is read-only after creation.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
— | Primary key. Read-only. Omit when creating |
chat_id |
string |
— | Parent chat id. Read-only (assigned at creation; never changes) |
user_id |
string |
✅ | Participant’s user id. Immutable |
username |
string |
— | Participant’s login (server JOIN on accounts_user). Read-only, computed |
role |
string |
✅ | admin (can manage members + metadata) or member. Immutable via CRUD |
wrapped_content_key |
string |
✅ | Chat content key wrapped for this participant (base64). 0x01 = AES-GCM wrap (creator), 0x02 = ECDH wrap (other participants) |
last_read_message_id |
string? |
— | The participant’s read cursor (snowflake id). Written by chat.markRead, drives unread_count |
muted_until |
string? |
ISO 8601 timestamp — notifications suppressed until then. null = not muted. Far-future = muted indefinitely |
|
pinned |
boolean |
Chat pinned to the top of the list for this participant. Default false |
|
created_at |
string |
— | ISO 8601 creation timestamp. Read-only |
find
List chats the caller owns or participates in, with member counts, unread badges, and last-message pointers.
WebSocket
Request
{
"jsonrpc": "2.0",
"method": "messages.chats.find",
"params": {
"sort": [{ "field": "updated_at", "dir": "desc" }],
"limit": 100,
"fields": [
"id", "kind", "owner_id", "owner_wrapped_key",
"enc_name", "enc_icon",
"member_count", "unread_count", "last_message_id",
"created_at", "updated_at",
"chat_user_role.id", "chat_user_role.user_id", "chat_user_role.username",
"chat_user_role.role", "chat_user_role.wrapped_content_key",
"chat_user_role.muted_until", "chat_user_role.pinned"
]
},
"id": 1
}
Response
{
"jsonrpc": "2.0",
"result": {
"items": [
{
"id": "220226115451486210",
"kind": "group",
"owner_id": "214775397793923072",
"owner_wrapped_key": "<base64>",
"enc_name": "<base64>",
"enc_icon": "<base64>",
"member_count": 3,
"unread_count": 2,
"last_message_id": "220240846547582978",
"created_at": "2026-08-20T10:15:00Z",
"updated_at": "2026-08-31T08:02:11Z",
"chat_user_role": [
{
"id": "220226115480846337",
"user_id": "214775397793923072",
"username": "alice",
"role": "admin",
"wrapped_content_key": "<base64>",
"muted_until": null,
"pinned": true
},
{
"id": "220226115493429249",
"user_id": "214782287248621568",
"username": "bob",
"role": "member",
"wrapped_content_key": "<base64>",
"muted_until": null,
"pinned": false
}
]
}
],
"total": 7
},
"id": 1
}
findOneById
Fetch a single chat (same shape as find, plus enc_description).
WebSocket
Request
{
"jsonrpc": "2.0",
"method": "messages.chats.findOneById",
"params": {
"id": "220226115451486210",
"fields": [
"id", "kind", "owner_id", "owner_wrapped_key",
"enc_name", "enc_icon", "enc_description",
"member_count", "unread_count", "last_message_id",
"created_at", "updated_at",
"chat_user_role.id", "chat_user_role.user_id", "chat_user_role.username",
"chat_user_role.role", "chat_user_role.wrapped_content_key",
"chat_user_role.muted_until", "chat_user_role.pinned"
]
},
"id": 2
}
Response
Same shape as one find item, with enc_description included.
create
Create a chat with its participants in a single transaction. The server assigns id, created_at, updated_at, and fills owner_id from the authenticated user.
WebSocket
Request
{
"jsonrpc": "2.0",
"method": "messages.chats.create",
"params": {
"document": {
"kind": "group",
"enc_name": "<base64>",
"owner_wrapped_key": "<base64>",
"chat_user_role": [
{ "user_id": "214782287248621568", "role": "member", "account_public_key": "<base64>" },
{ "user_id": "90517045413679104", "role": "member", "account_public_key": "<base64>" }
]
}
},
"id": 3
}
For a direct chat: "kind": "direct", no enc_name, exactly one other participant in chat_user_role.
Response
The created chat in the findOneById shape (with the creator’s admin row prepended to chat_user_role).
updateOneById
Partial update of chat metadata (enc_name / enc_icon / enc_description). Shared-path writes are role-gated: only participants with role: "admin" may edit. Membership changes do not flow through this method — see chat_user_roles.
WebSocket
Request
{
"jsonrpc": "2.0",
"method": "messages.chats.updateOneById",
"params": {
"id": "220226115451486210",
"document": {
"enc_name": "<base64-new>"
}
},
"id": 4
}
Response
The updated chat.
deleteOneById
Delete a chat with all its messages, reactions, attachments, and participant rows. Owner or chat admin only.
WebSocket
Request
{
"jsonrpc": "2.0",
"method": "messages.chats.deleteOneById",
"params": { "id": "220226115451486210" },
"id": 5
}
Response
The deleted chat’s final state.
markRead
Advance the caller’s read cursor in a chat (messages.chat.markRead, service messages.chat). Sets last_read_message_id on the caller’s own ChatUserRole row; unread_count on subsequent chats.find calls is computed against it. The target message must belong to the chat. Non-participants match zero rows (harmless no-op).
WebSocket
Request
{
"jsonrpc": "2.0",
"method": "messages.chat.markRead",
"params": {
"chat_id": "220226115451486210",
"message_id": "220240846547582978"
},
"id": 6
}
Response
{
"jsonrpc": "2.0",
"result": { "ok": true },
"id": 6
}