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_role must list every participant (the encryption middleware prepends the creator’s own admin row 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 into wrapped_content_key and strips account_public_key before 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
}