Create conversations and messages
Open a conversation from your own system, send replies through the channel, change its status and assignment, and keep everything safe to retry.
Last updated
Open a conversation
POST /conversations needs an inbox_id and a contact: either contact_id or an email, which resolves an existing contact by identity or creates one. An optional content becomes the first outbound message, delivered through the inbox's channel.
curl -X POST "$KOMMIO_URL/conversations" \
-H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: renewal-2026-ana" \
-d '{"inbox_id": 3, "email": "[email protected]", "subject": "Your renewal", "priority": "high",
"custom_attributes": {"plan": "team", "renews_on": "2026-10-01"},
"content": "Hi Ana, your plan renews on 1 October. Anything to change?"}'const { data: conv, error } = await api.POST("/conversations", {
body: {
inbox_id: 3, email: "[email protected]", subject: "Your renewal", priority: "high",
custom_attributes: { plan: "team", renews_on: "2026-10-01" },
content: "Hi Ana, your plan renews on 1 October. Anything to change?",
},
});
if (error) throw new Error(error.detail);conv = requests.post(f"{BASE}/conversations", headers={**H, "Idempotency-Key": "renewal-2026-ana"}, json={
"inbox_id": 3, "email": "[email protected]", "subject": "Your renewal", "priority": "high",
"custom_attributes": {"plan": "team", "renews_on": "2026-10-01"},
"content": "Hi Ana, your plan renews on 1 October. Anything to change?",
}).json()Send a reply or a private note
kind is reply (default, delivered to the customer) or note (visible only to people whose role can read notes). content_type is text (default), html (sanitised on write) or markdown.
curl -X POST "$KOMMIO_URL/conversations/4821/messages" \
-H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"content": "Renewed on the team plan. Invoice attached to your account.", "kind": "reply"}'
curl -X POST "$KOMMIO_URL/conversations/4821/messages" \
-H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"content": "Billing confirmed the card on file works.", "kind": "note"}'await api.POST("/conversations/{id}/messages", { params: { path: { id: conv.id } }, body: { content: "Renewed on the team plan.", kind: "reply" } });
await api.POST("/conversations/{id}/messages", { params: { path: { id: conv.id } }, body: { content: "Billing confirmed the card on file works.", kind: "note" } });Response 201 Created
{ "id": 55011, "conversation_id": 4821, "direction": "outbound", "kind": "reply", "content_type": "text", "content": "Renewed on the team plan.", "delivery_status": "queued", "created_at": "2026-09-22T09:14:00Z" }Delivery is asynchronous: the message is stored, then the channel sends it. Subscribe to message.delivered and message.delivery_failed to learn the outcome, or read delivery_status later.
Mention a teammate in a note
GET /conversations/ lists the members who may be mentioned: active members whose role sees the conversation and its private notes (the caller needs messages:write_private). Pass their ids as mention_ids on a note; mentioning someone who cannot see the note is refused with 422. A note sent without ids is resolved from its text (@First Last or @first) among the same people, and each mentioned person gets a mention notification in the app, by email or as a browser push, as they chose.
curl -X POST "$KOMMIO_URL/conversations/4821/messages" \
-H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"content": "@Lea can you check the refund?", "kind": "note", "mention_ids": [7]}'One conversation also downloads as a transcript in text, HTML or JSON: see exports.
Change status, priority, assignment
PATCH /conversations/{id} is the single write path the UI, automations and bots all use. Send only the fields you change.
# assign to a member and a team, set priority
curl -X PATCH "$KOMMIO_URL/conversations/4821" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"assignee_id": 7, "team_id": 2, "priority": "urgent"}'
# snooze until a time
curl -X PATCH "$KOMMIO_URL/conversations/4821" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"status": "snoozed", "snoozed_until": "2026-09-23T09:00:00Z"}'
# resolve
curl -X PATCH "$KOMMIO_URL/conversations/4821" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"status": "resolved"}'
# replace tags
curl -X PUT "$KOMMIO_URL/conversations/4821/tags" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"tag_ids": [4, 9]}'await api.PATCH("/conversations/{id}", { params: { path: { id: conv.id } }, body: { assignee_id: 7, team_id: 2, priority: "urgent" } });
await api.PATCH("/conversations/{id}", { params: { path: { id: conv.id } }, body: { status: "snoozed", snoozed_until: "2026-09-23T09:00:00Z" } });
await api.PATCH("/conversations/{id}", { params: { path: { id: conv.id } }, body: { status: "resolved" } });
await api.PUT("/conversations/{id}/tags", { params: { path: { id: conv.id } }, body: { tag_ids: [4, 9] } });requests.patch(f"{BASE}/conversations/4821", headers=H, json={"assignee_id": 7, "team_id": 2, "priority": "urgent"})
requests.patch(f"{BASE}/conversations/4821", headers=H, json={"status": "snoozed", "snoozed_until": "2026-09-23T09:00:00Z"})
requests.patch(f"{BASE}/conversations/4821", headers=H, json={"status": "resolved"})
requests.put(f"{BASE}/conversations/4821/tags", headers=H, json={"tag_ids": [4, 9]})Customer messages from your own channel
An API inbox is a channel you run yourself: a form, an in-app messenger, a marketplace. Record what the customer writes with POST /inboxes/{id}/messages (inboxes:manage); it takes the normal inbound path, so it finds or creates the contact, threads into the conversation named by reply_to, else the contact's open conversation in that inbox, else starts a new one, and runs rules, bots and webhooks exactly as an email or a chat would. Agents' replies reach you as message.created webhooks with direction: "outbound".
curl -X POST "$KOMMIO_URL/inboxes/5/messages" \
-H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"from": {"kind": "external", "value": "crm:9917", "name": "Ana Souza"},
"subject": "Problem with my order", "content": "The package arrived damaged.", "message_id": "form-88121"}'Response 201 Created
{ "conversation_id": 4822, "created": true, "message": { "id": 55020, "direction": "inbound", "kind": "reply", "content": "The package arrived damaged.", "content_type": "text", "created_at": "2026-09-22T09:12:00Z" } }from.kind is email, phone or external (your own user id). message_id is your id for the message: sending it again returns the stored message with created: false instead of a duplicate. reply_to names the message this answers, to thread into that conversation. Only API inboxes take messages this way; other channels receive them from their provider.
Attachments
Upload with POST /conversations/{id}/messages/upload (multipart: content, kind, one or more files). Files are checked by magic bytes and size, stored under the workspace, and served from /api/attachments/{id} as downloads unless they are images.
curl -X POST "$KOMMIO_URL/conversations/4821/messages/upload" -H "Authorization: Bearer $KOMMIO_KEY" \
-F content="Invoice attached." -F kind=reply -F [email protected]Retry safety
Any POST, PUT, PATCH or DELETE accepts Idempotency-Key. The first response is stored for 24 hours and replayed to a retry with the same key and body, marked Idempotent-Replayed: true; a different body with the same key is rejected with 422. The TypeScript client sets a key on every write automatically.
Every claim on these pages is covered by a test in the repository, and the API reference is generated from the same contract every server serves at /api/openapi. Corrections to [email protected].