# Send and reply (/core/send)

<!-- agent-signals: reading_time_min: 9 · est_tokens: 3593 · updated: 2026-09-06 -->
Related: [Receive email](/core/receive.md), [Manage conversations](/core/conversations.md), [Control who can email your agent](/core/inbound-control.md), [Connect external mail tools: IMAP and SMTP](/core/imap-smtp.md)



# Send email, reply, and schedule delivery

These endpoints send new mail from an inbox, answer or forward existing messages with the thread kept intact, and schedule drafts for later delivery. Use them whenever an agent originates or responds to email.

## Do this

Send a new email. At least one of `to`, `cc`, `bcc` must contain an address. Add an `Idempotency-Key` header so a retry cannot deliver a duplicate:

```bash
curl -X POST "https://api.agentmail.to/v0/inboxes/example@agentmail.to/messages/send" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Idempotency-Key: order-4821-receipt" \
  -H "Content-Type: application/json" \
  -d '{
    "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>"
  }'
```

Keep the returned `message_id` (needed to reply or forward later) and `thread_id` (fetches the conversation).

To answer a message the inbox holds, POST body fields to its reply endpoint. Recipients are derived from the original message:

```bash
curl -X POST "https://api.agentmail.to/v0/inboxes/example@agentmail.to/messages/<url_encoded_message_id>/reply" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Thanks, the tracking number arrived." }'
```

## SDK

```bash
npm install agentmail        # TypeScript
pip install agentmail        # Python
npm install -g agentmail-cli # CLI
```

* Send: TypeScript `client.inboxes.messages.send(inboxId, { to, subject, text, html })`, Python `client.inboxes.messages.send(inbox_id=..., to=[...], subject=..., text=..., html=...)`, CLI `agentmail inboxes:messages send`
* Reply: TypeScript `client.inboxes.messages.reply(inboxId, messageId, { text })`, Python `client.inboxes.messages.reply(inbox_id=..., message_id=..., text=...)`, CLI `agentmail inboxes:messages reply`
* Reply all: TypeScript `client.inboxes.messages.replyAll(inboxId, messageId, { text })`, Python `client.inboxes.messages.reply_all(inbox_id=..., message_id=..., text=...)`, CLI `agentmail inboxes:messages reply-all`
* Forward: TypeScript `client.inboxes.messages.forward(inboxId, messageId, { to, text })`, Python `client.inboxes.messages.forward(inbox_id=..., message_id=..., to=[...], text=...)`, CLI `agentmail inboxes:messages forward`
* Schedule a draft: TypeScript `client.inboxes.drafts.create(inboxId, { to, subject, text, sendAt })`, Python `client.inboxes.drafts.create(inbox_id=..., to=[...], send_at=...)`, CLI `agentmail inboxes:drafts create --send-at`
* Send a draft now: TypeScript `client.inboxes.drafts.send(inboxId, draftId, {})`, Python `client.inboxes.drafts.send(inbox_id=..., draft_id=...)`, CLI `agentmail inboxes:drafts send`
* Idempotency: TypeScript request option `{ idempotencyKey: "..." }`, Python keyword `idempotency_key="..."`
* Display name: TypeScript `client.inboxes.create({ username, displayName })` and `client.inboxes.update(inboxId, { displayName })`, Python `client.inboxes.create(request=CreateInboxRequest(username=..., display_name=...))` and `client.inboxes.update(inbox_id=..., display_name=...)`, CLI `agentmail inboxes create` and `agentmail inboxes update`

Full reference: [/integrations/sdks-and-cli](/integrations/sdks-and-cli).

## Facts

* `POST https://api.agentmail.to/v0/inboxes/{inbox_id}/messages/send` sends immediately. `to`, `cc`, and `bcc` each take one address or a list, bare or `Name <address>`.
* One message reaches up to 50 recipients across `to`, `cc`, and `bcc` combined.
* Body fields are `text` and `html`. Sending both helps deliverability, and clients that cannot render HTML fall back to `text`.
* `reply_to` sets the address or list of addresses replies go to instead of the sending inbox.
* Each `attachments` entry takes `filename` plus exactly one source: `content` (Base64 file bytes) or `url` (a link AgentMail downloads at send time).
* `labels` stores custom labels on the sent message, and AgentMail adds `sent` on top. The sent copy appears in the inbox.
* Mail from a shared `@agentmail.to` address on the Free or Agent plan carries a "Sent via AgentMail" footer. A verified custom domain (any plan) or a paid plan (Developer and above) sends without it.
* `POST https://api.agentmail.to/v0/inboxes/{inbox_id}/messages/{message_id}/reply` with no `to` sends to the original message's `Reply-To` address, or its sender if there is none. Pass `to`, `cc`, or `bcc` to override.
* A reply takes the same body fields as a send. The `text` or `html` goes above the quoted original, and AgentMail adds the quote block and the `Re:` subject (pass `subject` to override). The response is a new `message_id` and the same `thread_id`.
* Replies thread because AgentMail sets the standard `In-Reply-To` and `References` SMTP headers on the outgoing email. Received messages expose those ids as the `in_reply_to` and `references` fields.
* Reply-loop prevention, in order: never respond to your own outbound mail (skip `message.sent` webhook events, or when polling skip messages whose `from` is your inbox address); do not reply when a received message's `headers` map has `Auto-Submitted` with any value other than `no` (that header marks out-of-office notices and other machine-generated mail; `Auto-Submitted: no` explicitly marks human-originated mail and stays safe to answer); cap your own replies per `thread_id` so any loop that slips through is bounded.
* `headers` is a map of custom SMTP headers on immediate sends: new messages, replies, reply-alls, and forwards. Every value must be a string. Drafts do not take it: a `headers` field on draft create is silently ignored, so mail that needs custom headers must go through an immediate send. Sends from shared-domain (`agentmail.to`) inboxes always carry AgentMail's `List-Unsubscribe` headers; custom-domain inboxes pass their own headers through.
* `POST https://api.agentmail.to/v0/inboxes/{inbox_id}/messages/{message_id}/reply-all` derives `to` as the original `To` line plus the sender (or the sender's `Reply-To` address), minus the replying inbox's own address, and keeps the original `Cc` line.
* `POST https://api.agentmail.to/v0/inboxes/{inbox_id}/messages/{message_id}/forward` requires a recipient in `to`, `cc`, or `bcc`. The subject defaults to `Fwd:` plus the original (pass `subject` to override), the original's attachments ride along with any added in the request, and the returned `thread_id` is the source conversation's.
* A `message_id` contains `<`, `>`, and `@`. URL-encode it in reply, reply-all, and forward paths built by hand. The SDKs and CLI encode it automatically.
* `POST https://api.agentmail.to/v0/inboxes/{inbox_id}/drafts` takes the same fields as a send plus `send_at` (ISO 8601). Due drafts are checked every minute, and a `send_at` in the past goes out on the next check.
* A scheduled draft must have a recipient unless it is a reply draft created with `in_reply_to`, which derives recipients from the source message.
* A scheduled draft comes back with `draft_id`, `send_status` of `scheduled`, and the labels `draft` and `scheduled`. Listing drafts filtered by the `scheduled` label shows everything queued in an inbox.
* Reschedule a draft with a new `send_at`, unschedule with `send_at` set to `null` (the draft stays editable), cancel by deleting the draft.
* `send_status` values: `scheduled` (queued), `sending` (going out now), `failed` (set a new `send_at` to retry).
* A draft that sends successfully is deleted, so a draft that can still be fetched has not gone out.
* `POST https://api.agentmail.to/v0/inboxes/{inbox_id}/drafts/{draft_id}/send` delivers a draft immediately, scheduled or not. Optional `add_labels` and `remove_labels` adjust the labels stored on the sent message. Returns `message_id` and `thread_id`.
* The `Idempotency-Key` header works on every send: new messages, replies, reply-alls, forwards, and draft sends. A key is 1 to 256 characters from `A-Z a-z 0-9 - . _ ~`, one key per email.
* A retry with the same key and the same request returns the original `message_id` and `thread_id` and sends nothing, for 24 hours after the send. After 24 hours the key is free to reuse.
* When two simultaneous requests share a key, one sends and the other gets a `409`. Retrying the `409` request returns the replayed result.
* The sender name recipients see is the inbox's `display_name`, up to 256 characters. Rejected characters: `( ) < > @ , ; : \ " [ ]` and control characters.
* Set `display_name` on `POST https://api.agentmail.to/v0/inboxes` (`username` and `display_name` both optional, AgentMail generates an address when `username` is omitted) or on `PATCH https://api.agentmail.to/v0/inboxes/{inbox_id}` (must include `display_name`, `metadata`, or both).
* A new `display_name` applies to messages sent after the change. Delivered mail keeps the name it went out with, and the inbox's `email` address stays the same.

## Not supported

* There is no `body` field. A `body` field is dropped silently and the email goes out with an empty body. Use `text` and `html`.
* Reply-all does not take recipient overrides. A reply-all request with `to`, `cc`, or `bcc` sends to the derived list anyway. To pick recipients, use the reply endpoint with `to`.
* An inbox cannot mail its own address.
* A forward does not start a new thread. It stays attached to the conversation it was forwarded from.
* A sent draft cannot be fetched or reused. The draft is deleted when it sends, and the `draft_id` no longer resolves.
* A draft with `send_status` of `sending` can no longer be edited or canceled.
* Sends without an `Idempotency-Key` are not deduplicated.
* An empty `Idempotency-Key` value returns a `400` instead of sending without protection.
* A `display_name` does not override a recipient's saved contact. Some clients show the saved contact name instead.
* There is no header-suppression mechanism in the `headers` map, for your own custom headers or for headers AgentMail sets: a `null` value returns a `400 ValidationError`. To leave a custom header off a send, omit it from the map. `List-Unsubscribe` headers on shared-domain (`agentmail.to`) sends cannot be overridden or suppressed.

## Errors

| Error                                                                                        | HTTP | Cause                                                                                                                              | Fix                                                                    |
| -------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `to, cc, or bcc must be specified`                                                           | 400  | A send or forward reached AgentMail with no recipients, often an empty list or a misspelled field name.                            | Put at least one address in `to`, `cc`, or `bcc`.                      |
| `ValidationError`                                                                            | 400  | A `display_name` containing a rejected character, or an inbox update with an empty body (`Must provide display_name or metadata`). | Correct the fields named in the `errors` array.                        |
| `A scheduled draft (send_at) must specify to, cc, or bcc unless it is a reply (in_reply_to)` | 400  | A draft was scheduled with no recipients.                                                                                          | Give the draft a recipient before setting `send_at`.                   |
| `resource_taken`                                                                             | 403  | The inbox `username` belongs to another organization.                                                                              | Pick one of the up to 3 available variants in `suggestions` and retry. |
| `already_exists`                                                                             | 403  | The organization already owns that `username`.                                                                                     | Use the existing inbox or pick a variant from `suggestions`.           |
| `limit_exceeded`                                                                             | 403  | The plan's inbox cap is reached. This check runs before the collision check, so a duplicate `username` can surface as this error.  | Resolve the cap, then retry.                                           |
| `Message not found`                                                                          | 404  | No message with that id is visible to the credential, most often the `message_id` belongs to a different inbox.                    | Reply from the inbox that holds the message.                           |
| `Draft not found`                                                                            | 404  | The draft already sent, by its schedule or an earlier call, and was deleted with the send.                                         | Use the `message_id` and `thread_id` the send returned.                |
| `IdempotencyKeyConflictError`                                                                | 409  | The same `Idempotency-Key` was used for a different request, or a simultaneous duplicate is still in flight.                       | Retry the identical request, or generate a new key for a new message.  |

## Verify

A `200` response with `message_id` and `thread_id` confirms the send went out. To see the stored copy:

```bash
curl -s "https://api.agentmail.to/v0/inboxes/example@agentmail.to/messages?labels=sent&limit=1" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"
```

The newest entry carries the `sent` label plus any labels passed with the send.

## Related

* [/core/conversations](/core/conversations) works with the threads sends create.
* [/advanced/custom-domains](/advanced/custom-domains) sends from a custom domain instead of `@agentmail.to`.
* [/core/receive](/core/receive) lists the mail whose `message_id` a reply needs.
* [/advanced/errors](/advanced/errors) covers error codes and safe retries in full.
