> ## 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

> Durable transcripts: opening one, appending turns, and reading it back.

A conversation is a durable thread. Use one when the transcript matters — when
the next turn should see the previous ones, or when a person will read the
history later. If neither is true, [`POST /responses`](/v2/responses) is cheaper
to reason about and stores nothing that was said.

## Open one

```bash theme={null}
curl "$IMPEL_API/conversations" \
  -H "Authorization: Bearer $IMPEL_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c…" \
  -d '{
    "agent": "agt_support",
    "external_ref": "zendesk-ticket-88421",
    "metadata": { "queue": "billing" }
  }'
```

Needs `conversations:create`.

A conversation is bound to its tenant, environment and principal **at creation**
rather than acquiring a binding later. A window in which there is nothing to
check is a window in which every check passes.

### Two ways not to create a duplicate

<AccordionGroup>
  <Accordion title="`external_ref` — your own id for the thread">
    Resolving the same `external_ref` twice returns the **same conversation**
    rather than a second one beside it. That is what a CRM retrying a webhook
    needs, and it survives long past any idempotency window.

    The uniqueness is scoped by `(environment, tenant, external_ref)` and by
    nothing less. A blank ref resolves to nothing at all — a missing public
    identity must never resolve to a real one.

    Printable, no newlines, up to 200 characters.
  </Accordion>

  <Accordion title="`Idempotency-Key` — for when there is no external ref">
    The ordinary 24-hour replay contract. Covers the retry that happens before
    your side has an id to hand us.

    A duplicate conversation is a duplicate transcript and a split history, so
    this endpoint is one of the ones where sending a key is worth the trouble.
    [Idempotency](/v2/idempotency)
  </Accordion>
</AccordionGroup>

## Append a turn

```bash theme={null}
curl "$IMPEL_API/conversations/cnv_…/messages" \
  -H "Authorization: Bearer $IMPEL_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9a2e…" \
  -d '{ "input": "And if the item was on sale?" }'
```

Needs `conversations:write`. Same options and same response shape as
[`/responses`](/v2/responses) — including `"stream": true`.

<Note>
  The body does not name an agent. It is taken from the conversation. A body that
  named a different one would be a request to run somebody else's assistant on
  this transcript.
</Note>

## Read it back

```bash theme={null}
curl "$IMPEL_API/conversations/cnv_…" \
  -H "Authorization: Bearer $IMPEL_KEY"

curl "$IMPEL_API/conversations/cnv_…/messages?limit=50" \
  -H "Authorization: Bearer $IMPEL_KEY"
```

Both need `conversations:read`. Messages come back **oldest first**.

### Paging

This endpoint returns `next_cursor` and reads it back as **`after`** — not
`cursor`, which is what the document and extraction endpoints use. Its cursor is
a **message id** rather than an encoded pair.

```
GET /conversations/cnv_…/messages?limit=50
  → { "messages": [...], "next_cursor": "0f7a…" }

GET /conversations/cnv_…/messages?limit=50&after=0f7a…
```

`limit` defaults to 50 and is capped at 100 — a ceiling as well as a default,
because "give me every message" on a two-year-old conversation is a request that
succeeds slowly and then fails.

Ordering is on `(created_at, id)` and paging is on the same pair. A timestamp
alone is not stable: two messages written in the same millisecond would make a
page boundary ambiguous, and an ambiguous boundary is a message silently skipped
or repeated.

<Warning>
  An `after` cursor that names nothing in this conversation is a **400**, not an
  empty page. Silently returning the first page would make a paging client loop
  forever.
</Warning>

## Billing and the channel

A conversation opened over the Runtime API lives on the `api` channel. That is a
slug of its own and not an alias for the website: Runtime API spend is invoiced
and reported as itself rather than folded into another channel's column.

You can override it per turn with `channel` in the input — `web`, `website`,
`whatsapp`, `phone`, `voice` — when the Runtime API is carrying traffic that
really did arrive somewhere else. `""` means "not stated" and falls through to
the conversation's own channel.

## Not found means not found

The five-check binding gate runs before a single field is read. A guessed id gets
the same **404** whether it names another workspace's conversation or nothing at
all — byte for byte. There is no 403 that confirms existence.

## What a conversation stores

Rows recording what was said, and nothing about how the model was prompted. What
the model is shown is assembled at execution time and is not part of the
conversation state, so it is not readable back and not caller-settable.

Structured output is written to the transcript only after it validates. Output
that failed its schema never reaches the transcript at all — see
[Structured outputs](/v2/structured-outputs).
