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

# Conversations

> A thread between an identity and an external recipient. We do the threading; you address the conversation by id.

A **conversation** is a thread between one identity and one external
recipient. Each conversation has a stable `convId` (the only identifier
your code needs), an event-sourced timeline, and a few summary fields
that update as the conversation evolves.

The first send to a recipient creates a conversation. Replies stitch
into it by `Message-ID` / `References` headers. A reply that doesn't
match any existing conversation creates a new one with type
`email.received`.

## The shape

`GET /v1/identities/:handle/conversations/:convId` returns this:

```json theme={null}
{
  "identity": "alice.acme@inboxbase.ai",
  "conversation": {
    "id":             "conv_dd6e7130674645d3",
    "with":           "morgan@northwindrobotics.com",
    "subject":        "Quick intro — fleet rotation",
    "messageCount":   5,
    "archived":       false,
    "labels":         ["hot-lead"],
    "lastEventAt":    1777278929000,
    "noReplyAt":      null,
    "lastNoReplyAt":  null,
    "events":         [/* discriminated union, see below */]
  }
}
```

`events[]` is the source of truth. Everything else is a projection of
it that we materialize for you so you don't have to.

## The timeline

`events[]` is a discriminated union. Today it includes:

| `type`                    | What it is                                                        |
| ------------------------- | ----------------------------------------------------------------- |
| `message`                 | A message sent or received. Body, direction, snippet, timestamps. |
| `no_reply_expired`        | The no-reply timer fired without a reply. Carries `waitedMs`.     |
| `label_added`             | A label was added to the conv (today, dashboard-only).            |
| `label_removed`           | A label was removed.                                              |
| `archived` / `unarchived` | The conv was archived or unarchived.                              |

To pull just the messages, filter on `e.type === "message"` and read
`e.message`. To pull just the no-reply expirations, filter on
`e.type === "no_reply_expired"`. The discriminator is always `type`.

```js theme={null}
const messages = conv.events
  .filter((e) => e.type === "message")
  .map((e) => e.message);
```

## Threading we handle

Three things have to be right for a thread to look correct in the
recipient's inbox:

* **Subject line.** Replies use `Re: <original>` (we trim chains of
  `Re:` to a single one).
* **In-Reply-To and References headers.** Both are set to the most
  recent message's `Message-ID`, and References is the cumulative chain.
* **Sender continuity.** All messages in one conversation go from the
  same backing mailbox, even though our rotation picks a different one
  for new conversations. This is recipient affinity.

You get this for free when you reply by `convId`:

```bash theme={null}
curl https://api.inboxbase.ai/v1/identities/alice.acme@inboxbase.ai/send \
  -H "Authorization: Bearer sk_live_..." \
  -d '{ "convId": "conv_dd6e...", "text": "Friday 10 works." }'
```

We resolve the recipient, the subject, the threading headers, and the
mailbox from the `convId`. You pass the body, nothing else.

## Filtering and pagination

`GET /conversations` returns a paginated list, newest activity first.
Filters live in query params:

| Param        | Type       | Effect                                  |
| ------------ | ---------- | --------------------------------------- |
| `archived`   | bool       | Include only archived (or only active). |
| `with`       | email      | Exact correspondent match.              |
| `withDomain` | string     | Suffix match (e.g. `acme.com`).         |
| `since`      | ms epoch   | `lastEventAt >= since`.                 |
| `until`      | ms epoch   | `lastEventAt <= until`.                 |
| `cursor`     | ms epoch   | Paginate before this `lastEventAt`.     |
| `limit`      | int 1..200 | Page size; default 50.                  |

The list rows are summary shapes — id, correspondent, subject, snippet,
message count, last event timestamps. Not the full timeline. For the
timeline you fetch one conv at a time.

## Reading is cheap; copying is wasteful

The conversation read endpoint serves a fresh snapshot every call, off
the materialized state inside the durable object that owns the
conversation. There is no version of "give me the diff since last
read" — and you don't need one. The pattern we want you to use is:

* **Events stream** tells you something changed on `convId X`.
* **GET /conversations/:convId** gives you the current state.

You never reduce events into your own copy of the thread. We're the
reducer. See [Live thread list](/guides/live-thread-list) for the
full pattern.

<Note>
  We don't track per-conversation **read state**. Every integrator
  wants different semantics — a CRM marks "read" when the rep opens it,
  an agent marks it when the model has consumed it, a monitoring tool
  marks it when an alert was acknowledged. Run your own cursor on top
  of `lastEventAt` or webhook delivery dedup; it's three lines of code
  and you get the semantics you actually want.
</Note>
