# Sending email

This guide covers the single-send endpoint, `POST /v1/email/messages`. You build one JSON payload with a sender, recipients, and content; Bird returns `202 Accepted` with a message ID and delivers asynchronously. Full request and response schemas live in the [API reference](/docs/api/reference/create-email-message).

## A minimal send

The smallest valid payload is a `from`, at least one `to` recipient, a `subject`, and a body (`html`, `text`, or both). The `from` address must be on a domain you have verified in the workspace — with one exception for testing, below.

```bash
curl -X POST https://us1.platform.bird.com/v1/email/messages \
  -H "Authorization: Bearer bk_us1_..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "to": ["delivered@messagebird.dev"],
    "subject": "Hello from Bird",
    "html": "<p>It works.</p>"
  }'
```

Use your regional host (`https://us1.platform.bird.com` or `https://eu1.platform.bird.com`) with a matching `bk_{region}_...` key. The recipient here is `delivered@messagebird.dev`, a [sandbox address](/docs/guides/email/testing-sandbox) that always accepts mail — handy while you wire things up, since placeholder domains like `@example.com` and anything under `.test`, `.example`, `.invalid`, or `.localhost` are rejected with a `422` (they can't receive mail, and the bounces would hurt your reputation).

Haven't verified a domain yet? During onboarding you can send from the shared onboarding domain (for example `onboarding@messagebird.dev`). These sends skip the domain check but can only go to verified members of your own workspace and are subject to a daily recipient limit per organization.

## Building up the payload

### Recipients

`to`, `cc`, and `bcc` each take an array of up to 50 addresses; `to` requires at least one. Each entry can be a plain email string, an RFC 5322 mailbox string (`Jane <jane@example.com>`), or an object with an optional display name. Recipients on your workspace's [suppression list](/docs/guides/email/suppressions) are blocked per the active category's policy — each one surfaces as a rejected recipient with an inspectable reason, never a silent drop. Suppression is applied asynchronously, after the `202`: a suppressed recipient is accepted and then marked `status: rejected` (with reason `recipient_suppressed`) on the read endpoints, rather than failing the request up front. Even when _every_ requested recipient is suppressed, the request is still accepted with a `202`; each recipient comes back rejected.

### Content

`subject` is required (max 998 characters). Provide `html`, `text`, or both — at least one is required, and each is capped at 524,288 characters (512&nbsp;KiB, roughly 512&nbsp;KB for plain ASCII). Sending both is good practice: clients that can't (or won't) render HTML fall back to the text part.

### Reply-to and custom headers

`reply_to` is an array (1–25 entries, same address formats as recipients). Every recipient reply goes to _all_ listed addresses, so one or two is typical. `headers` is a string-to-string object for custom email headers, e.g. `{"X-Campaign": "spring-2026"}`.

### Tracking

`track_opens` and `track_clicks` both default to `true`. Set them to `false` to skip open-pixel injection or link rewriting for a given send. See [tracking and metrics](/docs/guides/email/tracking-and-metrics) for what each one does to your message.

### Category and IP pool

`category` classifies the content and controls suppression policy: `marketing` blocks delivery on all suppression reasons (use it for marketing content), while `transactional` delivers through complaint and unsubscribe suppressions (use it for receipts, password resets, and similar operational messages). It defaults to `marketing`, independent of which endpoint you call; set it to `transactional` explicitly for operational mail like receipts and password resets. See [categories](/docs/guides/email/categories).

`ip_pool` selects the sending pool: a pool ID (`ipp_...`) or `ipp_shared` to route through the shared pool explicitly. Omit it to use your organization's default pool. An unknown pool, or a pool with no dedicated IPs available, is rejected with a `422`.

### Field reference

| Field          | Type                     | Required     | Limits / notes                                                                  |
| :------------- | :----------------------- | :----------- | :------------------------------------------------------------------------------ |
| `from`         | address                  | yes          | Must be on a verified domain (or the onboarding domain)                         |
| `to`           | address[]                | yes          | 1–50                                                                            |
| `cc`, `bcc`    | address[]                | no           | Max 50 each                                                                     |
| `subject`      | string                   | yes          | Max 998 characters                                                              |
| `html`, `text` | string                   | at least one | Max 524,288 characters each (512 KiB)                                           |
| `reply_to`     | address[]                | no           | 1–25; replies hit all listed addresses                                          |
| `headers`      | object (string → string) | no           | Custom email headers                                                            |
| `tags`         | `{name, value}[]`        | no           | Max 20; name ≤ 32 chars, value ≤ 64 chars; `[A-Za-z0-9_-]` only                 |
| `metadata`     | object                   | no           | Arbitrary JSON, max 2 KB serialized                                             |
| `track_opens`  | boolean                  | no           | Default `true`                                                                  |
| `track_clicks` | boolean                  | no           | Default `true`                                                                  |
| `category`     | string                   | no           | `marketing` or `transactional`; default `marketing`                             |
| `ip_pool`      | string                   | no           | `ipp_...` or `ipp_shared`; omit for your org's default pool                     |
| `template`     | string                   | no           | Send a published template by ID (`emt_…`) or name; omit `subject`/`html`/`text` |
| `parameters`   | object                   | no           | Template variables, max 16 KB serialized; shared across recipients              |

## Sending with a template

Instead of inline content, you can send a published [email template](/docs/guides/email/templates): set `template` to the template's ID (`emt_…`) or its name, omit `subject`/`html`/`text`, and pass `parameters` for its variables.

```bash
curl -X POST https://us1.platform.bird.com/v1/email/messages \
  -H "Authorization: Bearer bk_us1_..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "to": ["delivered@messagebird.dev"],
    "template": "welcome-email",
    "parameters": { "first_name": "Jane" }
  }'
```

The template supplies the subject and body; `parameters` fill its `{{ variable }}` placeholders (a placeholder with no matching key renders empty). Everything else — recipients, `tags`, `metadata`, `category`, tracking, attachments — works exactly as an inline send. Two rules to know:

- **Inline or templated, not both.** Sending `template` together with `subject`, `html`, or `text` is rejected with a `422`.
- **Sends use the current published version.** A send resolves the template's live published version — drafts are never sent, and an unknown template (or one with no published version) is rejected with a `404`.

See the [templates guide](/docs/guides/email/templates) for authoring, publishing, and versioning.

## Tags vs metadata

Both attach your own data to a send, but they serve different jobs:

- **`tags`** are structured `{name, value}` pairs (max 20 per send; name ≤ 32 characters, value ≤ 64, ASCII `[A-Za-z0-9_-]` only). They are first-class filter dimensions: filter the message list by tag and slice analytics and dashboard rollups by tag. Use tags for low-cardinality labels like `category`, `experiment_variant`, or `template_id`.
- **`metadata`** is an arbitrary JSON object (max 2 KB serialized). It is stored, returned on API reads, and echoed on every webhook event — but it is not a dashboard filter dimension. Use it for round-trip context: internal IDs, foreign keys, structured context you want handed back with each event.

Rule of thumb: if you want a dashboard breakdown by this dimension, use a tag; if you want context to round-trip through the API, use metadata. Both your tags and metadata are echoed on every webhook event alongside the correlation IDs (`email_id`, `recipient_id`), so you can route and reconcile against your own records without an extra lookup. You don't need to encode device, geo, mailbox provider, bounce type, or recipient domain into tags — Bird captures those as first-class analytics dimensions automatically.

```json
{
  "tags": [{ "name": "category", "value": "onboarding" }],
  "metadata": { "user_id": "usr_12345", "order_id": "ord_98765" }
}
```

## The async model: what 202 means

A successful send returns `202 Accepted` with an `em_`-prefixed message ID and `status: accepted`:

```json
{
  "id": "em_019c1930687b7bfa...",
  "status": "accepted",
  "category": "transactional",
  "accepted_count": 1,
  "created_at": "2026-06-10T14:30:00Z"
}
```

The `202` is returned only after the send is durably accepted — it is never accepted and then silently dropped. Hard failures you can fix (unverified sender domain, field validation) fail immediately with a `422` instead. Suppression is not one of them: suppressed recipients are accepted and later surface as `status: rejected`, never a synchronous error. Actual delivery happens asynchronously: per-recipient outcomes (delivered, bounced, deferred, complained) arrive afterwards through webhooks and the message read endpoints.

Two consequences of the async design worth knowing:

- **Reads return state, not content.** `GET /v1/email/messages/{id}` returns message and recipient state, never the `html` or `text` body. Bodies are stored separately: content storage is on by default per workspace (you can opt out), and when it is on the stored `html` and `text` are available for up to 30 days from the dedicated `GET /v1/email/messages/{id}/content` endpoint.
- **Reads can briefly trail the 202.** There is a short window between the `202` and the message becoming visible on the read endpoints; a `404` immediately after a send resolves itself within moments.

## Attachments

Add files to a send with an `attachments` array — base64-encoded file bytes inline, up to 20 per message, within a 20 MB generated-message budget. Inline images, the field contract, blocked file types, and downloading attachments back are covered in [attachments](/docs/guides/email/attachments).

## Reserved fields

The following fields are part of the request vocabulary but are not yet available. Including any of them returns `422 unsupported_feature` — they are reserved now so SDKs and integrations converge on one name before launch:

`topic_id`, `contact_id`, `in_reply_to_message_id`

Don't build around these yet; watch the [API reference](/docs/api/reference/create-email-message) for availability. `scheduled_at` has since shipped — see [scheduled sending](/docs/guides/email/scheduled-sending).

## Retrying safely

Send the `Idempotency-Key` header with a unique value per logical send, and retries become safe: if your first request succeeded but you never saw the response (timeout, dropped connection), replaying the same request with the same key returns the original result instead of sending a duplicate email. Replayed responses carry an `Idempotency-Replay` header so you can tell them apart. See [idempotency](/docs/guides/idempotency) for key format and retention.

## Next steps

- [Templates](/docs/guides/email/templates) — author reusable content and send it by ID or name
- [Categories](/docs/guides/email/categories) — how `marketing` vs `transactional` changes suppression behavior
- [Suppressions](/docs/guides/email/suppressions) — who Bird won't deliver to, and why
- [Tracking and metrics](/docs/guides/email/tracking-and-metrics) — opens, clicks, and the analytics they feed
- [Testing sandbox](/docs/guides/email/testing-sandbox) — sandbox recipients and pre-verification sending
- [API reference](/docs/api/reference/create-email-message) — full request and response schemas