# Send email

> The fields, limits and options of a send request.

Source: https://sndr.sh/docs/sending

Send with `POST /v1/send`. The API answers `202 Accepted` straight away and delivers in the background.

![Your app posts to /v1/send and gets 202 queued. SNDR DKIM-signs and delivers the email; the result (delivered, bounced or failed) goes to your webhook endpoint.](https://sndr.sh/docs/images/send-lifecycle.svg)

cURL:

```bash
curl -X POST https://api.sndr.sh/v1/send \
  -H "Authorization: Bearer $SNDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@mail.acme.com>",
    "to": ["customer@example.com"],
    "subject": "Hello from SNDR",
    "html": "<p>It works.</p>"
  }'
```

Node.js:

```js
import { Sndr } from '@rkiza/sndr';

const sndr = new Sndr(process.env.SNDR_API_KEY);

const email = await sndr.emails.send({
  from: 'Acme <hello@mail.acme.com>',
  to: 'customer@example.com',
  subject: 'Hello from SNDR',
  html: '<p>It works.</p>',
});

console.log(email.id); // eml_...
```

Python:

```python
import os
import requests

res = requests.post(
    "https://api.sndr.sh/v1/send",
    headers={"Authorization": f"Bearer {os.environ['SNDR_API_KEY']}"},
    json={
        "from": "Acme <hello@mail.acme.com>",
        "to": ["customer@example.com"],
        "subject": "Hello from SNDR",
        "html": "<p>It works.</p>",
    },
)
res.raise_for_status()
print(res.json()["id"])
```

Go:

```go
body := strings.NewReader(`{
  "from": "Acme <hello@mail.acme.com>",
  "to": ["customer@example.com"],
  "subject": "Hello from SNDR",
  "html": "<p>It works.</p>"
}`)

req, _ := http.NewRequest("POST", "https://api.sndr.sh/v1/send", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SNDR_API_KEY"))
req.Header.Set("Content-Type", "application/json")

res, err := http.DefaultClient.Do(req)
```

## Request fields

- `from` (string, required): The sender: `hello@mail.acme.com` or `Acme <hello@mail.acme.com>`. Its domain must be verified.
- `to` (string[], required): Recipients, 1 to 1,000 addresses. Always an array in the HTTP API.
- `cc` (string[]): Copied recipients, up to 100.
- `bcc` (string[]): Hidden recipients, up to 100.
- `reply_to` (string): Where replies go, if not to `from`.
- `subject` (string, required): One line, up to 998 characters. Optional when the template has a subject.
- `html` (string): HTML body. Send `html`, `text` or both.
- `text` (string): Plain-text body for clients that don’t show HTML.
- `template_id` (string): A saved template. Its subject and bodies fill any field you leave empty.
- `variables` (object): Values for `{{name}}` placeholders in the subject and bodies, e.g. `{"name": "Sara"}`.
- `attachments` (object[]): Files: `{ "filename", "content", "content_type" }`, with `content` base64-encoded.
- `headers` (object): Extra email headers, e.g. `{"X-Campaign": "welcome"}`.

## Limits

| Limit | Value |
| --- | --- |
| Recipients (`to` + `cc` + `bcc`) | 1,000 per request |
| Attachments | 10 files, 10 MiB total after decoding |
| Request body | 16 MiB |
| Subject | 998 characters, one line |

## Retry safely

Add an `Idempotency-Key` header with a unique value, such as your order ID. A retry with the same key returns the first response instead of sending the email again.

```bash
-H "Idempotency-Key: order-1234-receipt"
```

## Suppressed recipients

Addresses on your suppression list (hard bounces, complaints, unsubscribes) are removed before sending. If every `to` address is suppressed, the request fails with `suppressed`.

## Response

202 Accepted:

```json
{
  "id": "eml_01HXKQ8B3N",
  "from": "Acme <hello@mail.acme.com>",
  "to": ["customer@example.com"],
  "subject": "Hello from SNDR",
  "status": "queued",
  "created_at": "2026-01-15T10:30:00Z"
}
```

`status` starts as `queued` and ends as `delivered`, `bounced` or `failed`. Read it with `GET /v1/emails/{id}`.
