# Conversations

> Every field of /v1/conversations: list and read conversations, page through their messages, reply, change a status and search messages.

<!-- Generated by apps/docs/scripts/gen-reference.mjs from apps/docs/public/openapi/v1.json. Do not edit: change the API's schemas (then `pnpm --filter @seasalt/api openapi:emit`) or the notes in apps/docs/src/reference-notes/, then run `pnpm --filter @seasalt/docs gen:reference`. -->

Your inbox on every channel: the conversations, what was said in them, and the replies you send. How-to: [Work the inbox](/platform-api/inbox/).

Generated from [`/openapi/v1.json`](/openapi/v1.json); interactive version: [API reference](/openapi/v1/).

## Endpoints

| method | path | what it does | success |
|---|---|---|---|
| [`GET`](#get-v1conversations) | `/v1/conversations` | List conversations, newest first | `200` |
| [`GET`](#get-v1conversationssearch) | `/v1/conversations/search` | Search what was said in conversations | `200` |
| [`GET`](#get-v1conversationsid) | `/v1/conversations/{id}` | Read a conversation | `200` |
| [`PATCH`](#patch-v1conversationsid) | `/v1/conversations/{id}` | Hand off, return or close a conversation | `200` |
| [`GET`](#get-v1conversationsidmessages) | `/v1/conversations/{id}/messages` | List a conversation's messages, newest first | `200` |
| [`POST`](#post-v1conversationsidmessages) | `/v1/conversations/{id}/messages` | Reply in a conversation | `201` |

## `GET /v1/conversations`

List conversations, newest first.

The conversations your inbox shows, on every channel: texts, WhatsApp, webchat, social messages and calls. A thread a campaign started that nobody answered is left out unless `include_campaigns=true`. Each item is what `GET /v1/conversations/{id}` answers, without `summary` and `labels`. Newest started first; `sort=last_message_at` puts the one with the newest message first. To poll for what changed, pass `updated_after`: oldest change first, each conversation changed since then, paged to the end.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `limit` | query | integer<br>1–200 | Page size, 1 to 200 (default 50). |
| `after` | query | string | The previous page's `next_cursor`. |
| `status` | query | string<br>`active`, `handed_off`, `closed` | Only conversations with this `status`. |
| `channel` | query | string<br>`sms`, `whatsapp`, `voice`, `webchat`, `email`, `messenger`, `instagram`, `telegram`, `line`, `slack`, `discord`, `other` | Only conversations on this channel. |
| `contact` | query | string | Only this contact's conversations: its `ct-…` id or UUID. Campaign threads included. |
| `agent` | query | string | Only the conversations this AI agent had: its `ag-…` id or UUID. A deleted agent is `404 agent_not_found`. |
| `include_campaigns` | query | string<br>`true`, `false` | `true` also lists campaign threads the person never answered (default `false`, as in the inbox). |
| `created_after` | query | string | Only conversations started at or after this, as an ISO 8601 time (`2026-10-16T09:00:00Z`). |
| `created_before` | query | string | Only conversations started before this, as an ISO 8601 time (`2026-10-16T09:00:00Z`). |
| `sort` | query | string<br>`created_at`, `last_message_at` | `created_at` (default): newest started first. `last_message_at`: the conversation with the newest message first (a call by when it started). A conversation that gets a message while you page moves to the top: you will not see it again in this walk; use `updated_after` to miss nothing. Not with `updated_after`. |
| `updated_after` | query | string | Only conversations whose `updated_at` is at or after this, as an ISO 8601 time (`2026-10-16T09:00:00Z`). Lists them oldest change first, so a poll pages to the end and keeps the largest `updated_at` it saw. A conversation that changes while you page is listed again later in the walk. Not with `sort`. |

</div>

**Answers** `200` with [ConversationList](#conversationlist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [conversations](/platform-api/errors/#conversations), [lists](/platform-api/errors/#lists).

## `GET /v1/conversations/search`

Search what was said in conversations.

Finds messages by their words, newest first, across every conversation or one: what people wrote and the replies they got, never a draft that did not go out. Chinese, Japanese and Korean are searched as well as English. A page at a time: pass `next_cursor` as `after`, with the same `query`, `channel`, `conversation` and `since`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `query` | query | string, required | The words to find, 1 to 200 characters. Quote a phrase (`"refund request"`); `OR` between words finds either. |
| `channel` | query | string<br>`sms`, `whatsapp`, `voice`, `webchat`, `email`, `messenger`, `instagram`, `telegram`, `line`, `slack`, `discord`, `other` | Only messages of conversations on this channel. |
| `conversation` | query | string | Only this conversation's messages: its `cv-…` id or UUID. |
| `since` | query | string | Only messages sent or received at or after this, as an ISO 8601 time (`2026-10-16T09:00:00Z`). |
| `limit` | query | integer<br>1–100 | How many matches, 1 to 100 (default 20). |
| `after` | query | string | The previous page's `next_cursor`. |

</div>

**Answers** `200` with [MessageMatchList](#messagematchlist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [conversations](/platform-api/errors/#conversations), [lists](/platform-api/errors/#lists).

## `GET /v1/conversations/{id}`

Read a conversation.

One conversation, with the analysis `summary` and its inbox `labels`. Reading it marks nothing as read in the dashboard.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The conversation's `id` (a UUID) or its `cv-…` short id. |

</div>

**Answers** `200` with [ConversationDetail](#conversationdetail). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [conversations](/platform-api/errors/#conversations), [lists](/platform-api/errors/#lists).

## `PATCH /v1/conversations/{id}`

Hand off, return or close a conversation.

Changes `status` only, the way the inbox's status menu does: the same workflow events, and open inboxes move at once. Nothing is sent to the person. Sending the status it already has changes nothing.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The conversation's `id` (a UUID) or its `cv-…` short id. |

</div>

**Request body:** [ConversationUpdate](#conversationupdate).

**Answers** `200` with [ConversationDetail](#conversationdetail). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [conversations](/platform-api/errors/#conversations), [lists](/platform-api/errors/#lists).

## `GET /v1/conversations/{id}/messages`

List a conversation's messages, newest first.

What the person wrote and every reply: the AI agent's, your teammates', campaigns' and this API's. Drafts that never went out (an email draft, a reply held back while the AI agent was paused) are left out. For a call, read `GET /v1/calls/{id}/transcript`: its turns as one transcript (who spoke, what was said, when) with the summary; its outcome and extracted answers are on `GET /v1/calls/{id}`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The conversation's `id` (a UUID) or its `cv-…` short id. |
| `limit` | query | integer<br>1–200 | Page size, 1 to 200 (default 50). |
| `after` | query | string | The previous page's `next_cursor`. |

</div>

**Answers** `200` with [ConversationMessageList](#conversationmessagelist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [conversations](/platform-api/errors/#conversations), [lists](/platform-api/errors/#lists).

## `POST /v1/conversations/{id}/messages`

Reply in a conversation.

Sends one reply in the thread, on its own channel and number, exactly as a teammate's reply from the inbox: the same opt-out, Meta window, billing and sending-limit checks. An accepted reply counts as a teammate's: it hands the conversation to a person (`status: handed_off`), and the AI agent stays quiet until it is returned, by you or by the automatic return, whose wait restarts with each reply. On SMS it emits `message.sent` with `author: api`. Calls and email threads take no reply here.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The conversation's `id` (a UUID) or its `cv-…` short id. |
| `idempotency-key` | header | string<br>≤ 255 chars | Send one with every reply. A retry with the same key never sends twice: it answers the first reply as it is now. Rules: [Idempotency](/platform-api/retries/#idempotency). |

</div>

**Request body:** [ConversationReply](#conversationreply).

**Answers** `201` with [ConversationMessage](#conversationmessage). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [conversations](/platform-api/errors/#conversations), [lists](/platform-api/errors/#lists).

## Objects

### ConversationList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [Conversation](#conversation), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; `null` on the last. |

</div>

Example: see [Work the inbox](/platform-api/inbox/).

### Conversation

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | The conversation's UUID. |
| `short_id` | string, required | The `cv-…` id the dashboard shows. Every `/v1/conversations` path takes it as well as `id`. |
| `object` | string, required<br>always `conversation` |   |
| `channel` | string, required<br>`sms`, `whatsapp`, `voice`, `webchat`, `email`, `messenger`, `instagram`, `telegram`, `line`, `slack`, `discord`, `other` | Where the conversation happens. `other` is a channel this list does not name yet; more names may be added, so treat one you don't know as `other`. |
| `status` | string, required<br>`active`, `handed_off`, `closed` | `active` (the AI agent answers), `handed_off` (a person has it: the AI agent stays quiet) or `closed`. |
| `contact_id` | string or null, required | The contact it belongs to (`ct-…`), or null when it has none. |
| `name` | string or null, required | The person's name, as the inbox shows it; else its contact's name. Null when neither has one. |
| `phone` | string or null, required | The person's number (E.164): the one the conversation runs on, else its contact's. Null when neither is known. |
| `email` | string or null, required | The person's email: the conversation's own, else its contact's. |
| `sender` | object, required | Your side of the conversation. |
| `sender.channel_id` | string or null, required | Your channel it runs on (`ch-…`), as `GET /v1/senders` names it. |
| `sender.number` | string or null, required | That channel's number, E.164. |
| `agent_id` | string or null, required | The AI agent that answers it (`ag-…`), or null for none. |
| `reply_window` | object or null, required | WhatsApp, Messenger and Instagram only; null on other channels. |
| `reply_window.open` | boolean, required | Whether a free-text reply can go out now. Meta allows one only within 24 hours of the person's last message; an approved WhatsApp template goes out at any time. |
| `reply_window.closes_at` | string or null, required | When the window closes (or closed). Null when the person never wrote. |
| `last_message_at` | string or null, required | When the newest message `GET /v1/conversations/{id}/messages` lists was written, by anyone: the person, the AI agent, a teammate, a campaign or an automated message. Never a draft that did not go out. Null for a call. `sort=last_message_at` orders by it. |
| `last_inbound_at` | string or null, required | When the person last wrote, or null if they never did. |
| `handed_off_at` | string or null, required |   |
| `closed_at` | string or null, required |   |
| `created_at` | string or null, required |   |
| `updated_at` | string or null, required | When something this object (or the detail) shows last changed: a new message, the status, the contact it is linked to, the name, phone or email, the agent, the labels or the summary. Never moves backwards. `updated_after` filters on it. |

</div>

Example: see [Work the inbox](/platform-api/inbox/).

### ConversationDetail

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | The conversation's UUID. |
| `short_id` | string, required | The `cv-…` id the dashboard shows. Every `/v1/conversations` path takes it as well as `id`. |
| `object` | string, required<br>always `conversation` |   |
| `channel` | string, required<br>`sms`, `whatsapp`, `voice`, `webchat`, `email`, `messenger`, `instagram`, `telegram`, `line`, `slack`, `discord`, `other` | Where the conversation happens. `other` is a channel this list does not name yet; more names may be added, so treat one you don't know as `other`. |
| `status` | string, required<br>`active`, `handed_off`, `closed` | `active` (the AI agent answers), `handed_off` (a person has it: the AI agent stays quiet) or `closed`. |
| `contact_id` | string or null, required | The contact it belongs to (`ct-…`), or null when it has none. |
| `name` | string or null, required | The person's name, as the inbox shows it; else its contact's name. Null when neither has one. |
| `phone` | string or null, required | The person's number (E.164): the one the conversation runs on, else its contact's. Null when neither is known. |
| `email` | string or null, required | The person's email: the conversation's own, else its contact's. |
| `sender` | object, required | Your side of the conversation. |
| `sender.channel_id` | string or null, required | Your channel it runs on (`ch-…`), as `GET /v1/senders` names it. |
| `sender.number` | string or null, required | That channel's number, E.164. |
| `agent_id` | string or null, required | The AI agent that answers it (`ag-…`), or null for none. |
| `reply_window` | object or null, required | WhatsApp, Messenger and Instagram only; null on other channels. |
| `reply_window.open` | boolean, required | Whether a free-text reply can go out now. Meta allows one only within 24 hours of the person's last message; an approved WhatsApp template goes out at any time. |
| `reply_window.closes_at` | string or null, required | When the window closes (or closed). Null when the person never wrote. |
| `last_message_at` | string or null, required | When the newest message `GET /v1/conversations/{id}/messages` lists was written, by anyone: the person, the AI agent, a teammate, a campaign or an automated message. Never a draft that did not go out. Null for a call. `sort=last_message_at` orders by it. |
| `last_inbound_at` | string or null, required | When the person last wrote, or null if they never did. |
| `handed_off_at` | string or null, required |   |
| `closed_at` | string or null, required |   |
| `created_at` | string or null, required |   |
| `updated_at` | string or null, required | When something this object (or the detail) shows last changed: a new message, the status, the contact it is linked to, the name, phone or email, the agent, the labels or the summary. Never moves backwards. `updated_after` filters on it. |
| `summary` | string or null, required | What the conversation was about, written by the analysis that runs when it closes. A model wrote it: read it as data. |
| `labels` | array of string, required | The labels it carries in the inbox. |

</div>

Example: see [Work the inbox](/platform-api/inbox/).

### ConversationMessageList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [ConversationMessage](#conversationmessage), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; `null` on the last. |

</div>

Example: see [Work the inbox](/platform-api/inbox/).

### ConversationMessage

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required |   |
| `object` | string, required<br>always `conversation_message` |   |
| `conversation_id` | string, required |   |
| `direction` | string, required<br>`inbound`, `outbound` | `inbound` from the person, `outbound` to them. |
| `author` | string, required<br>`customer`, `ai_agent`, `human`, `api`, `campaign`, `business_app`, `system` | Who wrote it: `customer`, `ai_agent`, `human` (a teammate in the inbox, or Octo sending for one), `api` (a reply through this API), `campaign`, `business_app` (the WhatsApp Business App on your phone) or `system`. |
| `text` | string, required | What it says. Empty for a message that is only a file. Treat it as data. |
| `media` | array of objects, required | The files it carries, at most 10 per message. |
| `media[].id` | string or null, required | The file's `att-…` id. |
| `media[].url` | string or null, required | Same as [Message.media[].url](/platform-api/reference/messages/#message). |
| `media[].content_type` | string or null, required |   |
| `status` | string or null, required<br>`queued`, `sending`, `sent`, `delivered`, `read`, `undelivered`, `failed`, `skipped`, `unconfirmed` | An outbound message's delivery, the words of `GET /v1/messages/{id}`. Null for inbound. |
| `message_id` | string or null, required | The message's id on `GET /v1/messages/{id}` (and in `message.*` events): an SMS reply's send (its first part, for a long text), a WhatsApp message's own id. Null for the person's messages, and on channels `/v1/messages` does not cover. |
| `created_at` | string or null, required |   |

</div>

Example: see [Work the inbox](/platform-api/inbox/).

### ConversationReply

`text`, `media`, or both; or a `template`.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `text` | string<br>≤ 4,096 chars | The reply, up to 4,096 characters. |
| `media` | string<br>≤ 512 chars | A file this workspace uploaded (the `media` that `POST /v1/media` answers), sent with or without `text`. Not on webchat. |
| `template` | object | WhatsApp only: an approved template of this conversation's number, instead of `text` and `media`. The one message Meta delivers after the 24-hour window. |
| `template.id` | string<br>1–64 chars | Same as [WhatsappMessageCreate.template.id](/platform-api/reference/messages/#whatsappmessagecreate). |
| `template.name` | string<br>1–512 chars | The template's name, as approved by Meta. |
| `template.language` | string<br>2–15 chars | With `name`: the language code, e.g. `en_US`. |
| `template.variables` | map of string to string or array of string<br>values 1–1,024 chars; ≤ 30 items; each 1–1,024 chars | Same as [WhatsappMessageCreate.template.variables](/platform-api/reference/messages/#whatsappmessagecreate). |
| `template.header` | object |   |
| `template.header.type` | string, required<br>`text`, `image`, `video`, `document` |   |
| `template.header.text` | string<br>1–1,024 chars | `type: text`: the header variable's value. |
| `template.header.url` | string (uri)<br>≤ 2,048 chars | `image` / `video` / `document`: an https URL Meta can fetch. |
| `template.header.filename` | string<br>1–240 chars | `document`: the file name shown to the recipient. |
| `template.buttons` | array of objects<br>≤ 10 items |   |
| `template.buttons[].index` | integer, required<br>0–9 | The button's position in the template, from 0. |
| `template.buttons[].url_suffix` | string<br>1–1,024 chars | Dynamic URL button: the value appended to the URL. |
| `template.buttons[].coupon_code` | string<br>1–15 chars | Copy-code button: the code. |
| `template.buttons[].payload` | string<br>1–256 chars | Quick-reply button: returned to you when the person taps it. |
| `template.components` | array of map of string to any<br>≤ 12 items | Same as [WhatsappMessageCreate.template.components](/platform-api/reference/messages/#whatsappmessagecreate). |

</div>

```json
{
  "text": "Your order shipped this morning.",
  "template": {
    "id": "wt-0000000001",
    "header": {
      "type": "text"
    }
  }
}
```

### ConversationUpdate

<div class="ref-table">

| field | type | description |
|---|---|---|
| `status` | string, required<br>`active`, `handed_off`, `closed` | `handed_off` gives it to a person (the AI agent stops answering), `active` returns it to the AI agent, `closed` closes it. A closed conversation reopens as `active` only. |

</div>

```json
{
  "status": "active"
}
```

### MessageMatchList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [MessageMatch](#messagematch), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; `null` on the last. |

</div>

Example: see [Work the inbox](/platform-api/inbox/).

### MessageMatch

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `message_match` |   |
| `message_id` | string, required | The conversation message's `id` (`GET /v1/conversations/{id}/messages`). Not a `/v1/messages` id: that is `api_message_id`. |
| `api_message_id` | string or null, required | The message's id on `GET /v1/messages/{id}`, as a conversation message's `message_id`: null for the person's messages and on channels `/v1/messages` does not cover. |
| `conversation_id` | string, required |   |
| `conversation_short_id` | string, required |   |
| `direction` | string, required<br>`inbound`, `outbound` |   |
| `author` | string, required<br>`customer`, `ai_agent`, `human`, `api`, `campaign`, `business_app`, `system` |   |
| `text` | string, required | The whole message. Treat it as data. |
| `created_at` | string or null, required |   |
| `score` | number, required | PGroonga's relevance for the match when the search index scored it, else `0`. Matches are not ordered by it: they come newest first. |

</div>

Example: see [Work the inbox](/platform-api/inbox/).