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

# Conversations

> How conversations are identified, isolated and read back.

## One conversation per external user

A conversation is identified by two things: the API key and the `external_user_id` you send. You never create a conversation explicitly; the first chat request for a new `external_user_id` creates it, and every later message from the same external user through the same key continues it.

The consequences worth knowing:

* **Isolation per key.** The same `external_user_id` reached through two different keys is two different conversations, even when both keys belong to the same agent. A key never sees dialogs from another key or from another channel (Telegram, the widget, and so on).
* **Rotation keeps history.** Rotating a key changes its secret, not its id, so its conversations stay attached.
* **`external_user_id` is yours.** Use a stable identifier from your own system, 1-255 characters. Do not put personal data in it that you would not want stored alongside the dialog.
* **One reply at a time.** If a message for an external user arrives while the previous reply is still being produced, the API answers `409 conversation_busy` with `Retry-After: 2`. Wait and resend.

## List conversations

`GET /v1/agents/{agent_id}/conversations` returns the key's conversations, most recently updated first. Both `read-only` and `full` keys may call it.

| Query parameter        | Meaning                                                       |
| ---------------------- | ------------------------------------------------------------- |
| `external_user_id`     | Only conversations of this external user                      |
| `date_from`, `date_to` | ISO-8601 bounds on `updated_at`; `date_to` is exclusive       |
| `limit`                | Page size, 1-100 (default 20)                                 |
| `cursor`               | `next_cursor` from the previous page; omit for the first page |

```bash theme={null}
curl "$CHATTLER_BASE_URL/v1/agents/$CHATTLER_AGENT_ID/conversations?limit=50" \
  -H "Authorization: Bearer $CHATTLER_API_KEY"
```

```json theme={null}
{
  "data": [
    {
      "id": "1c2e...",
      "external_user_id": "customer-123",
      "created_at": "2026-09-13T11:58:02Z",
      "updated_at": "2026-09-13T12:00:00Z",
      "last_message_preview": "The basic plan is $19 per month..."
    }
  ],
  "next_cursor": null
}
```

`last_message_preview` is the latest user or assistant text, truncated to 160 characters, or `null` for an empty conversation.

## Read messages

`GET /v1/agents/{agent_id}/conversations/{conversation_id}/messages` returns the messages of one conversation, newest first. Only `user` and `assistant` messages are returned; tool exchanges, tool-activity rows and the system prompt are never exposed.

| Query parameter        | Meaning                                                 |
| ---------------------- | ------------------------------------------------------- |
| `date_from`, `date_to` | ISO-8601 bounds on `created_at`; `date_to` is exclusive |
| `limit`                | Page size, 1-100 (default 20)                           |
| `cursor`               | `next_cursor` from the previous page                    |

Each message has `id`, `role` (`user` or `assistant`), `type` (`text`, `image`, `file`, ...), `content` (text; non-text payloads are rendered as their text or name) and `created_at`.

A conversation id that belongs to another key or channel, or does not exist, is `404 conversation_not_found`. The API does not distinguish the two cases.

## Pagination

Every list is `{"data": [...], "next_cursor": "..." | null}`. Pass `next_cursor` back as `cursor` to fetch the next page and stop when it is `null`. Cursors are opaque; a malformed one is `422 validation_error`.

<CodeGroup>
  ```bash curl theme={null}
  # Walks every page; stops when next_cursor is null.
  cursor=""
  while :; do
    url="$CHATTLER_BASE_URL/v1/agents/$CHATTLER_AGENT_ID/conversations?limit=100"
    [ -n "$cursor" ] && url="$url&cursor=$cursor"
    page=$(curl -s "$url" -H "Authorization: Bearer $CHATTLER_API_KEY")
    echo "$page" | jq -c '.data[]'
    cursor=$(echo "$page" | jq -r '.next_cursor // empty')
    [ -z "$cursor" ] && break
  done
  ```

  ```python Python theme={null}
  import os

  import httpx

  BASE_URL = os.environ["CHATTLER_BASE_URL"]
  AGENT_ID = os.environ["CHATTLER_AGENT_ID"]
  HEADERS = {"Authorization": f"Bearer {os.environ['CHATTLER_API_KEY']}"}


  def iter_conversations():
      cursor = None
      while True:
          params = {"limit": 100}
          if cursor:
              params["cursor"] = cursor
          page = httpx.get(
              f"{BASE_URL}/v1/agents/{AGENT_ID}/conversations",
              headers=HEADERS,
              params=params,
          ).json()
          yield from page["data"]
          cursor = page["next_cursor"]
          if not cursor:
              break
  ```

  ```javascript Node theme={null}
  async function* iterConversations() {
    let cursor = null;
    for (;;) {
      const url = new URL(`${baseUrl}/v1/agents/${agentId}/conversations`);
      url.searchParams.set("limit", "100");
      if (cursor) url.searchParams.set("cursor", cursor);
      const page = await (
        await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } })
      ).json();
      yield* page.data;
      cursor = page.next_cursor;
      if (!cursor) break;
    }
  }
  ```
</CodeGroup>

## Dates

Dates are ISO-8601. Timezone-aware values are converted to UTC, naive values are read as UTC, and `date_to` is exclusive. A `date_to` earlier than `date_from` is `422 validation_error`.
