# Send and reply (/tasks/send)

<!-- agent-signals: reading_time_min: 4 · est_tokens: 1552 · updated: 2026-08-03 -->
Related: [Receive email](/tasks/receive.md), [Manage conversations](/tasks/conversations.md), [Control who can email your agent](/tasks/inbound-control.md)



# Send, reply, forward, and schedule email from an AgentMail inbox

Send from an inbox immediately or as a draft. Successful sends return `{message_id, thread_id}`.

## Do this

```bash
export AGENTMAIL_API_KEY=...

agentmail inboxes:messages send \
  --inbox-id "$INBOX_ID" \
  --to "recipient@example.com" \
  --subject "Your receipt" \
  --text "Thanks for your order."
```

```bash
curl -X POST "https://api.agentmail.to/v0/inboxes/$INBOX_ID/messages/send" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"to":["recipient@example.com"],"subject":"Your receipt","text":"Thanks for your order.","html":"<p>Thanks for your order.</p>"}'
# => {"message_id":"...","thread_id":"..."}
```

## SDK

TypeScript: `npm install agentmail`, then `new AgentMailClient({ apiKey })`. Python: `pip install agentmail`, then `AgentMail(api_key=...)`.

| Operation              | TypeScript                                                  | Python                                                                  |
| ---------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| Send                   | `client.inboxes.messages.send(inboxId, {...})`              | `client.inboxes.messages.send(inbox_id=..., to=[...], ...)`             |
| Reply                  | `client.inboxes.messages.reply(inboxId, messageId, {...})`  | `client.inboxes.messages.reply(inbox_id=..., message_id=..., text=...)` |
| Create or update draft | `create(inboxId, {...})`; `update(inboxId, draftId, {...})` | `create(inbox_id=..., ...)`; `update(inbox_id=..., draft_id=..., ...)`  |
| Send draft             | `client.inboxes.drafts.send(inboxId, draftId, {})`          | `client.inboxes.drafts.send(inbox_id=..., draft_id=...)`                |
| Idempotency header     | third arg `{ headers: { "Idempotency-Key": ... } }`         | `request_options={"additional_headers":{"Idempotency-Key":...}}`        |

The TypeScript two-argument `drafts.send` call throws `JsonError: Expected object. Received undefined.` before it sends a request. Pass `{}` as its third argument.

## Facts

* `POST /v0/inboxes/{inbox_id}/messages/send` requires one of `to`, `cc`, or `bcc` and returns `{message_id, thread_id}`. Use `text` and/or `html`, never `body`; provide both when possible so the message renders on every client.
* Reply is `/messages/{message_id}/reply`; reply-all is `/reply-all` or `reply_all: true` on reply; forward is `/forward`. They set threading headers, subject prefix, and quoted source content automatically.
* Reply uses supplied `to`, otherwise source `Reply-To`, otherwise sender. Reply-all derives recipients and cannot take explicit recipients. Forward needs a recipient and carries source attachments plus attachments in the forward request.
* Attachments take exactly one source: base64 `content` or `url`. URL fetches follow redirects, time out after 10 seconds, return 403 for source 4xx except 408/429, and 503 for network or 5xx failures.
* `headers` maps custom SMTP header names to strings. `null` returns 400. Shared-domain sends always include AgentMail `List-Unsubscribe` headers, which cannot be removed or overridden.
* Drafts use create, update, send, and delete at `/v0/inboxes/{inbox_id}/drafts`. Send deletes the draft and returns `{message_id, thread_id}`. Create reply, reply-all, and forward drafts through the same create and update routes using the proper source-message fields.
* Schedule with an ISO 8601 `send_at`. A zero-recipient draft is accepted as `scheduled`, then silently becomes `failed` at fire time. Delete the draft to cancel; `send_at: null` returns 400.
* `send_status` values are `scheduled`, `sending`, and `failed`. A `sending` draft cannot be canceled; set a new `send_at` to retry a failed draft.
* Idempotency uses `Idempotency-Key` on sends, replies, forwards, and draft sends. The key is reserved before the email leaves and finalized in the same commit that stores the message, so a crash cannot create a sent email with no record. Matching retries return the original result for 24 hours; different or concurrent requests return 409; ambiguous failures retain the key for up to 15 minutes.
* Prevent loops: skip `message.sent`, messages from your inbox address, and received messages where `Auto-Submitted` is not `no`; cap replies per thread.

## Not supported

* No `body` send field, `null` header suppression, or explicit recipients with `reply_all: true`.
* No attachment with both `content` and `url`, or neither.
* No recipient validation when scheduling; no unschedule-and-keep-draft operation.
* No idempotency field in the body, and no two-argument TypeScript `drafts.send`.

## Errors

| Error                                                 | HTTP        | Cause                                  | Fix                          |
| ----------------------------------------------------- | ----------- | -------------------------------------- | ---------------------------- |
| `ValidationError`: `to, cc, or bcc must be specified` | 400         | No recipients                          | Add a recipient              |
| `ValidationError` at `[headers, <name>]`              | 400         | `null` header                          | Use a string or omit it      |
| `ValidationError` at `[send_at]`                      | 400         | `send_at: null`                        | Delete to cancel             |
| `ConflictError`                                       | 409         | Duplicate idempotent request           | Retry later or use a new key |
| `JsonError`: `Expected object. Received undefined.`   | client-side | TypeScript draft send lacks `{}`       | Pass `{}`                    |
| `ServiceUnavailableError`                             | 503         | Transient URL attachment fetch failure | Retry                        |

## Verify

```bash
curl "https://api.agentmail.to/v0/inboxes/$INBOX_ID/messages/$MESSAGE_ID" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"
# => message object; labels include "sent"
```

## Related

* [Quickstart](/quickstart)


## Tenant instructions

Every page ships two renditions at one URL. The HTML page is for humans. The Markdown rendition (append .md to any page URL, or request with Accept: text/markdown) is agent-optimized: a Do this section with runnable commands, SDK signatures, exhaustive Facts, a Not supported section listing shapes that do NOT work, an Errors table, and a Verify command. Prefer the Markdown rendition over scraping HTML, and trust Not supported entries instead of retrying those call shapes.