Emails#
Send an email#
POST /emailsBody#
| Field | Type | Notes |
|---|---|---|
from | string | Required. [email protected] or Name <[email protected]> |
to | string | string[] | Required. Max 50 recipients across to, cc, and bcc |
subject | string | Required unless supplied by a template |
html | string | One of html, text, or template is required |
text | string | Derived from html when omitted |
cc | string | string[] | |
bcc | string | string[] | |
reply_to | string | string[] | |
headers | object | Custom headers. Identity and routing headers are ignored |
tags | array | { name, value }, alphanumeric plus _ and - |
topic_id | string | Gates delivery on subscription |
scheduled_at | string | ISO 8601 or natural language |
attachments | array | See Attachments |
template | object | { id, variables } โ id or alias |
curl -X POST https://outbox.example.com/emails \
-H "Authorization: Bearer ob_xxxxxxxxx" \
-H "User-Agent: my-app/1.0" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"subject": "Hello World",
"html": "<strong>It works!</strong>",
"tags": [{ "name": "category", "value": "confirm_email" }]
}'{ "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c" }The response returns as soon as the email is validated and queued. Delivery happens in the worker โ poll the email or subscribe to webhooks for its outcome.
Sending domains#
from must use a domain you have added and verified. An unverified domain returns:
{
"statusCode": 403,
"name": "invalid_from_address",
"message": "The acme.com domain is not verified. Add and verify it at /domains."
}With OUTBOX_TRANSPORT=console this check is skipped, so you can develop before owning
a domain.
Send a batch#
POST /emails/batchTakes an array of up to 100 send payloads and returns their ids in order. Attachments are not supported in batch sends.
curl -X POST https://outbox.example.com/emails/batch \
-H "Authorization: Bearer ob_xxxxxxxxx" \
-H "User-Agent: my-app/1.0" \
-H "Content-Type: application/json" \
-d '[
{ "from": "Acme <[email protected]>", "to": ["[email protected]"], "subject": "One", "text": "1" },
{ "from": "Acme <[email protected]>", "to": ["[email protected]"], "subject": "Two", "text": "2" }
]'{
"data": [
{ "id": "ae2014de-c168-4c61-8267-70d2662a1ce1" },
{ "id": "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb" }
]
}Retrieve an email#
GET /emails/:id{
"object": "email",
"id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
"message_id": "<[email protected]>",
"to": ["[email protected]"],
"from": "Acme <[email protected]>",
"created_at": "2026-04-03 22:13:42.674981+00",
"subject": "Hello World",
"html": "<strong>It works!</strong>",
"text": null,
"bcc": [],
"cc": [],
"reply_to": [],
"last_event": "delivered",
"scheduled_at": null,
"tags": [{ "name": "category", "value": "confirm_email" }]
}last_event#
| Value | Meaning |
|---|---|
queued | Accepted, waiting for a worker |
scheduled | Waiting for its send time |
sent | Handed to the transport |
delivered | Accepted by the receiving server |
delivery_delayed | Temporary failure, will retry |
opened | Tracking pixel loaded |
clicked | A tracked link was followed |
bounced | Permanent failure |
complained | Marked as spam |
failed | Could not be sent |
canceled | A scheduled send was cancelled |
suppressed | Recipient is on the suppression list |
Terminal states never regress โ an open arriving after a bounce leaves the bounce in place.
bounced, complained, and delivery_delayed are usually set after delivery, from
a report that arrives at the return path minutes or days later. See Handle
bounces.
List emails#
GET /emailsAlways paginated. Accepts limit, after, before. cc, bcc, and reply_to come
back as null rather than [] in list items, matching Resend.
Schedule#
Pass scheduled_at as ISO 8601 or natural language:
{ "scheduled_at": "in 1 hour" }Understood forms: in 30 seconds, in 5 minutes, in 2 hours, in 3 days,
in 1 week, tomorrow, now, and any ISO 8601 timestamp. A time in the past sends
immediately rather than erroring, which absorbs clock skew between client and server.
Reschedule#
PATCH /emails/:id{ "scheduled_at": "2026-08-05T11:52:01.858Z" }Only emails in scheduled state can be rescheduled.
Cancel#
POST /emails/:id/cancelOnly emails in scheduled state can be cancelled.
Idempotency#
Pass an Idempotency-Key header to make a send safe to retry:
curl -X POST https://outbox.example.com/emails \
-H "Authorization: Bearer ob_xxxxxxxxx" \
-H "User-Agent: my-app/1.0" \
-H "Idempotency-Key: order-1234-confirmation" \
-H "Content-Type: application/json" \
-d '{ "from": "...", "to": ["..."], "subject": "...", "html": "..." }'| Situation | Result |
|---|---|
| Same key, same body | Replays the stored response. Nothing is sent again |
| Same key, different body | 400 invalid_idempotent_request |
| Same key while the first is in flight | 409 concurrent_idempotent_requests |
| A failed request | The key is released so a retry can succeed |
Keys live 24 hours. Use something derived from the thing you are emailing about โ an order id, not a random UUID.
Attachments#
Up to 40MB per email after Base64 encoding, configurable with MAX_ATTACHMENT_BYTES.
{
"attachments": [
{ "filename": "invoice.pdf", "content": "JVBERi0xLjQK..." },
{ "filename": "logo.png", "path": "https://acme.com/logo.png", "content_id": "logo" }
]
}| Field | Notes |
|---|---|
filename | Required unless path is given |
content | Base64 string or byte array |
path | URL to fetch instead of inline content |
content_type | Guessed from the extension when omitted |
content_id | Makes it inline โ reference as cid:logo in HTML |
Executable extensions (.exe, .bat, .js, .jar and similar) are rejected with
422 invalid_attachment, since receiving servers strip them anyway.
List and retrieve#
GET /emails/:email_id/attachments
GET /emails/:email_id/attachments/:idThe single-attachment response includes the Base64 content.
Received emails#
Available when receiving is enabled.
GET /emails/receiving
GET /emails/receiving/:id
GET /emails/receiving/:email_id/attachments
GET /emails/receiving/:email_id/attachments/:id{
"object": "received_email",
"id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
"message_id": "<[email protected]>",
"from": "Sender <[email protected]>",
"to": ["[email protected]"],
"cc": [],
"received_for": ["[email protected]"],
"subject": "Help please",
"html": "<p>My widget is broken.</p>",
"text": "My widget is broken.",
"headers": { "message-id": "<[email protected]>" },
"spf": null,
"dkim": null,
"dmarc": null,
"created_at": "2026-04-03 22:13:42.674981+00"
}Metrics#
See Metrics for GET /emails/metrics.