REST endpoints for conversations + messages, the mark-read semantics, and the WebSocket kind:"message" envelope.
Messaging API reference
This page covers the seven REST endpoints and the WebSocket envelope that the messaging dock uses. Same surface is available to any client with a tenant-scoped JWT.
Authentication
Every endpoint below requires a tenant-scoped JWT — the same token the rest of the platform uses. See Authentication for how to mint and present one.
Conversations
Create a DM
POST /api/v1/tenant/messaging/conversations
Content-Type: application/json
Authorization: Bearer <tenant JWT>
{ "participant_user_ids": ["u_…", "u_…"] }
The platform computes dm_key = sha256(sorted participant user_ids) and returns the existing conversation if one already
matches that key. This is upsert semantics — the same participant
set always lands in the same conversation.
Response: the conversation record, including id, dm_key,
participants, and created_at.
List your conversations
GET /api/v1/tenant/messaging/conversations
Authorization: Bearer <tenant JWT>
Returns the conversations the caller participates in, plus
per-conversation unread_count.
Messages
List messages in a conversation
GET /api/v1/tenant/messaging/conversations/:id/messages
Authorization: Bearer <tenant JWT>
Returns messages in the conversation. Soft-deleted messages are
returned with a non-null deleted_at; their body is omitted from
the response.
Send a message
POST /api/v1/tenant/messaging/conversations/:id/messages
Content-Type: application/json
Authorization: Bearer <tenant JWT>
{ "body": "hi @[Alice](user:u_alice) — can you take a look?" }
The body is parsed for @[Name](user:…) tokens; matched
user_ids each receive a notification (which lights their bell).
Edit your message
PATCH /api/v1/tenant/messaging/conversations/:id/messages/:mid
Content-Type: application/json
Authorization: Bearer <tenant JWT>
{ "body": "new body" }
Sets edited_at to now(). Returns 403 if the caller is not the
original sender.
Delete your message
DELETE /api/v1/tenant/messaging/conversations/:id/messages/:mid
Authorization: Bearer <tenant JWT>
Soft-deletes — sets deleted_at to now() and clears the body.
The message stays in place visually as "deleted." Returns 403 if
the caller is not the original sender.
Mark as read
POST /api/v1/tenant/messaging/conversations/:id/read
Authorization: Bearer <tenant JWT>
Sets last_read_at on the caller's participant record to
now(). Returns 204 with no body.
Unread count is recomputed lazily as the count of non-own
non-deleted messages in the conversation with
created_at > last_read_at.
Errors
- 400 — request validation failure (missing fields, malformed JSON).
- 403 — the caller is not a participant of the conversation (or, for edit / delete, not the original sender of the message).
- 404 — the conversation does not exist in the caller's tenant.
Responses use the standard AppError JSON envelope. See
Error envelope for the full status-code
table and worked examples.
Real-time events
Where to connect
The platform exposes a single multiplexed WebSocket at
/api/v1/ws, authenticated with the same tenant JWT (header or
query param per existing platform convention — see
Authentication).
Envelope shape
The server emits JSON envelopes tagged by kind. Messaging uses
kind: "message":
{
"kind": "message",
"event": "created",
"conversation_id": "conv_…",
"message": {
"id": "msg_…",
"sender_user_id": "u_…",
"body": "…",
"created_at": "2026-06-19T12:00:00Z"
}
}
Server-side filter
The server only sends kind: "message" envelopes for
conversations the connected user participates in. Clients do
not need to subscribe per-conversation — opening the WebSocket
is enough.
What events ship today
event: "created" only. Edit and delete events are not
broadcast — clients poll the message list after acting.
Other kinds
The same WebSocket carries other event kinds (notifications,
etc.); each kind will be documented in its own docs section.
Filter on kind and ignore what you don't recognize.
What's not in the API today
The following are explicit non-features. Same list as the overview page, scoped to API impact:
- No channels endpoints (no broadcast-style group spaces).
- No entity-threads endpoints (no record-bound conversations).
- No presence or typing endpoints.
- No attachment endpoints.
- No reactions endpoints.
- No full-text search endpoint.