Messages

Every endpoint here takes an end-user JWT. The caller must be a member of the conversation. Message bodies are encrypted at rest and returned in plaintext to members. See Encryption & retention.

The message object

{
  "id": "5f0c2a7e-8d1b-4e3f-a6c9-2b7d0e4f1a38",
  "tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
  "conversation_id": "c8a1f4e2-6b3d-4f7a-9c2e-1d5b8a0f3e61",
  "sender_id": "11111111-2222-3333-4444-555555555555",
  "sender_external_id": "acme-alice",
  "type": "text",
  "body": "hello",
  "reply_to_id": null,
  "thread_root_id": null,
  "reply_count": 0,
  "quoted_message_id": null,
  "quoted_message": null,
  "mentions": [],
  "reactions": { "👍": ["22222222-3333-4444-5555-666666666666"] },
  "attachments": [],
  "edited_at": null,
  "deleted_at": null,
  "sequence": 184,
  "inserted_at": "2026-06-02T08:13:55.123456Z",
  "updated_at": "2026-06-02T08:13:55.123456Z"
}
FieldTypeDescription
iduuid
tenant_id, conversation_iduuid
sender_iduuid | nullThe sender's poolse id. null on messages poolse writes itself (type: "system").
sender_external_idstring | nullThe sender's external_id.
type"text" | "custom" | "system"custom bodies are JSON strings. system messages are written by poolse, for example one per finished call. You cannot send them.
bodystring | nullPlaintext. "" on an attachment-only message, null once deleted.
reply_to_iduuid | nullThe message this replies to in a thread.
thread_root_iduuid | nullThe thread's top message. null for messages in the main feed.
reply_countintegerNumber of non-deleted replies in the thread this message starts.
quoted_message_iduuid | nullA message quoted inline. The new message stays in the main feed.
quoted_messageobject | nullPreview of the quoted message: { id, sender_id, sender_external_id, body, deleted_at, inserted_at }. body is cut to 200 bytes plus ….
mentionsuuid[]Poolse ids of mentioned members.
reactionsobjectEmoji to the list of user ids who reacted with it.
attachmentsAttachment[]Linked attachments. See Attachments.
edited_atstring | nullSet by an edit.
deleted_atstring | nullSet by a delete.
sequenceintegerPer-conversation order, assigned at send. Thread replies take numbers from the same counter, so the main feed has gaps.
inserted_at, updated_atstringISO 8601.

The realtime message:new and message:updated events carry the same shape.

List messages

GET/v1/conversations/:conversation_id/messages
End-user JWT

The main feed, newest first. Deleted messages and thread replies are not included. Fetch replies with GET /v1/messages/:id/replies.

Query parameters

NameTypeDefaultDescription
limitinteger501–100. Values outside the range are clamped.
beforeintegernoneOnly messages with sequence lower than this. Pass the lowest sequence you have to load older messages.

Response

200: { "data": [ Message, … ] }. There is no cursor field: when you get fewer than limit rows, you have reached the start.

curl "https://api.poolse.dev/v1/conversations/$CONV_ID/messages?limit=50&before=135" \
  -H "Authorization: Bearer $USER_JWT"
CodeStatusWhen
forbidden403The caller is not a member.
not_found404No such conversation.

Send a message

POST/v1/conversations/:conversation_id/messages
End-user JWT

Request body

FieldTypeRequiredDescription
bodystringunless attachment_ids is setUp to 64 KB of text. A message needs text, attachments, or both.
type"text" | "custom"noDefault text. For custom, body must be valid JSON.
attachment_idsuuid[]noUp to 10 attachments, uploaded by the caller and not yet used on another message. See Attachments.
reply_to_iduuidnoMakes this a thread reply. Replying to a reply joins the same thread.
quoted_message_iduuidnoQuote a message from the same conversation.
mentionsuuid[]noPoolse ids of current members to mention. Each gets a mention:new event on their user channel.
iduuidnoYour own id for the message, so a retried send cannot create a duplicate. Reusing an existing id fails with validation_failed.

Response

201: the message, with sequence and attachments set.

Sending also marks the conversation read for the sender, updates the conversation's last_message_* fields, and pushes the message to members over realtime.

curl -X POST https://api.poolse.dev/v1/conversations/$CONV_ID/messages \
  -H "Authorization: Bearer $USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{"body":"hello","mentions":["22222222-3333-4444-5555-666666666666"]}'

Errors

CodeStatusWhen
validation_failed422No text and no attachments, body over 64 KB, type not text / custom, a custom body that is not JSON, more than 10 or duplicate attachment_ids, a mention of a non-member, or an id already in use. details names the field.
send_failed422reply_to_id or quoted_message_id not found, a quote from another conversation, or an attachment that is unknown, already used, uploaded by someone else, or whose stored file is larger than the byte_size declared for it (the message then contains attachment_size_mismatch).
bad_request400reply_to_id, quoted_message_id, or an entry of attachment_ids or mentions is not a valid UUID.
forbidden403The caller is not a member, or your subscription is inactive.
not_found404No such conversation.

Edit a message

PATCH/v1/messages/:id
End-user JWT

Replaces the body. Only the sender can edit. The new text may be blank only if the message has attachments.

If this is the conversation's latest message, its last_message_preview is rewritten too, and members get conversation:updated on their user channel.

FieldTypeRequiredDescription
bodystringyesThe new text, up to 64 KB.

200: the message, with edited_at set.

curl -X PATCH https://api.poolse.dev/v1/messages/$MSG_ID \
  -H "Authorization: Bearer $USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{"body":"hello, world"}'
CodeStatusWhen
forbidden403The caller is not the sender.
not_found404No such message in the tenant.
message_deleted409The message is deleted.
validation_failed422body missing, not a string, or over 64 KB, or blank on a message with no attachments (details: { body: ["can't be blank"] }).

Delete a message

DELETE/v1/messages/:id
End-user JWT

Soft-deletes the message: body becomes null and deleted_at is set. Its attachments are deleted, files included. Deleted messages no longer appear in the message list. The sender can delete their own messages. A conversation owner or admin can delete anyone's.

204 on success. Deleting an already-deleted message also returns 204.

CodeStatusWhen
forbidden403Not the sender, and not an owner or admin.
not_found404No such message in the tenant.

List thread replies

GET/v1/messages/:id/replies
End-user JWT

The replies in the thread started by message :id, oldest first. The top message itself and deleted replies are not included.

NameTypeDefaultDescription
limitinteger5001–1000. Values outside the range are clamped.
afterintegernoneOnly replies with sequence higher than this.

200: { "data": [ Message, … ] }.

CodeStatusWhen
forbidden403The caller is not a member.
not_found404No such message.

Add a reaction

POST/v1/messages/:id/reactions
End-user JWT
FieldTypeRequiredDescription
emojistringyesAny non-empty string, usually one emoji.

Adding a reaction you already have changes nothing.

200: the updated message.

curl -X POST https://api.poolse.dev/v1/messages/$MSG_ID/reactions \
  -H "Authorization: Bearer $USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{"emoji":"👍"}'
CodeStatusWhen
validation_failed422emoji missing or empty.
message_deleted409The message is deleted.
forbidden403The caller is not a member.
not_found404No such message.

Remove a reaction

DELETE/v1/messages/:id/reactions/:emoji
End-user JWT

Removes the caller's own reaction. URL-encode the emoji in the path. Removing a reaction you don't have changes nothing.

200: the updated message.

CodeStatusWhen
forbidden403The caller is not a member.
not_found404No such message.

Mark read

POST/v1/conversations/:conversation_id/read
End-user JWT

Moves the caller's read position to a message. This drives their unread_count. The position is set to exactly the message you pass, even an older one: marking an earlier message read moves the position back and raises the unread count again. Send the newest message you have shown.

FieldTypeRequiredDescription
message_iduuidyesA message in this conversation.

204 on success. Other members get a member:read event on the conversation channel if the workspace's read-receipt setting lets them see it. See Read receipts & unread.

curl -X POST https://api.poolse.dev/v1/conversations/$CONV_ID/read \
  -H "Authorization: Bearer $USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{"message_id":"5f0c2a7e-8d1b-4e3f-a6c9-2b7d0e4f1a38"}'
CodeStatusWhen
validation_failed422message_id missing.
bad_request400message_id is not a valid UUID.
message_not_found404The message is not in this conversation.
forbidden403The caller is not a member.
not_found404No such conversation.

Webhooks

Your backend can receive message.created, message.updated, message.deleted and mention.created. See Webhooks.

Messages · API · poolse docs