Skip to content
AgentMail
AgentMail
Core tasks

Send and reply

Send messages, reply or forward with threading, schedule drafts, and make sends idempotent.

Send from an inbox, reply in a thread, or create a draft for later delivery. Every successful send returns message_id and thread_id.

Send a new message

Send immediately from an inbox. inbox_id is the inbox email address, and at least one of to, cc, or bcc must be non-empty.

agentmail inboxes:messages send \
  --inbox-id "agent@yourdomain.com" \
  --to "customer@example.com" \
  --subject "Your receipt" \
  --text "Thanks for your order."
Sample response
{
  "message_id": "<message_id>",
  "thread_id": "<thread_id>"
}
FieldTypeNotes
to, cc, bccaddress arrayAt least one must be non-empty.
subjectstringOptional.
text, htmlstringThere is no body field. Provide both when you can so the message renders on every client.
reply_toaddress arraySets Reply-To.
attachmentsarraySee Attachments.
headersmapCustom SMTP headers with string values. null returns 400; omit a key to leave it out.
labelsstring arrayLabels stored on the sent message. AgentMail adds sent.

Shared-domain (agentmail.to) sends always include AgentMail List-Unsubscribe headers. The headers map cannot override or remove them.

Attach files

Attach a file by giving it exactly one source: base64 content, or a url AgentMail fetches at send time. Passing both, or neither, returns 400.

{"attachments":[
  {"filename":"receipt.pdf","content":"<base64 bytes>"},
  {"filename":"logo.png","url":"https://example.com/logo.png","content_id":"logo"}
]}

content_type is inferred from filename or the URL response. content_disposition defaults to inline when a content_id is set, otherwise attachment. URL fetches follow redirects and time out after 10 seconds; a source 4xx becomes 403 (except 408 and 429), and a network error or 5xx becomes 503.

Reply, reply-all, or forward

ActionEndpoint
ReplyPOST /inboxes/{inbox_id}/messages/{message_id}/reply
Reply allPOST /inboxes/{inbox_id}/messages/{message_id}/reply-all
ForwardPOST /inboxes/{inbox_id}/messages/{message_id}/forward

They accept the send body and set In-Reply-To, References, Re: or Fwd:, and quoted source content automatically.

  • Reply uses supplied to, otherwise source Reply-To, otherwise the sender.
  • Reply-all derives original To plus sender, excludes your inbox address, and retains original Cc. Use /reply-all or reply_all: true on /reply; neither accepts explicit recipients.
  • Forward needs a recipient and carries the source attachments plus any attachments in the forward request.
agentmail inboxes:messages reply \
  --inbox-id "agent@yourdomain.com" \
  --message-id "<message-id@example.com>" \
  --text "Confirmed. Shipping today."

Create, schedule, and send drafts

A draft is an unsent message. Create one, update it as many times as you need, then send it — or set send_at to an ISO 8601 datetime and AgentMail sends it at that time. Reply, reply-all, and forward drafts use the same routes: set the source-message fields when you create the draft.

ActionEndpoint
CreatePOST /inboxes/{inbox_id}/drafts
UpdatePATCH /inboxes/{inbox_id}/drafts/{draft_id}
SendPOST /inboxes/{inbox_id}/drafts/{draft_id}/send
agentmail inboxes:drafts create \
  --inbox-id "agent@yourdomain.com" \
  --to "prospect@example.com" \
  --subject "Following up" \
  --text "Checking in." \
  --send-at "2026-07-13T09:00:00Z"
Sample response
{
  "inbox_id": "agent@yourdomain.com",
  "draft_id": "<draft_id>",
  "labels": ["drafts", "scheduled"],
  "to": ["prospect@example.com"],
  "subject": "Following up",
  "text": "Checking in.",
  "send_status": "scheduled",
  "send_at": "2026-07-13T09:00:00Z"
}

send_status is scheduled, sending, or failed. Sending deletes the draft and returns message_id and thread_id — there is no sent status because a sent draft no longer exists.

  • Reschedule: update the draft with a new send_at.
  • Cancel: delete the draft. Updating with "send_at": null returns 400, so you cannot unschedule and keep the draft. A draft already sending cannot be canceled.
  • Send now: call the send endpoint. In TypeScript pass an empty options object — client.inboxes.drafts.send(inboxId, draftId, {}) — or the client throws a JsonError before any request is made.
  • Retry a failed draft: set a new send_at.

A draft with no recipients is accepted as scheduled, then silently becomes failed at fire time. Set recipients before the scheduled time and monitor send_status.

Prevent reply loops

Skip message.sent events and messages whose from is your inbox address. Do not reply when received headers.Auto-Submitted is present and not no. Limit replies per thread.

Make sends idempotent

A retry after a timeout can send the same email twice. To make retries safe, set an Idempotency-Key header on any send: new messages, replies, forwards, and draft sends. Requests without the header are not deduplicated.

curl -X POST "https://api.agentmail.to/v0/inboxes/agent@yourdomain.com/messages/send" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Idempotency-Key: order-4821-receipt" \
  -H "Content-Type: application/json" \
  -d '{"to":["customer@example.com"],"subject":"Your receipt","text":"Thanks for your order."}'

Retrying the same request with the same key returns the original result for 24 hours instead of sending again. A different request reusing the key, or a concurrent duplicate, returns 409; after an ambiguous failure the key stays reserved for up to 15 minutes. Keys are organization-scoped, 1–256 characters from A-Z a-z 0-9 - . _ ~.

The key is reserved before the email leaves and finalized in the same commit that stores the message, so a crash cannot produce a sent email with no record.

Was this page helpful?Suggest editsRaise issue