# Batch sending

Up to 100 emails in one request, each with its own recipient, body, and result.

POST /v1/emails/batch takes an array of the exact bodies POST /v1/emails takes, up to 100 of them. Each entry is its own email to its own recipient: nobody sees anybody else's address, and one bad entry does not stop the rest.

- `POST /v1/emails/batch` Send up to 100 in one request.

Two emails, one request:
```bash
curl -X POST https://emails.sh/v1/emails/batch \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": [
      {
        "from": "Acme <hello@acme.com>",
        "to": ["ada@example.com"],
        "subject": "Your invite to Acme",
        "html": "<p>Hello Ada, here is your invite.</p>"
      },
      {
        "from": "Acme <hello@acme.com>",
        "to": ["grace@example.com"],
        "subject": "Your invite to Acme",
        "html": "<p>Hello Grace, here is your invite.</p>"
      }
    ]
  }'
```

207 Multi-Status:
```json
{
  "data": [
    { "id": "em_01J9X8Q2K7Y4RN3M", "status": "queued" },
    { "id": null, "status": "failed", "error": { "code": "recipient_suppressed", "message": "grace@example.com hard bounced on 2026-07-02 and is suppressed on this workspace", "next": "Remove it at https://emails.sh/dashboard/suppressions if you know it is good." } }
  ]
}
```

The array comes back in the order you sent it, one entry per input, so index 3 in the response is index 3 in the request. A refused entry has id: null and an error object saying why. Walk it and record the ids; an entry with an error never sent and will not retry itself.

#### `emails.batch`

`{ emails: Send[] }`

Send up to 100 emails in one request. Each entry succeeds or fails on its own, and the response keeps the order you sent them in.

| Parameter | Type | Required |
| --- | --- | --- |
| emails | `Send[]` | yes |

Returns: { data: ({ id, status } | { id: null, status: "failed", error })[] }

### What batch is not

It is not a mailing list, and it does not template. Every entry carries its own rendered subject and body, so personalisation happens in your code before the call. If you want a list with subscription state, see /docs/audiences.

The whole request counts as its entries against your quota, so 100 emails in one batch and 100 sent one at a time draw down the same allowance. What batch saves is round trips, which matters when you are sending from a serverless function with a wall-clock budget.

idempotency_key works per entry, not per batch. Give each entry its own key and a redelivered queue message re-sends nothing.

---

Base URL: https://emails.sh/v1. Auth: `Authorization: Bearer esh_...`.
Whole API in one file: https://emails.sh/llms.txt. All documentation: https://emails.sh/docs.md.
