Conversations
A conversation is either direct (two people) or group. Every endpoint
here takes an end-user JWT and acts as that user. See
Conversations & roles for the model.
Not available: there is no API to delete a conversation, to change a member's role after they joined, or to list conversations with an API key.
The conversation object
{
"id": "c8a1f4e2-6b3d-4f7a-9c2e-1d5b8a0f3e61",
"tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
"type": "group",
"name": "Engineering",
"avatar_url": null,
"created_by_user_id": "11111111-2222-3333-4444-555555555555",
"member_limit": null,
"custom_data": {},
"settings": {},
"last_message_at": "2026-06-02T08:13:55.123456Z",
"last_message_preview": "ship it",
"last_sequence": 184,
"unread_count": 3,
"member_external_ids": ["acme-alice", "acme-bob", "acme-charlie"],
"inserted_at": "2026-04-12T09:00:00.000000Z",
"updated_at": "2026-06-02T08:13:55.123456Z"
}
| Field | Type | Description |
|---|---|---|
id | uuid | |
tenant_id | uuid | |
type | "direct" | "group" | |
name | string | null | Up to 255 characters. |
avatar_url | string | null | Up to 2048 characters. |
created_by_user_id | uuid | null | The creator. They are the conversation's owner. |
member_limit | integer | null | Maximum number of members of a group, the owner included. null means no limit. |
custom_data | object | Free-form JSON. |
settings | object | Free-form JSON. |
last_message_at | string | null | Time of the latest message. |
last_message_preview | string | null | The start of the latest message's text, for an inbox row. "Attachment" or "N attachments" for a message with no text. |
last_sequence | integer | The highest message sequence used so far in this conversation. |
unread_count | integer | null | The caller's unread messages: non-deleted messages after their read position. Only set by GET /v1/conversations. It is null on the other endpoints. |
member_external_ids | string[] | The external_id of every member, the caller included. |
inserted_at, updated_at | string | ISO 8601. |
List conversations
/v1/conversationsThe conversations the caller is a member of, most recently active first
(by last_message_at, or creation time for a conversation with no
messages).
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | 1–100. Values outside the range are clamped. |
There is no cursor. The endpoint returns at most 100 conversations.
Response
200: { "data": [ Conversation, … ] }, with unread_count set on each.
curl "https://api.poolse.dev/v1/conversations?limit=20" \
-H "Authorization: Bearer $USER_JWT"
Create a conversation
/v1/conversationsThe caller becomes the owner. The other people you list join as
member.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
type | "direct" | "group" | yes | |
member_external_ids | string[] | no | The other people, by your external_id. Unknown ids are created as new users with only external_id set. |
name | string | no | Up to 255 characters. |
avatar_url | string | no | Up to 2048 characters. |
member_limit | integer | no | Greater than 0. Groups only. Counts every member, the owner included. |
custom_data | object | no | Free-form JSON. |
settings | object | no | Free-form JSON. |
Direct conversations are deduplicated. When type is direct and
member_external_ids names exactly one other person, and a direct
conversation between the two already exists, you get that existing
conversation back instead of a new one. A direct conversation holds at
most 2 members.
Response
201: the conversation, also when an existing direct conversation was
returned.
curl -X POST https://api.poolse.dev/v1/conversations \
-H "Authorization: Bearer $USER_JWT" \
-H "Content-Type: application/json" \
-d '{"type":"group","name":"Launch chat","member_external_ids":["acme-alice","acme-bob"]}'
Errors
| Code | Status | When |
|---|---|---|
validation_failed | 422 | type missing or not direct / group, or a field fails validation. |
member_add_failed | 422 | A direct conversation with more than one other person. |
lazy_provision_rate_limited | 429 | The caller has created more than 50 unknown users in the last hour. Retry-After header in seconds, and details: { retry_after_ms }. |
Get a conversation
/v1/conversations/:id200: the conversation (unread_count is null).
| Code | Status | When |
|---|---|---|
forbidden | 403 | The caller is not a member. |
not_found | 404 | No such conversation in the tenant. |
Update a conversation
/v1/conversations/:idChanges any of name, avatar_url, member_limit, custom_data and
settings. Other fields are ignored. The caller must be the owner or
an admin.
curl -X PATCH https://api.poolse.dev/v1/conversations/$CONV_ID \
-H "Authorization: Bearer $USER_JWT" \
-H "Content-Type: application/json" \
-d '{"name":"Launch chat (archived)"}'
200: the updated conversation.
| Code | Status | When |
|---|---|---|
forbidden | 403 | The caller is not an owner or admin. |
not_found | 404 | No such conversation in the tenant. |
validation_failed | 422 | A field fails validation. |
Members
The member object
{
"id": "0b6e9d1c-7a2f-4c8e-b3d5-9f1a2e4c6b80",
"tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
"conversation_id": "c8a1f4e2-6b3d-4f7a-9c2e-1d5b8a0f3e61",
"user_id": "11111111-2222-3333-4444-555555555555",
"external_id": "acme-alice",
"role": "owner",
"joined_at": "2026-04-12T09:00:00.000000Z",
"muted_until": null,
"last_read_message_id": "5f0c2a7e-8d1b-4e3f-a6c9-2b7d0e4f1a38",
"last_read_sequence": 181,
"last_read_at": "2026-06-02T08:10:02.000000Z",
"inserted_at": "2026-04-12T09:00:00.000000Z",
"updated_at": "2026-06-02T08:10:02.000000Z"
}
role is owner, admin or member. The last_read_* fields are the
member's read position.
List members
/v1/conversations/:conversation_id/members200: { "data": [ Member, … ] }. Any member may call it.
Other members' last_read_message_id, last_read_sequence and
last_read_at are null when the workspace's read-receipt setting hides
them from the caller. The caller's own are always present. See
Read receipts & unread. Rows may also
carry a share_read_receipts boolean.
| Code | Status | When |
|---|---|---|
forbidden | 403 | The caller is not a member. |
not_found | 404 | No such conversation. |
Add members
/v1/conversations/:conversation_id/membersThe caller must be the owner or an admin.
| Field | Type | Required | Description |
|---|---|---|---|
external_ids | string[] | no | The people to add, by your external_id. Unknown ids are created as new users. Omitted or empty adds nobody and returns an empty list. |
role | "owner" | "admin" | "member" | no | Role for everyone added. Default member. Any other value is treated as member. |
curl -X POST https://api.poolse.dev/v1/conversations/$CONV_ID/members \
-H "Authorization: Bearer $USER_JWT" \
-H "Content-Type: application/json" \
-d '{"external_ids":["acme-bob","acme-charlie"]}'
201: { "data": [ Member, … ] } with the memberships created. People
who were already members are skipped silently and are not in the list.
| Code | Status | When |
|---|---|---|
forbidden | 403 | The caller is not an owner or admin. |
not_found | 404 | No such conversation. |
member_add_failed | 422 | A direct conversation already has 2 members, or the group reached its member_limit. Nobody after the failing person is added. |
validation_failed | 422 | An external_id is invalid. |
lazy_provision_rate_limited | 429 | The caller has created more than 50 unknown users in the last hour. Retry-After header in seconds, and details: { retry_after_ms }. |
Remove a member
/v1/conversations/:conversation_id/members/:user_iduser_id is the member's poolse id, not their external_id. Any
member can remove themselves. An owner or admin can remove anyone,
including the owner. Nothing stops the last owner from leaving.
204 on success. If the removed user has the conversation's realtime
channel open, it is closed at once: they stop receiving the
conversation's events.
| Code | Status | When |
|---|---|---|
forbidden | 403 | Removing someone else without being owner or admin. |
not_found | 404 | No such conversation, or that user is not a member. |
Realtime and webhooks
Changes here also reach clients and your backend without polling:
- Every member of a new conversation, the creator included, gets
conversation:createdon their user channel once it has been saved, with all members inmember_external_ids. A user added later gets it too. See User channel. - Your backend can receive
conversation.created,conversation.updated,member.addedandmember.removed. See Webhooks.