Manage conversations
List and read whole email threads, search them, watch every inbox in your organization, and track conversation state with labels.
List the threads in an inbox
A message is one email. A thread is the conversation around it: the message that started it plus every reply, in both directions.
inbox_id is the only required input.
agentmail inboxes:threads list \
--inbox-id "example@agentmail.to"| Param | Type | What it means |
|---|---|---|
limit | integer | Threads per page. |
page_token | string | Where to resume. Pass the next_page_token from the previous page. |
labels | string[] | Only threads that carry every label you list. See Track conversation state with labels. |
before, after | RFC 3339 timestamp | Bound the results by time, like 2026-08-24T00:00:00Z. |
ascending | boolean | Oldest first instead of newest first. |
senders, recipients, subject | string[] | Match against any sender, any recipient, or the subject. Values match whole words and word starts. Pass several and every value must match. |
include_spam, include_blocked, include_unauthenticated, include_trash | boolean | Also return threads those labels normally hide. |
Sending an email creates a new thread_id, which you can use when reading the thread.
A thread you started that nobody has replied to yet is not in the default threads list until the first reply arrives. To get these threads, filter labels by sent.
{
"count": 1,
"threads": [
{
"organization_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"pod_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"inbox_id": "example@agentmail.to",
"thread_id": "7c00cf26-ab80-4e20-a8a6-0285223b385d",
"labels": ["received", "sent"],
"timestamp": "2026-08-24T14:46:47Z",
"received_timestamp": "2026-08-24T14:46:47Z",
"sent_timestamp": "2026-08-24T14:46:05Z",
"senders": [
"You <you@example.com>",
"Quickstart test <example@agentmail.to>"
],
"recipients": [
"example@agentmail.to",
"You <you@example.com>",
"Quickstart test <example@agentmail.to>"
],
"subject": "Weekly metrics report",
"preview": "Yes, all five refunds for week 35 are confirmed final.\n\nOn Mon, Aug 24, 2026 at 2:46 PM UTC example@agentmail.to wrote:\n\n> Thanks. Is the week 35 refund co",
"attachments": [
{
"attachment_id": "079de291-1800-4156-8f6a-3ee90fd701eb",
"filename": "weekly-metrics.csv",
"size": 38,
"content_type": "text/csv",
"content_disposition": "attachment"
}
],
"last_message_id": "<010001a0343cff6a-cb637a47-c313-4245-82ac-2638d40395dc-000000@email.amazonses.com>",
"message_count": 3,
"size": 14299,
"created_at": "2026-08-24T14:38:14Z",
"updated_at": "2026-08-24T14:53:01Z"
}
]
}Each entry summarizes the whole conversation rather than any single message:
labelsis the union of every label on the thread’s messages.receivedandsenttogether mean the conversation has traffic in both directions.sendersandrecipientsaccumulate everyone who has appeared, as a bare address orDisplay Name <address>depending on what the mail carried.previewis the beginning of the newest message, andmessage_countandsizetell you how much conversation there is.timestampis the newest activity, withreceived_timestampandsent_timestampfor the newest in each direction.
thread_id is what you pass to read the whole conversation next. last_message_id is the newest message’s id, ready to hand to a reply call without fetching anything else.
Read a whole thread
With a thread_id from a list, a search, a send response, or a webhook, one call returns the entire conversation: the summary fields above plus every message in full. thread_id is the only input. The request names only the thread, so any key whose scope covers the owning inbox can use it.
agentmail threads get \
--thread-id "<thread_id>"Messages come back oldest first: the first element started the thread and the last one is the newest. To continue the conversation, reply to that newest message from the inbox that owns the thread.
Each message carries its body in up to four fields, and the difference matters once replies start quoting each other:
textandhtmlare the body as it was sent, including the quoted copy of earlier messages that most mail clients append below a reply.extracted_textandextracted_htmlare only what that message added.
Give your model the thread once for context, then read each new message’s extracted_text, so it does not re-read the conversation replayed inside every reply.
{
"organization_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"pod_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"inbox_id": "example@agentmail.to",
"thread_id": "9cedb4c5-e13c-4afd-a907-eaf09d53360c",
"labels": ["sent", "received", "unread"],
"timestamp": "2026-08-24T15:06:22Z",
"senders": [
"Quickstart test <example@agentmail.to>",
"You <you@example.com>"
],
"recipients": [
"you@example.com",
"Quickstart test <example@agentmail.to>"
],
"subject": "Quote for 40 units",
"preview": "Sure. 40 units lands at 1,840 dollars with shipping. Quote attached to follow.",
"last_message_id": "<010001a0344eed83-56a55c40-3ed6-44f6-b7c1-ce7f506ab23d-000000@email.amazonses.com>",
"message_count": 2,
"size": 7193,
"messages": [
{
"message_id": "<010001a0344bb1d9-1cece2a2-68c9-451c-a821-417fc8ac42ab-000000@email.amazonses.com>",
"labels": ["sent"],
"timestamp": "2026-08-24T15:02:51Z",
"from": "Quickstart test <example@agentmail.to>",
"to": ["you@example.com"],
"subject": "Quote for 40 units",
"text": "Hi, can you send over a quote for 40 units by Friday?"
},
{
"message_id": "<010001a0344eed83-56a55c40-3ed6-44f6-b7c1-ce7f506ab23d-000000@email.amazonses.com>",
"labels": ["received", "unread"],
"timestamp": "2026-08-24T15:06:22Z",
"from": "You <you@example.com>",
"to": ["Quickstart test <example@agentmail.to>"],
"subject": "Re: Quote for 40 units",
"in_reply_to": "<010001a0344bb1d9-1cece2a2-68c9-451c-a821-417fc8ac42ab-000000@email.amazonses.com>",
"text": "Sure. 40 units lands at 1,840 dollars with shipping. Quote attached to follow.\n\nOn Mon, Aug 24, 2026 at 3:02 PM UTC example@agentmail.to wrote:\n\n> Hi, can you send over a quote for 40 units by Friday?",
"extracted_text": "Sure. 40 units lands at 1,840 dollars with shipping. Quote attached to follow."
}
]
}Each message’s message_id drives the per-message calls: replying, forwarding, downloading its attachments, and the label updates further down this page.
Search threads and messages
List filters match specific fields and keep everything in time order. Search takes free text, looks at who sent it, who received it, the subject, and the full message bodies, and ranks conversations by how well they match. Reach for it when your agent knows what it is looking for but not where it is.
q is the only required input. The full parameter list is below the code.
agentmail inboxes:threads search \
--inbox-id "example@agentmail.to" \
-q "refund"| Param | Type | What it means |
|---|---|---|
q | string | The search text. Matched against senders, recipients, and the subject as whole words and word starts, and against message bodies as full text (refund also finds refunds). |
limit | integer | Results per page, up to 100. |
page_token | string | Where to resume. Pass the next_page_token from the previous page. |
before, after | RFC 3339 timestamp | Bound the results by time. |
How search behaves:
- Results are ranked by relevance, not recency. When only a time window matters, bound it with
beforeandafterand let the ranking sort within it. - Search covers sent and received mail alike, including outbound threads the default list leaves out. Threads carrying
spam,trash,blocked, orunauthenticatedare always excluded. - New mail and label changes take a few seconds to become searchable.
You get the same thread summaries as listing, each with one addition: highlights, showing where the query matched. A key (from, recipients, subject, or text) is present only when that field matched, and matched terms are wrapped in **. That tells your agent which conversation to open when several come back, and gives it the matching line to quote.
{
"count": 1,
"threads": [
{
"organization_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"pod_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"inbox_id": "example@agentmail.to",
"thread_id": "e16bc3e9-1833-48c9-a0e6-2ac6f164e4c2",
"labels": ["sent", "received", "unread"],
"timestamp": "2026-08-24T14:46:20Z",
"received_timestamp": "2026-08-24T14:46:20Z",
"sent_timestamp": "2026-08-24T14:46:11Z",
"senders": [
"Quickstart test <example@agentmail.to>",
"You <you@example.com>"
],
"recipients": [
"you@example.com",
"Quickstart test <example@agentmail.to>"
],
"subject": "Following up",
"preview": "Yes, all five refunds for week 35 are confirmed final.",
"last_message_id": "<010001a0343c94b5-b942bb93-d775-4004-aa42-5c4f2348c567-000000@email.amazonses.com>",
"message_count": 2,
"size": 7071,
"created_at": "2026-08-24T14:46:11Z",
"updated_at": "2026-08-24T14:46:20Z",
"highlights": {
"text": ["Yes, all five **refunds** for week 35 are confirmed final."]
}
}
]
}Note the query matched a thread whose subject says nothing about refunds: the term was inside a message body, and highlights shows the line.
When you need the exact messages rather than the conversations around them, run the same search against an inbox’s messages. The parameters are the same, and the hits are message summaries with the same highlights. Each hit’s message_id and thread_id take you to the message or its conversation.
agentmail inboxes:messages search \
--inbox-id "example@agentmail.to" \
-q "refund"List threads across your whole organization
Drop the inbox_id and the same listing and search run across every inbox at once. That is the view for a supervisor agent watching a fleet, an analytics dashboard, or a router that assigns conversations to agents. How far it reaches comes from your API key:
- an organization key covers every inbox in the organization
- a pod key covers its pod’s inboxes
- an inbox key covers only its own inbox
Pods also have their own paths for keeping tenants apart, covered in Build a multi-tenant platform.
The parameters are the same as the inbox versions.
agentmail threads list
agentmail threads search -q "refund"Every row still carries its inbox_id, which is how a supervisor routes a conversation back to the agent that owns it:
{
"count": 2,
"limit": 2,
"next_page_token": "eyJ0aHJlYWRfaWQiOi...",
"threads": [
{
"organization_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"pod_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"inbox_id": "example@agentmail.to",
"thread_id": "9cedb4c5-e13c-4afd-a907-eaf09d53360c",
"labels": ["sent", "received", "unread"],
"timestamp": "2026-08-24T15:06:22Z",
"received_timestamp": "2026-08-24T15:06:22Z",
"sent_timestamp": "2026-08-24T15:02:51Z",
"senders": [
"Quickstart test <example@agentmail.to>",
"You <you@example.com>"
],
"recipients": [
"you@example.com",
"Quickstart test <example@agentmail.to>"
],
"subject": "Quote for 40 units",
"preview": "Sure. 40 units lands at 1,840 dollars with shipping. Quote attached to follow.",
"last_message_id": "<010001a0344eed83-56a55c40-3ed6-44f6-b7c1-ce7f506ab23d-000000@email.amazonses.com>",
"message_count": 2,
"size": 7193,
"created_at": "2026-08-24T15:02:51Z",
"updated_at": "2026-08-24T15:09:22Z"
},
{
"organization_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"pod_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
"inbox_id": "support-agent27@agentmail.to",
"thread_id": "d4f79544-f9fd-44db-9afd-1e0a2566c1bc",
"labels": ["received", "unread"],
"timestamp": "2026-08-24T15:02:52Z",
"received_timestamp": "2026-08-24T15:02:52Z",
"senders": ["Quickstart test <example@agentmail.to>"],
"recipients": ["support-agent27@agentmail.to"],
"subject": "Quote follow-up",
"preview": "Following up on the quote request from earlier today.",
"last_message_id": "<010001a0344bb56f-c6b2f5a9-d01d-4286-88a5-005db00f4f18-000000@email.amazonses.com>",
"message_count": 1,
"size": 4674,
"created_at": "2026-08-24T15:02:52Z",
"updated_at": "2026-08-24T15:02:52Z"
}
]
}Each inbox holds its own copy of a conversation, so mail between two of your own inboxes shows up once per inbox, with a different thread_id in each. If the results cover less than you expect, check the key: a pod or inbox key cannot see past its scope.
Track conversation state with labels
Labels are plain string tags on messages, and a thread carries the union of every label on its messages. They are how an agent leaves state on a conversation where every other agent, and every list filter, can see it. There are two kinds:
- System labels record what happened to the mail:
received,sent,bounced,complained,delayed,delivered,rejected,opened, andscheduled. AgentMail writes them, and the API rejects edits to them. - Custom labels are any other string, yours to add and remove:
triaged,waiting-reply,needs-human-review, a campaign tag.
You can attach custom labels at send time with the labels field. To change them afterward, update the message. inbox_id and message_id are required, plus add_labels, remove_labels, or both.
agentmail inboxes:messages update \
--inbox-id "example@agentmail.to" \
--message-id "<message_id>" \
--add-labels triaged \
--remove-labels waiting-replyA message_id contains <, >, and @, so URL-encode it when you build the API path by hand. The CLI and SDKs do that for you.
How the edit is applied:
- Values are trimmed and lowercased on write. Each label can be up to 256 characters, and a message can carry up to 256 labels.
- Adding a label the message already has, or removing one it does not, changes nothing, so retrying an update is safe.
- A label in both lists ends up removed.
The response is the message’s full label set after the change, so you can confirm the new state without another read:
{
"message_id": "<010001a0344bb1d9-1cece2a2-68c9-451c-a821-417fc8ac42ab-000000@email.amazonses.com>",
"labels": ["sent", "triaged"]
}The thread reflects the change right away: its labels union updates, and the label starts working as a filter. The agent lists conversations in one state, works on them, and moves them to the next:
agentmail inboxes:threads list \
--inbox-id "example@agentmail.to" \
--label triagedThree things about label filters:
- Pass several labels and a thread must carry all of them, in any spread across its messages.
- Like the default listing, a label filter only shows threads that have received mail, unless
sentis one of the labels you pass. A campaign tag alone will miss unanswered sends. Filter by the tag plussentto cover them. - Label filters are served by search, so a change takes a few seconds to show up in them. Reading the message or thread directly shows it immediately.
When a poll returns a whole page to process, label the messages in one call instead of one at a time: the same change applied to up to 50 message ids.
curl -X POST "https://api.agentmail.to/v0/inboxes/example@agentmail.to/messages/batch-update" \
-H "Authorization: Bearer $AGENTMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message_ids": ["<message_id_1>", "<message_id_2>"],
"add_labels": ["processed"]
}'The batch is atomic: every resolved message changes or none do. An id that does not resolve (deleted, another inbox’s message, or hidden from your key) is skipped silently rather than failing the batch.
{
"updates": [
{
"message_id": "<010001a0344bb1d9-1cece2a2-68c9-451c-a821-417fc8ac42ab-000000@email.amazonses.com>",
"labels": ["sent", "processed"]
},
{
"message_id": "<010001a0344bb56f-c6b2f5a9-d01d-4286-88a5-005db00f4f18-000000@email.amazonses.com>",
"labels": ["sent", "processed"]
}
],
"limit": 2,
"count": 2
}limit is the number of ids you sent and count the number updated, so count below limit means some ids were skipped. Compare updates[].message_id with the ids you sent to find which ones.