> ## Documentation Index
> Fetch the complete documentation index at: https://docs.impellabs.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Conversations

> Reading a merchant's threads, replying into them, and taking one off the assistant.

We are the system of record for a merchant's conversations. Your product reads
them, replies into them, and decides when a person should take over.

<Note>
  These routes need `messages:read` and `messages:write`. They are deliberately
  **not** covered by `assistants:write` — a key issued to edit a prompt should not
  thereby be able to message a merchant's customers.
</Note>

## Reading

```http theme={null}
GET /api/v1/partner/tenants/{ref}/conversations/
GET /api/v1/partner/tenants/{ref}/conversations/{id}/
GET /api/v1/partner/tenants/{ref}/conversations/{id}/messages/?limit=50&after={message_id}
```

Messages page oldest-first on a stable cursor.

```json theme={null}
{ "object": "list", "data": [ … ], "has_more": true, "next_cursor": "…" }
```

`limit` is 1–100, default 50. A cursor naming nothing in this conversation is a
**400**, not an empty page — silently returning the first page would make a
paging client loop forever.

### The message shape

```json theme={null}
{
  "id": "…",
  "object": "message",
  "conversation": "…",
  "role": "user",
  "type": "text",
  "text": "Do you deliver to Kochi?",
  "created_at": "2026-09-07T10:12:00Z",
  "channel": {
    "name": "whatsapp",
    "message_id": "wamid.HBg…",
    "from": "+919876543210",
    "media": { "mime": "audio/ogg", "transcription": "…" },
    "template": { "name": "order_update" }
  }
}
```

<Warning>
  **`role` has three values and the third one is the point.** `user` is the
  customer, `assistant` is the AI, and `human` is a person — a salesperson
  replying from the WhatsApp Business App, or your own team through this API.

  An inbox that renders `assistant` and `human` identically is an inbox showing
  its operator something untrue.
</Warning>

Never present: prompts, retrieval evidence, token counts, internal reply-button
ids, or Meta media ids — the last are useless without the merchant's own access
token anyway.

## Sending

```http theme={null}
POST /api/v1/partner/tenants/{ref}/conversations/{id}/messages/send/
```

```json theme={null}
{ "type": "text", "text": "Yes — next-day delivery to Kochi.", "reply_to": "wamid.…" }
```

<ParamField body="type" type="string" default="text">
  `text`, `template`, `image`, `video`, `audio`, `document`, `interactive`,
  `location` or `reaction`.
</ParamField>

<ParamField body="reply_to" type="string">
  A `channel.message_id` from this thread. WhatsApp draws it as a quoted reply,
  which is how an answer to a question asked four messages ago stops reading like
  a non-sequitur.
</ParamField>

| `type`                              | Fields                                                     |
| ----------------------------------- | ---------------------------------------------------------- |
| `text`                              | `text`                                                     |
| `template`                          | `template_name`, `lang` (default `en_US`), `body_params[]` |
| `image` `video` `audio` `document`  | `link` **or** `media_id`, `caption`, `filename`            |
| `interactive` `location` `reaction` | `payload` — the Cloud API object                           |

Prefer `link` over `media_id` wherever a public URL exists: Meta fetches and
caches by URL, so one catalogue photo sent to a thousand customers is fetched
once instead of uploaded once and served a thousand times.

**201** returns the stored message and the conversation. It is recorded as
`role: "human"` and fires `message.sent` like any other outbound message.

### Two failures to handle

<AccordionGroup>
  <Accordion title="422 outside_customer_service_window">
    The 24-hour window has closed and this is not a template.

    ```json theme={null}
    { "error": {
        "code": "outside_customer_service_window",
        "message": "The 24-hour customer service window is closed. Send an approved template instead.",
        "window": { "open": false, "last_inbound_at": "…", "expires_at": "…" }
    } }
    ```

    Checked **before** Meta is called, so you get a named condition and a remedy
    rather than an unmapped provider error. Read `window.expires_at` off the
    conversation and show a countdown instead of discovering the limit by failing.
  </Accordion>

  <Accordion title="502 channel_rejected">
    Meta refused the message. Their words are in the body. Common causes: a template
    that is not approved yet, a number that has been blocked by the recipient, or a
    media URL Meta could not fetch.
  </Accordion>
</AccordionGroup>

## Taking over from the assistant

```http theme={null}
POST /api/v1/partner/tenants/{ref}/conversations/{id}/handoff/
```

```json theme={null}
{ "on": true, "reason": "agent_took_over" }
```

While a human holds a conversation the assistant stays silent and stops
nudging. Send `{"on": false}` to give it back.

Handover is a judgement somebody made, so only somebody releases it. The one
exception is credit exhaustion: a conversation paused because the merchant ran
out of credits resumes on its own when they top up, provided no human has
replied in the meantime.

<Note>
  The merchant's own staff can do this from their phone. Replying from the
  WhatsApp Business App takes over automatically, and `/human`, `/ai` and
  `/clear` pause, release and wipe a thread. Every one of those reaches you as
  `handoff.requested` — so your inbox stays correct even when nobody used your UI.
</Note>

## Asking the assistant to answer

```http theme={null}
POST /api/v1/partner/tenants/{ref}/conversations/{id}/turns/
```

```json theme={null}
{ "text": "optional message to inject first", "force": false }
```

**202**, and the turn runs asynchronously — the reply arrives as `message.sent`.
Useful for a "draft a reply" button, or to re-run a turn that failed on a
provider outage.

Refused with **409 `human_control`** while a human holds the conversation,
unless you pass `force`. Without that, your own takeover would be silently
undone by your own next call.

## What a conversation looks like

```json theme={null}
{
  "id": "…",
  "object": "conversation",
  "status": "waiting_reply",
  "channel": "whatsapp",
  "agent": "…",
  "turns": 6,
  "contact": { "name": "Priya", "phone": "+919876543210", "lead_id": "…" },
  "human_control": { "on": false, "reason": "", "since": "" },
  "window": { "open": true, "last_inbound_at": "…", "expires_at": "…" },
  "whatsapp_number": "106540352242922",
  "created_at": "…", "updated_at": "…"
}
```

`whatsapp_number` is the number this thread is happening on. Replies go out from
it automatically — answering from a merchant's other number would arrive, to the
customer, as a message from a stranger.
