# Manage conversations (/core/conversations)

<!-- agent-signals: reading_time_min: 7 · est_tokens: 3002 · updated: 2026-09-06 -->
Related: [Receive email](/core/receive.md), [Send and reply](/core/send.md), [Control who can email your agent](/core/inbound-control.md), [Connect external mail tools: IMAP and SMTP](/core/imap-smtp.md)



# Manage conversations as threads

A thread is the conversation around a message: the email that started it plus every reply in both directions, built automatically from reply headers. Use these endpoints to pick the next conversation to handle, read one in full, search by content across one inbox or the whole organization, and track per-conversation state with labels.

## Do this

Work an inbox in three calls: list its threads, read one thread in full, then label the handled message.

```bash
# 1. List threads. Default view: threads with at least one received message, newest received first.
curl "https://api.agentmail.to/v0/inboxes/example@agentmail.to/threads?limit=10" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"

# 2. Read a whole thread by the thread_id from step 1. Messages come back oldest first.
curl "https://api.agentmail.to/v0/threads/<thread_id>" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"

# 3. Track state: label the handled message. URL-encode the message_id in the path.
curl -X PATCH "https://api.agentmail.to/v0/inboxes/example@agentmail.to/messages/<url_encoded_message_id>" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "add_labels": ["triaged"], "remove_labels": ["waiting-reply"] }'
```

Feed the model each new message's `extracted_text`, not `text`, so it does not re-read the quoted history replayed inside every reply.

## SDK

Install: `npm install agentmail` (TypeScript), `pip install agentmail` (Python), `npm install -g agentmail-cli` (CLI).

* List inbox threads: TS `client.inboxes.threads.list("example@agentmail.to")`, Python `client.inboxes.threads.list(inbox_id="example@agentmail.to")`, CLI `agentmail inboxes:threads list --inbox-id "example@agentmail.to"`
* Read a thread: TS `client.threads.get("<thread_id>")`, Python `client.threads.get(thread_id="<thread_id>")`, CLI `agentmail threads get --thread-id "<thread_id>"`
* Search inbox threads: TS `client.inboxes.threads.search("example@agentmail.to", { q: "refund" })`, Python `client.inboxes.threads.search(inbox_id="example@agentmail.to", q="refund")`, CLI `agentmail inboxes:threads search --inbox-id "example@agentmail.to" -q "refund"`
* Search inbox messages: TS `client.inboxes.messages.search("example@agentmail.to", { q: "refund" })`, Python `client.inboxes.messages.search(inbox_id="example@agentmail.to", q="refund")`, CLI `agentmail inboxes:messages search --inbox-id "example@agentmail.to" -q "refund"`
* List threads across the organization: TS `client.threads.list()`, Python `client.threads.list()`, CLI `agentmail threads list`
* Search threads across the organization: TS `client.threads.search({ q: "refund" })`, Python `client.threads.search(q="refund")`, CLI `agentmail threads search -q "refund"`
* Update message labels: TS `client.inboxes.messages.update("example@agentmail.to", "<message_id>", { addLabels: [...], removeLabels: [...] })`, Python `client.inboxes.messages.update(inbox_id="example@agentmail.to", message_id="<message_id>", add_labels=[...], remove_labels=[...])`, CLI `agentmail inboxes:messages update --inbox-id "example@agentmail.to" --message-id "<message_id>" --add-labels triaged --remove-labels waiting-reply`
* Batch update labels: TS `client.inboxes.messages.batchUpdate("example@agentmail.to", { messageIds: [...], addLabels: [...] })`, Python `client.inboxes.messages.batch_update(inbox_id="example@agentmail.to", message_ids=[...], add_labels=[...])`

Full reference: `/integrations/sdks-and-cli`.

## Facts

* Sending an email creates its thread immediately. The send response returns the new `thread_id`, and `GET /v0/threads/{thread_id}` works right away.
* The default `GET /v0/inboxes/{inbox_id}/threads` list holds only threads with at least one received message, ordered by the newest received message. An outbound thread nobody has answered is absent from it.
* Filtering `GET /v0/inboxes/{inbox_id}/threads` by `labels=sent` returns every thread with sent mail, newest send first. That view covers unanswered outbound threads.
* Thread list parameters: `limit`, `page_token`, `labels`, `before`, `after` (RFC 3339 timestamps), `ascending`, `senders`, `recipients`, `subject`, `include_spam`, `include_blocked`, `include_unauthenticated`, `include_trash`.
* `senders`, `recipients`, and `subject` values match whole words and word starts, every passed value must match, and any of these filters routes the request through search, which caps `limit` at 100.
* A thread summary's `labels` is the union of every label on the thread's messages. `preview` is the beginning of the newest message. `timestamp` is the newest activity, with `received_timestamp` and `sent_timestamp` per direction. `last_message_id` is the newest message's id, ready for a reply call.
* `GET /v0/threads/{thread_id}` returns the whole conversation, messages oldest first. Any key whose scope covers the owning inbox can call it.
* Thread ids are UUIDs. Message ids look like `<...@email.amazonses.com>` and contain `<`, `>`, and `@`, so URL-encode a `message_id` when building an API path by hand. The CLI and SDKs encode it automatically.
* Message bodies: `text` and `html` are the body as sent, including the quoted copy of earlier messages. `extracted_text` and `extracted_html` are only what the message added.
* Search endpoints: `GET /v0/inboxes/{inbox_id}/threads/search`, `GET /v0/inboxes/{inbox_id}/messages/search`, and `GET /v0/threads/search`. `q` is required. Optional: `limit` (up to 100), `page_token`, `before`, `after`.
* Search matches `q` against senders, recipients, and subject as whole words and word starts, and against message bodies as full text (`refund` also finds `refunds`). Results rank by relevance, not recency.
* Search covers sent and received mail, including outbound threads the default list leaves out. Threads carrying `spam`, `trash`, `blocked`, or `unauthenticated` are always excluded from search.
* Search hits add `highlights`. Keys `from`, `recipients`, `subject`, and `text` appear only when that field matched, with matched terms wrapped in `**`.
* Reach of the organization-wide `GET /v0/threads` and `GET /v0/threads/search` follows the API key: an organization key covers every inbox, a pod key covers its pod's inboxes, an inbox key covers only its own inbox. Every result row carries its `inbox_id`.
* Each inbox holds its own copy of a conversation. Mail between two owned inboxes shows up once per inbox, with a different `thread_id` in each.
* System labels are written by AgentMail: `received`, `sent`, `bounced`, `complained`, `delayed`, `delivered`, `rejected`, `opened`, `scheduled`. Custom labels are any other string.
* Label update: `PATCH /v0/inboxes/{inbox_id}/messages/{message_id}` with `add_labels`, `remove_labels`, or both. The response is the message's full label set after the change.
* Labels are trimmed and lowercased on write. A label can be up to 256 characters, and a message can carry up to 256 labels. Custom labels can also be attached at send time via the `labels` field.
* Adding a label a message already has, or removing one it lacks, changes nothing, so retrying a label update is safe. A label in both lists ends up removed.
* A label filter only shows threads with received mail unless `sent` is among the passed labels. Label filters are served by search, so new mail and label changes take a few seconds to show in them. Reading the message or thread directly shows changes immediately.
* Batch label update: `POST /v0/inboxes/{inbox_id}/messages/batch-update` with `message_ids` (up to 50) plus `add_labels` and/or `remove_labels`. The batch is atomic: every resolved message changes or none do. Ids that do not resolve (deleted, another inbox's message, or hidden from the key) are skipped silently.
* In a batch response, `limit` is the number of ids sent and `count` the number updated. `count` below `limit` means some ids were skipped. Compare `updates[].message_id` with the sent ids to find which.

## Not supported

* No endpoint creates, edits, or deletes a thread. AgentMail builds threads automatically from reply headers.
* System labels cannot be added or removed. A system label in `add_labels` or `remove_labels` returns `400` `validation_error` ("Cannot use system label: sent").
* A label update with both `add_labels` and `remove_labels` empty returns `400` `validation_error`.
* The default thread list does not show outbound threads with no reply yet. Filter `labels` by `sent` or fetch the thread directly by `thread_id`.
* Search has no include flags. Threads carrying `spam`, `trash`, `blocked`, or `unauthenticated` never appear in search results.
* Search and filtered lists reject `limit` above 100.
* Batch update rejects more than 50 `message_ids` or the same id twice, and does not report skipped ids as errors.
* Search results and label filters do not reflect changes instantly. Read the message or thread directly for current state.

## Errors

| Error                                                               | Status | Cause                                                                  | Fix                                                                     |
| ------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `not_found` ("Inbox not found")                                     | 404    | The inbox is not visible to the API key, usually a typo in `inbox_id`. | Check `inbox_id` and the key's scope.                                   |
| `not_found` ("Thread not found")                                    | 404    | The id matches no thread visible to the key.                           | Check the `thread_id` and that the key's scope covers the owning inbox. |
| `validation_error` ("Invalid UUID")                                 | 400    | The value is not a thread id, usually a `message_id` passed instead.   | Pass the UUID `thread_id`.                                              |
| `validation_error` ("limit cannot exceed 100 for filtered queries") | 400    | `limit` above 100 on search or a filtered list.                        | Set `limit` to 100 or less.                                             |
| `validation_error` (missing or empty `q`)                           | 400    | Search called without search text.                                     | Pass a non-empty `q`.                                                   |
| `validation_error` ("Cannot use system label: sent")                | 400    | A system label in `add_labels` or `remove_labels`.                     | Edit custom labels only.                                                |
| `validation_error` (empty label update)                             | 400    | Both `add_labels` and `remove_labels` empty.                           | Pass at least one label change.                                         |
| `validation_error` (batch limits)                                   | 400    | More than 50 `message_ids`, or the same id twice.                      | Dedupe ids and send at most 50 per batch.                               |

## Verify

```bash
curl -s -w "\n%{http_code}\n" "https://api.agentmail.to/v0/inboxes/example@agentmail.to/threads?limit=1" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"
```

A `200` with `count` and a `threads` array confirms the key and inbox. Each thread carries `thread_id`, `labels`, `message_count`, and `last_message_id`. A label update verifies itself: the `PATCH` response is the full label set after the change.

## Related

* `/core/inbound-control` - decide which senders reach the threads listed here
* `/core/receive#read-one-message-in-full` - how quoted text and `extracted_text` relate
* `/core/send` - reply to a thread's `last_message_id` and set `labels` at send time
* `/advanced/multi-tenant` - pod paths for keeping tenants apart
