Skip to content
AgentMail
AgentMail
Core tasks

Send and reply

Send new emails, reply and forward with automatic threading, schedule drafts, and make retries safe with idempotency keys.

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

Send an email

A send goes out immediately from the inbox you choose. You address it with to (or cc or bcc), and at least one of those three must contain an address. Everything else is optional.

agentmail inboxes:messages send \
  --inbox-id "example@agentmail.to" \
  --to "you@example.com" \
  --subject "Your receipt" \
  --text "Thanks for your order. Receipt no. 4821." \
  --html "<p>Thanks for your order. Receipt no. 4821.</p>"
ParamTypeWhat it means
to, cc, bccstring or string[]Recipients. Each takes one address or a list, as a bare address or Name <address>. At least one of the three must contain an address.
subjectstringThe subject line.
textstringThe plain-text body.
htmlstringThe HTML body. Send both text and html when you can: clients that cannot render HTML fall back to the text version, and having both helps deliverability.
reply_tostring or string[]Address or list of addresses that replies should go to instead of the sending inbox.
attachmentsobject[]Files to attach. Each entry takes a filename plus exactly one source: content (the Base64-encoded file bytes) or url (a link AgentMail downloads at send time).
labelsstring[]Your own labels to store on the sent message, so you can find or filter it later. AgentMail adds sent on top. In the CLI, pass them as a JSON array: --label '["orders"]'.
headersobjectCustom SMTP headers to set on the outgoing email, as a map of header name to string value. Immediate sends only (send, reply, reply-all, forward); drafts silently ignore it.

One message can reach up to 50 recipients across to, cc, and bcc combined.

Every value in the headers map must be a string. The only way to leave a custom header off a send is to omit it from the map. Sends from shared-domain inboxes (addresses on agentmail.to) always carry AgentMail’s List-Unsubscribe headers, and the headers map cannot override or suppress them. To control unsubscribe headers yourself, send from a verified custom domain, which passes your own headers through.

The send returns the new message’s message_id and the thread_id of the conversation it starts.

  • message_id is what you reply to or forward later
  • thread_id fetches the whole conversation when you manage conversations.

The sent copy appears in your inbox with the sent label plus any labels you passed.

Mail sent from a shared @agentmail.to address carries a “Sent via AgentMail” footer while your organization is on the Free plan or the Agent plan (the plan an agent lands on when it signs itself up). To send without it:

  • send from an inbox on a verified custom domain, on any plan
  • or move to a paid plan (Developer and above), which sends unbranded even from @agentmail.to

Set the name recipients see

The name next to your address on an email app such as Gmail or Apple Mail is the inbox’s display_name.

Set it when you create the inbox

Both create fields are optional: username picks the local part of the address (AgentMail generates one when you omit it), and display_name sets the name.

agentmail inboxes create \
  --username "updates" \
  --display-name "Acme Updates"

A display name can be up to 256 characters. Characters that have meaning in an email header are rejected: ( ) < > @ , ; : \ " [ ] and control characters.

Rename an existing inbox

You can update the display_name any time but the new name applies to messages sent after the change, mail already delivered keeps the name it went out with.

agentmail inboxes update \
  --inbox-id "example@agentmail.to" \
  --display-name "Acme Updates"

Reply to a message

To reply, you need the message_id of the message you are answering. You get it when you receive email or list messages. Recipients are optional on a reply because AgentMail derives them from the original message:

  • With no to, the reply goes to the original message’s Reply-To address, or to its sender if there is none.
  • Pass to, cc, or bcc to override that and address the reply yourself.
agentmail inboxes:messages reply \
  --inbox-id "example@agentmail.to" \
  --message-id "<message_id>" \
  --text "Thanks, the tracking number arrived."

A reply takes the same body fields as a send. Your text or html goes above the quoted original, and AgentMail adds the quote block and the Re: subject for you (pass subject to override it). When you call the API directly, URL-encode the message_id in the path. It contains <, >, and @, which are not valid URL characters, so an unencoded id makes the request fail. The SDKs and CLI encode it for you.

The reply returns a new message_id and the same thread_id as the message it answered, which is how the conversation stays grouped:

Sample response
{
  "message_id": "<010001a034382b3e-6c572037-a296-4430-a35a-e075e4f2476d-000000@email.amazonses.com>",
  "thread_id": "7f4d9495-cb6c-4554-b209-f551ad48b50c"
}

The reply lands in the same conversation because AgentMail sets the standard threading headers (In-Reply-To and References) on the outgoing email for you. On received messages, those same ids are exposed as the in_reply_to and references fields.

How to not get trapped in a loop

If you’re building an auto-reply agent, you need to design for the case where it interacts with an out-of-office auto-responder or another bot.

  1. Never respond to your own outbound mail. If you consume webhook events, skip message.sent events entirely; they fire for your agent’s own sends. If you poll, skip messages whose from is your inbox address.
  2. Skip auto-generated mail. Received messages include a headers map. Do not reply when Auto-Submitted is present with any value other than no; that header marks out-of-office notices, delivery reports, and other machine-generated mail. (Auto-Submitted: no explicitly marks a human-originated message, so it stays safe to answer.)
  3. Cap replies per thread. Before replying, count your inbox’s prior messages in the thread and stop past a fixed limit. This bounds the damage of any loop the first two checks miss.

Reply all

A reply-all answers everyone at once: the sender plus every to and cc recipient of the message you are answering. You do not pass recipients. AgentMail derives the full list from the original message:

  • to becomes the original To line plus the sender (or the sender’s Reply-To address if it set one), minus your own inbox address.
  • cc keeps the original Cc line.
agentmail inboxes:messages reply-all \
  --inbox-id "example@agentmail.to" \
  --message-id "<message_id>" \
  --text "Looping everyone in on the update."

A reply-all request that includes to, cc, or bcc sends to the derived list anyway. To pick the recipients yourself, send a reply with to instead.

The response has the same shape as a reply: a new message_id and the thread’s thread_id.

Forward a message

A forward sends a message your inbox holds onward to someone new, with the original’s attachments riding along. It needs a recipient: to, cc, or bcc must contain an address.

agentmail inboxes:messages forward \
  --inbox-id "example@agentmail.to" \
  --message-id "<message_id>" \
  --to "teammate@example.com" \
  --text "Forwarding the shipping notice for your records."

Your note appears above a “Forwarded message” block that quotes the original with its sender, date, and subject. The subject defaults to Fwd: plus the original subject (pass subject to override), and any attachments you add in the request are sent alongside the original’s.

The returned thread_id is the source conversation’s, so the forward stays attached to the thread you forwarded from:

Sample response
{
  "message_id": "<010001a0343a7698-e9b5cd4d-18cb-4b42-93ac-dbd35c573fa4-000000@email.amazonses.com>",
  "thread_id": "eb8d00e0-3a49-4fe9-9ce8-ed68a6e7df9d"
}

Schedule an email with a draft

A draft is a saved, unsent message. It takes the same fields as a send, plus send_at as an ISO 8601 datetime. AgentMail delivers it at that time, with no cron job or polling on your side. A scheduled draft must already have a recipient (unless it is a reply draft created with in_reply_to, which derives its recipients from the source message).

agentmail inboxes:drafts create \
  --inbox-id "example@agentmail.to" \
  --to "you@example.com" \
  --subject "Following up" \
  --text "Checking in ahead of our call tomorrow." \
  --send-at "2026-08-26T09:00:00Z"

Due drafts are checked every minute, so delivery starts within a minute of send_at. A send_at in the past is accepted and goes out on the next check.

Sample response
{
  "organization_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
  "pod_id": "1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d",
  "inbox_id": "example@agentmail.to",
  "draft_id": "5e971da0-d608-4a72-90ad-0af2fed7740b",
  "subject": "Following up",
  "to": ["you@example.com"],
  "text": "Checking in ahead of our call tomorrow.",
  "send_at": "2026-08-26T09:00:00Z",
  "preview": "Checking in ahead of our call tomorrow.",
  "send_status": "scheduled",
  "labels": ["draft", "scheduled"],
  "updated_at": "2026-08-24T14:44:52.387Z",
  "created_at": "2026-08-24T14:44:52.387Z"
}

You can change your mind any time before the draft fires:

  • To reschedule, update the draft with a new send_at.
  • To unschedule but keep the draft, update it with send_at set to null. It loses the scheduled label and stays editable as a plain draft.
  • To cancel entirely, delete the draft.

send_status tells you where a scheduled draft stands:

  • scheduled means it is queued for its send_at time.
  • sending means it is going out right now, so it can no longer be edited or canceled.
  • failed means the send attempt failed. Set a new send_at to retry it.

The full lifecycle:

A draft that sends successfully is deleted, so a draft you can still fetch has not gone out.

Send a draft now

Sending a draft delivers it immediately, whether it is scheduled or a plain unscheduled draft. This is also the approval step when a human reviews drafts an agent wrote: the agent creates the draft, and your review flow sends it.

agentmail inboxes:drafts send \
  --inbox-id "example@agentmail.to" \
  --draft-id "<draft_id>"

You can pass add_labels and remove_labels in the request to adjust the labels stored on the sent message.

The send returns the new message’s message_id and thread_id, and the draft itself is deleted. Keep those ids, because the draft_id no longer resolves:

Sample response
{
  "message_id": "<010001a0343c70da-4545632f-1ee8-4ba0-a62d-e6c095079f47-000000@email.amazonses.com>",
  "thread_id": "e16bc3e9-1833-48c9-a0e6-2ac6f164e4c2"
}

Make sends idempotent

Retries are unavoidable, and a retried send without protection delivers the same email twice. Pass an Idempotency-Key header (idempotencyKey in TypeScript, idempotency_key in Python) on any send your code might retry and AgentMail sends exactly once, replaying the original message_id and thread_id on retries. Key rules, the replay window, and the 409/400 failure modes are on Make sends safe to retry.

Next Steps

Was this page helpful?Suggest editsRaise issue