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

# Messages and threads

> Send, reply, forward, and read conversations back — plus how replies find the right thread.

## Sending

```bash theme={null}
POST /v0/inboxes/{inbox_id}/messages/send
```

```json theme={null}
{
  "to": "customer@example.com",
  "subject": "Your refund for order 4471",
  "text": "Hi — the refund is processed.",
  "html": "<p>Hi — the refund is processed.</p>"
}
```

| Field                 | Required | Notes                                                                   |
| --------------------- | -------- | ----------------------------------------------------------------------- |
| `to`                  | Yes      | A single address or an array.                                           |
| `text`                | Yes      | Plain-text body.                                                        |
| `subject`             | No       | Up to 998 characters.                                                   |
| `cc`, `bcc`           | No       | Arrays of addresses.                                                    |
| `html`                | No       | Sent alongside `text` as a multipart alternative.                       |
| `reply_to`            | No       | Overrides the generated thread-token `Reply-To`. See the warning below. |
| `thread_id`           | No       | Sends onto an existing thread.                                          |
| `reply_to_message_id` | No       | Sets `In-Reply-To` to a specific parent.                                |
| `labels`              | No       | Applied to the stored message.                                          |
| `headers`             | No       | Extra headers, as a flat object.                                        |
| `attachments`         | No       | Up to 32, base64-encoded.                                               |

### Sending onto an existing thread

Pass `thread_id` and the message joins that conversation — no inbound message required. This is how an agent chases something it promised last week:

```json theme={null}
{
  "to": "customer@example.com",
  "thread_id": "thd_4tq8rv2mzx7kd9nbhf3wpc6s",
  "text": "Following up — did that refund land?"
}
```

This is the capability most email-for-agents setups lack. An agent limited to replying inside an inbound webhook can only ever react; one that can open its own turn on a thread can actually follow up.

## Replying

```bash theme={null}
POST /v0/inboxes/{inbox_id}/messages/{message_id}/reply
```

```json theme={null}
{ "text": "Checked again — it cleared this morning." }
```

Recipients are derived from the parent message, so `to` is optional. By default the reply goes to the parent's `Reply-To` (falling back to its `From`). Pass `reply_all: true` to include everyone on the original `to` and `cc`, minus the inbox itself.

There is no cap on replies per conversation.

## Forwarding

```bash theme={null}
POST /v0/inboxes/{inbox_id}/messages/{message_id}/forward
```

```json theme={null}
{ "to": "escalations@example.com", "text": "Passing this to billing." }
```

The original message is quoted beneath your note.

## Reading messages

```bash theme={null}
GET /v0/inboxes/{inbox_id}/messages?limit=50
GET /v0/inboxes/{inbox_id}/messages/{message_id}
GET /v0/inboxes/{inbox_id}/messages/{message_id}/raw
```

`/raw` returns the original MIME source, byte for byte, when it was retained.

### The message object

```json theme={null}
{
  "message_id": "msg_4tq8rv2mzx7kd9nbhf3wpc6s",
  "thread_id": "thd_9k3mxz7bd2vr8ncq4twf6hps",
  "inbox_id": "ava@agent.waterr.ai",
  "direction": "received",
  "from": { "name": "Dana Reed", "address": "dana@example.com" },
  "to": [{ "address": "ava@agent.waterr.ai" }],
  "cc": [],
  "subject": "Re: Your refund for order 4471",
  "preview": "Still nothing on my end. Can you check again?",
  "text": "Still nothing on my end. Can you check again?\n\nOn Tue, 23 Sep 2026 at 14:02, Ava wrote:\n> Hi — the refund is processed...",
  "extracted_text": "Still nothing on my end. Can you check again?",
  "attachments": [],
  "labels": ["inbox", "unread"],
  "created_at": "2026-09-24T09:15:22.881Z"
}
```

<Tip>
  Give your model `extracted_text`, not `text`. `text` is the body exactly as it arrived, which on a mature thread is mostly the conversation quoted back at you — it burns context and tempts the model into answering a message from two weeks ago.
</Tip>

### Labels

`PATCH /v0/inboxes/{inbox_id}/messages/{message_id}` sets labels. The service applies a few itself:

| Label        | Meaning                                                        |
| ------------ | -------------------------------------------------------------- |
| `inbox`      | Delivered normally.                                            |
| `unread`     | Not yet marked read by your code.                              |
| `auto-reply` | Detected as a vacation responder or other automated mail.      |
| `blocked`    | Matched a block list; stored, but kept out of the normal flow. |

The `auto-reply` label is the one to check before your agent answers. Replying to an out-of-office generates another out-of-office, and two automated systems will happily do that to each other for as long as you let them.

## Threads

```bash theme={null}
GET /v0/inboxes/{inbox_id}/threads
GET /v0/inboxes/{inbox_id}/threads/{thread_id}
GET /v0/threads                              # across every inbox in scope
DELETE /v0/inboxes/{inbox_id}/threads/{thread_id}
```

Fetching a single thread includes its `messages` array in order, which is what you want when assembling context for a model.

```json theme={null}
{
  "thread_id": "thd_9k3mxz7bd2vr8ncq4twf6hps",
  "inbox_id": "ava@agent.waterr.ai",
  "subject": "Your refund for order 4471",
  "preview": "Still nothing on my end. Can you check again?",
  "participants": ["dana@example.com", "ava@agent.waterr.ai"],
  "message_count": 4,
  "created_at": "2026-09-23T14:02:10.004Z",
  "updated_at": "2026-09-24T09:15:22.881Z"
}
```

`participants` has sub-address tags stripped, so `ava+thd_abc.9f2@agent.waterr.ai` appears as `ava@agent.waterr.ai` and a thread does not look like it has a new participant every time someone replies.

## Searching

```bash theme={null}
GET /v0/inboxes/{inbox_id}/search?q=refund&limit=20
```

Full-text search across the messages in one inbox. `q` is required; omitting it returns `422`.

## How threading works

Getting a reply onto the right conversation is the hard part of agent email, and it drives the design.

Two obvious signals are both unreliable. **Message-ID** is assigned by the sending provider, and some providers — Cloudflare included — refuse to let you set it and do not report the one they chose, so a sent message cannot always contribute its own id to the chain. **Subject and References** get rewritten, stripped or dropped by real mail clients.

So resolution runs in layers, strongest signal first:

<Steps>
  <Step title="Reply-To thread token">
    Outbound mail carries `Reply-To: ava+thd_9k3mxz.a41f@agent.waterr.ai`. The `+tag` routes to the same inbox, so it costs no extra DNS or configuration, and it survives any subject edit. The tag is HMAC-signed, so a guessed or tampered token is rejected rather than filing mail onto someone else's thread.
  </Step>

  <Step title="In-Reply-To and References">
    Matched against stored message ids, for clients that preserve them.
  </Step>

  <Step title="Normalized subject plus participant overlap">
    Within a time window. `Re: Fwd: RE: Invoice` normalizes to `invoice`.
  </Step>

  <Step title="New thread">
    If nothing matches, a new thread is created rather than guessing.
  </Step>
</Steps>

The `message.received` event carries a `matched_by` field naming which layer resolved it (`token`, `references`, `subject` or `new`), so a threading problem is diagnosable from your event log rather than being a mystery. It travels on the event, not on the stored message.

<Warning>
  Setting `reply_to` yourself **replaces** the thread token, and drops resolution to the weaker layers. Only override it when you have a specific reason, and expect threading to get less reliable when you do.
</Warning>
