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."{
"message_id": "<message_id>",
"thread_id": "<thread_id>"
}| Field | Type | Notes |
|---|---|---|
to, cc, bcc | address array | At least one must be non-empty. |
subject | string | Optional. |
text, html | string | There is no body field. Provide both when you can so the message renders on every client. |
reply_to | address array | Sets Reply-To. |
attachments | array | See Attachments. |
headers | map | Custom SMTP headers with string values. null returns 400; omit a key to leave it out. |
labels | string array | Labels 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
| Action | Endpoint |
|---|---|
| Reply | POST /inboxes/{inbox_id}/messages/{message_id}/reply |
| Reply all | POST /inboxes/{inbox_id}/messages/{message_id}/reply-all |
| Forward | POST /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 sourceReply-To, otherwise the sender. - Reply-all derives original To plus sender, excludes your inbox address, and retains original Cc. Use
/reply-allorreply_all: trueon/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.
| Action | Endpoint |
|---|---|
| Create | POST /inboxes/{inbox_id}/drafts |
| Update | PATCH /inboxes/{inbox_id}/drafts/{draft_id} |
| Send | POST /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"{
"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": nullreturns 400, so you cannot unschedule and keep the draft. A draft alreadysendingcannot 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 aJsonErrorbefore any request is made. - Retry a
faileddraft: set a newsend_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.