Skip to content
AgentMail
AgentMail
Core tasks

Manage conversations

List and read whole email threads, search them, watch every inbox in your organization, and track conversation state with labels.

If you don’t have an API key yet, follow the Quickstart first.

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"
ParamTypeWhat it means
limitintegerThreads per page.
page_tokenstringWhere to resume. Pass the next_page_token from the previous page.
labelsstring[]Only threads that carry every label you list. See Track conversation state with labels.
before, afterRFC 3339 timestampBound the results by time, like 2026-08-24T00:00:00Z.
ascendingbooleanOldest first instead of newest first.
senders, recipients, subjectstring[]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_trashbooleanAlso 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.

Sample response
{
  "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:

  • labels is the union of every label on the thread’s messages. received and sent together mean the conversation has traffic in both directions.
  • senders and recipients accumulate everyone who has appeared, as a bare address or Display Name <address> depending on what the mail carried.
  • preview is the beginning of the newest message, and message_count and size tell you how much conversation there is.
  • timestamp is the newest activity, with received_timestamp and sent_timestamp for 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:

  • text and html are the body as it was sent, including the quoted copy of earlier messages that most mail clients append below a reply.
  • extracted_text and extracted_html are 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.

Sample response
{
  "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"
ParamTypeWhat it means
qstringThe 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).
limitintegerResults per page, up to 100.
page_tokenstringWhere to resume. Pass the next_page_token from the previous page.
before, afterRFC 3339 timestampBound the results by time.

How search behaves:

  • Results are ranked by relevance, not recency. When only a time window matters, bound it with before and after and 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, or unauthenticated are 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.

Sample response
{
  "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:

Sample response
{
  "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, and scheduled. 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-reply

A 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:

Sample response
{
  "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 triaged

Three 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 sent is one of the labels you pass. A campaign tag alone will miss unanswered sends. Filter by the tag plus sent to 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.

Sample response
{
  "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.

Next Steps

Was this page helpful?Suggest editsRaise issue