# Email delivery

> Email rendered files through your own Brevo, SMTP, Postmark, Resend, Amazon SES or Microsoft 365 account, with connections, sender checks, email rules, test renders, send statuses, events, limits, errors and billing.



Email delivery sends a render's files to the people named in its data. You connect your own email provider account, add email rules to a template, and every succeeded render of that template sends one email per rule. The mail goes out through your account and from your domain; nothing is sent from ours. Emails aren't billed: your provider charges for them, and the render is billed as usual.

Email delivery is included in the Growth, Pro, Scale and Enterprise plans. On Free and Starter, a template's rules don't send, and creating or changing connections and rules fails with `402 plan_feature_unavailable`. See [Plans and billing](#plans-and-billing).

Email delivery is being switched on workspace by workspace. While it isn't enabled for yours, the dashboard doesn't show it, and creating, changing or testing connections and creating or changing rules fail with `403 feature_not_enabled`; reading, deleting and switching rules off still work. To have it enabled, write to [support@dynamicdocumentapi.com](mailto:support@dynamicdocumentapi.com).

## How it works [#how-it-works]

1. **Connect your provider.** An email connection (`emc_…`) holds the credentials of your provider account, the default sender and what test renders do.
2. **Add email rules to a template.** An email rule (`emr_…`) says which connection sends, from which sender, to whom, with which subject, body and files, and under which condition. Recipients, subject and body use the [template language](/docs/template-language) with the render's data.
3. **Render the template.** When the render succeeds, each enabled rule becomes one email send (`ems_…`) that goes out through the rule's connection. The render lists its emails in `email`, and webhooks report how each one ended.

Rules apply to renders of the template from the API, including the convenience endpoints and batches, from the dashboard and from integrations. No email is sent for:

* previews, including the editor's
* signed-link renders, because anyone who has the URL could otherwise choose the recipients
* renders without a template: HTML, URL and Markdown input, PDF tools and `POST /v1/einvoices`
* zero-retention renders, which keep no file to send (see [Zero retention and hosted files](#zero-retention-and-hosted-files))
* renders that fail or are canceled

A rule that can't be rendered fails only its own email. The render, its files and its other emails are unaffected.

In the [dashboard](https://app.dynamicdocumentapi.com), connections are under **Email**, and a template's rules on its **Email** tab. API keys need these scopes:

| Scope          | Allows                                                                         |
| -------------- | ------------------------------------------------------------------------------ |
| `email:read`   | Reading connections and rules, and rule previews                               |
| `email:write`  | Creating, changing, testing and deleting connections and rules, and rule tests |
| `renders:read` | Reading emails, their delivery events and the statistics                       |
| `render:write` | Resending emails                                                               |

Rendering a template that has rules needs no extra scope.

## Connect your provider [#connect-your-provider]

Create a connection in the dashboard under **Email** → **Add connection**, or through the API with a key that has the `email:write` scope:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/email-connections \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Brevo",
    "provider": "brevo",
    "credentials": { "api_key": "xkeysib-…" },
    "default_from": { "email": "billing@example.com", "name": "Example Billing" },
    "reply_to": "accounts@example.com",
    "test_mode": "sandbox"
  }'
```

The response is the connection, without the credentials:

```json
{
  "id": "emc_01J9ZK3M7Q8V5W2X4Y6Z8A0B1F",
  "object": "email_connection",
  "name": "Brevo",
  "provider": "brevo",
  "config": { "attachment_budget_bytes": 10485760, "max_per_second": 10 },
  "credentials": { "configured": true },
  "default_from": { "email": "billing@example.com", "name": "Example Billing" },
  "reply_to": "accounts@example.com",
  "test_mode": "sandbox",
  "test_recipient": null,
  "senders": [],
  "status": "active",
  "last_test_at": null,
  "last_test_status": null,
  "last_error": null,
  "credentials_delete_at": null,
  "egress_ips": ["203.0.113.10"],
  "events": {},
  "created_at": "2026-10-01T09:10:00Z",
  "updated_at": "2026-10-01T09:10:00Z"
}
```

| Field            | Description                                                                                                                                                                                   |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`           | A label for the dashboard, up to 100 characters                                                                                                                                               |
| `provider`       | `brevo`, `smtp`, `postmark`, `resend`, `ses` (Amazon SES) or `graph` (Microsoft 365). It can't change after the connection is created.                                                        |
| `config`         | The provider's settings, see [Providers](#providers). `attachment_budget_bytes` and `max_per_second` apply to every provider; reads always show them, filled in with the provider's defaults. |
| `credentials`    | The provider's secrets. Write-only: reads show `{"configured": true}`.                                                                                                                        |
| `default_from`   | `email` (required) and `name`: the sender of rules that don't set their own. `email` is one address, without a display name.                                                                  |
| `reply_to`       | Optional default reply-to address                                                                                                                                                             |
| `test_mode`      | What test renders do: `sandbox` (Brevo only, and its default), `redirect` or `skip` (the default for every other provider). See [Live and test renders](#live-and-test-renders).              |
| `test_recipient` | Required with `redirect`: the verified email address of a member of your workspace                                                                                                            |

Reads also return:

| Field                              | Description                                                                                                                                        |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                               | The connection ID (`emc_…`)                                                                                                                        |
| `senders`                          | The verified senders and domains that the last test found, see [Senders and verification](#senders-and-verification)                               |
| `status`                           | `active`, or `failing` after the provider refused the credentials or our IP addresses                                                              |
| `last_test_at`, `last_test_status` | When the last test ran, and whether it `succeeded` or `failed`                                                                                     |
| `last_error`                       | Why the connection is failing: `code` (`email_provider_auth_failed`, `email_ip_not_authorized` or `email_credentials_removed`), `message` and `at` |
| `credentials_delete_at`            | Set after a downgrade: when the credentials will be deleted                                                                                        |
| `egress_ips`                       | The IP addresses our email traffic leaves from, see [Authorise our IP addresses](#authorise-our-ip-addresses)                                      |
| `events`                           | Brevo only: the delivery events webhook per region, `registered` or `failed`                                                                       |

| Endpoint                               | Description                                                                                                      |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `GET /v1/email-connections`            | List connections. The list also carries `egress_ips`.                                                            |
| `POST /v1/email-connections`           | Create a connection                                                                                              |
| `GET /v1/email-connections/{id}`       | Retrieve a connection                                                                                            |
| `PATCH /v1/email-connections/{id}`     | Change a connection. A body without `credentials` keeps the stored ones.                                         |
| `DELETE /v1/email-connections/{id}`    | Delete a connection. While rules use it, the answer is `409 email_connection_in_use`, with the rules in `rules`. |
| `POST /v1/email-connections/{id}/test` | [Test the connection](#test-a-connection)                                                                        |

A workspace can have up to 10 connections. Deleting a connection cancels its emails that haven't been sent yet; the history of sent emails stays.

### How credentials are stored [#how-credentials-are-stored]

Credentials are encrypted before they are stored, and the API never returns them. They are decrypted only to call your provider for you. To replace them, send new `credentials` with `PATCH`. Deleting a connection deletes its credentials, and when your plan stops including email delivery they are deleted after 30 days (see [Plans and billing](#plans-and-billing)).

> **Use a separate Brevo key**
>
> A Brevo API key gives access to your whole Brevo account. Create a key only for this connection, so you can revoke it without affecting anything else.

### Providers [#providers]

| `provider` | `config`                                                             | `credentials`                        |
| ---------- | -------------------------------------------------------------------- | ------------------------------------ |
| `brevo`    | —                                                                    | `api_key`                            |
| `smtp`     | `host`, `port` (587 or 2525, default 587), `username`                | `password`                           |
| `postmark` | `message_stream` (default `outbound`)                                | `server_token`                       |
| `resend`   | —                                                                    | `api_key`                            |
| `ses`      | `region`, such as `eu-central-1`, and optionally `configuration_set` | `access_key_id`, `secret_access_key` |
| `graph`    | `tenant_id`, `client_id`, `sender`                                   | `client_secret`                      |

* **Brevo**: an API key (`xkeysib-…`) from **SMTP & API** → **API keys**. Brevo's SMTP keys (`xsmtpsib-…`) aren't accepted, and an SMTP connection to Brevo's relay is refused: use the `brevo` provider.
* **SMTP**: any server that offers STARTTLS and authentication on port 587 or 2525. TLS 1.2 or later is required, and a server that offers authentication without STARTTLS is refused, so your password never travels unencrypted. `host` is the server's public host name, without a scheme or port.
* **Postmark**: the server's API token, and the message stream to send through.
* **Resend**: an API key with sending access. Every attempt of an email carries the same idempotency key, so an interrupted call is retried safely instead of ending as `unknown`.
* **Amazon SES**: the access key of an IAM user that may call `ses:SendRawEmail` and, for the connection test, read the account and its identities. Temporary credentials aren't supported.
* **Microsoft 365**: register an app in Microsoft Entra ID with the `Mail.Send` application permission. `tenant_id` is the directory (tenant) ID or one of your domains, `client_id` the application (client) ID, and `sender` the address of the mailbox that sends. Emails are saved in that mailbox's Sent Items.

Every provider also takes these `config` fields:

| Field                     | Description                                                                                                                                                  |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `attachment_budget_bytes` | Bytes of files attached to one email. Files beyond it are linked or fail the email, depending on the rule's [attachments](#attachments-and-download-links).  |
| `max_per_second`          | Emails per second through this connection, 1 to 100 (default 10). Further emails wait their turn, so a large batch stays within your provider's rate limits. |

| Provider      | Attachment budget, default | Maximum |
| ------------- | -------------------------- | ------- |
| Brevo         | 10 MiB                     | 13 MiB  |
| SMTP          | 6 MiB                      | 25 MiB  |
| Postmark      | 6 MiB                      | 6 MiB   |
| Resend        | 10 MiB                     | 25 MiB  |
| Amazon SES    | 10 MiB                     | 25 MiB  |
| Microsoft 365 | 2 MiB                      | 2 MiB   |

The maximums leave room for encoding and the body within each provider's message size limit.

### Test a connection [#test-a-connection]

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/email-connections/emc_01J9ZK3M7Q8V5W2X4Y6Z8A0B1F/test \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"send_to": "alex@example.com"}'
```

The test checks the saved settings one step at a time, through the same IP addresses as your emails:

| Provider      | Steps                                                                                                                                                                         |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Brevo         | `credentials` (the API key works), `senders` (lists your verified senders and domains), `sandbox_send` (Brevo checks a message from the default sender without delivering it) |
| SMTP          | `connect`, `starttls`, `smtp_auth`                                                                                                                                            |
| Postmark      | `credentials`                                                                                                                                                                 |
| Resend        | `credentials`, which also lists your verified domains                                                                                                                         |
| Amazon SES    | `credentials`, `senders`                                                                                                                                                      |
| Microsoft 365 | `credentials` (Microsoft Entra ID issues a token)                                                                                                                             |

With `send_to`, a final `send` step sends a real test email to that address, which must be the verified email address of a workspace member. In the dashboard, **Send a test email to me** uses your own address.

```json
{
  "object": "email_connection_test",
  "ok": true,
  "steps": [
    { "name": "credentials", "ok": true, "detail": "Brevo accepted the API key (account: Example GmbH)." },
    { "name": "senders", "ok": true, "detail": "Verified: 1 sender and 1 domain." },
    { "name": "sandbox_send", "ok": true, "detail": "Brevo accepted a sandboxed email from billing@example.com; nothing was delivered." },
    { "name": "send", "ok": true, "detail": "The test email was accepted (message id <…@smtp-relay.mailin.fr>)." }
  ],
  "senders": [
    { "email": "billing@example.com", "name": "Example Billing", "kind": "sender", "verified": true },
    { "email": "example.com", "name": null, "kind": "domain", "verified": true }
  ],
  "connection": { "id": "emc_01J9ZK3M7Q8V5W2X4Y6Z8A0B1F", "object": "email_connection", "status": "active", "last_test_status": "succeeded" }
}
```

The `connection` above is shortened; the response carries the whole connection.

A failed step answers `200` with `ok: false`. The test stops at that step, and its `detail` says what went wrong. Every test updates `last_test_at` and `last_test_status`, and `senders` when the provider listed them. A passing test sets `status` to `active`. A failed `credentials` or `smtp_auth` step sets it to `failing`, with `last_error`.

Emails update the status too. When they fail because the provider refuses the credentials or our IP addresses, the connection turns `failing` within a few minutes, and the workspace's owners and admins get an email, at most once a day per connection. The next accepted email or passing test sets the connection back to `active`.

### Authorise our IP addresses [#authorise-our-ip-addresses]

Your emails leave from a fixed set of IP addresses, listed in `egress_ips` and on the dashboard's **Email** page. If your provider only accepts calls from known IP addresses, authorise them there. Brevo blocks unknown addresses once its IP security has learned your usual ones: add ours under **Security** → **Authorized IPs**. Changes to the addresses are announced 30 days ahead.

An email that the provider refuses because of our IP address is retried (`email_ip_not_authorized`), so it still goes out if you authorise the addresses while its retries last.

## Senders and verification [#senders-and-verification]

Each email is sent from the rule's `from_email`, or else from the connection's `default_from`. The sender name and reply-to address come from the rule when they render to something, and from the connection otherwise. Your provider decides which senders it accepts, so send from an address or domain that you verified there.

The connection test lists what your provider has verified, and stores it on the connection as `senders`:

| Provider                      | `senders`                                                                                        |
| ----------------------------- | ------------------------------------------------------------------------------------------------ |
| Brevo                         | Active senders, and authenticated or verified domains                                            |
| Resend                        | Verified domains. A key that can only send can't list them, and the test passes without senders. |
| Amazon SES                    | Verified identities, both domains and addresses. The `senders` step fails when there are none.   |
| SMTP, Postmark, Microsoft 365 | None: these providers don't list senders to us                                                   |

Each entry has `email` (an address, or the domain for `kind: "domain"`), `name`, `kind` (`sender` or `domain`) and `verified`.

A rule whose `from_email` matches no entry, neither the address itself nor its domain, is saved with a warning:

```json
"warnings": [
  {
    "code": "email_sender_unverified",
    "message": "invoices@example.com is not among the verified senders of Brevo; the provider may refuse it.",
    "path": "/from_email"
  }
]
```

The check runs only once a test has listed senders, and it never blocks anything: your provider decides when the email goes out, and a sender it refuses fails the email with `email_provider_rejected`. After you verify a new sender at your provider, test the connection again to refresh `senders`.

## Email rules [#email-rules]

A template can have up to five rules. Every enabled rule sends its own email, in `position` order, for example the invoice to the customer and a copy to accounting. Add rules on the template's **Email** tab, or with a key that has the `email:write` scope:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/templates/tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C/email-rules \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Invoice to customer",
    "connection_id": "emc_01J9ZK3M7Q8V5W2X4Y6Z8A0B1F",
    "from_email": "billing@example.com",
    "from_name": "Example Billing",
    "to": "{{ customer.name }} <{{ customer.email }}>",
    "cc": "{{ customer.accounting_email }}",
    "subject": "Invoice {{ number }}",
    "html": "<p>Dear {{ customer.name }},</p><p>please find invoice {{ number }} attached.</p>",
    "attachments": "attach_or_link",
    "attach_formats": ["pdf", "xml"],
    "condition": "send_invoice"
  }'
```

The response is the rule with its ID (`emr_…`), its fields as saved and `warnings`.

| Field               | Template language   | Description                                                                                                                   |
| ------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `name`              | no                  | A label, up to 100 characters                                                                                                 |
| `connection_id`     | no                  | The connection that sends (`emc_…`)                                                                                           |
| `enabled`           | no                  | `false` switches the rule off. Default `true`.                                                                                |
| `position`          | no                  | 0 to 4; the other rules move. Defaults to the end.                                                                            |
| `from_email`        | no                  | The sender: one address, without a display name. Empty uses the connection's `default_from`.                                  |
| `from_name`         | yes                 | The sender name. Empty uses the connection's.                                                                                 |
| `reply_to`          | yes                 | The reply-to address. Empty uses the connection's.                                                                            |
| `to`, `cc`, `bcc`   | yes                 | The recipients, see [Recipients](#recipients)                                                                                 |
| `subject`           | yes                 | Required, unless a [Brevo template](#brevo-templates) provides it                                                             |
| `html`              | yes, values escaped | The body. `html` or `text` is required.                                                                                       |
| `text`              | yes                 | The plain-text body. Empty derives it from `html`.                                                                            |
| `attachments`       | no                  | `attach_or_link` (default), `attach`, `link` or `none`, see [Attachments and download links](#attachments-and-download-links) |
| `attach_formats`    | no                  | Only files of these formats: `pdf`, `png`, `jpeg`, `webp`, `html`, `xml`, `zip`. Empty sends every file.                      |
| `condition`         | expression          | The rule sends only when the expression is true. Empty always sends.                                                          |
| `provider_template` | —                   | Brevo only: send one of your Brevo templates instead of `html` and `text`                                                     |

Saving compiles every field. A field that doesn't compile fails with `422 email_template_error`, and each entry of `errors[]` gives the field as `path` (such as `/subject`) with `line` and `column`. Each field's source can be up to 16 KiB, and `html` up to 256 KiB.

Rules are live settings, not part of template versions:

* A change applies to renders submitted after you save, without publishing. A render keeps the rules as they were when it was submitted.
* Switching a rule off or deleting it cancels its emails that haven't been sent yet.
* Duplicating a template copies its rules switched off, so the copy doesn't send before someone has checked them.
* Transferring a template to another workspace deletes its rules, because connections belong to the workspace.

| Endpoint                                       | Description                                                           |
| ---------------------------------------------- | --------------------------------------------------------------------- |
| `GET /v1/templates/{template_id}/email-rules`  | List a template's rules in `position` order                           |
| `POST /v1/templates/{template_id}/email-rules` | Create a rule                                                         |
| `GET /v1/email-rules/{id}`                     | Retrieve a rule                                                       |
| `PATCH /v1/email-rules/{id}`                   | Change a rule. `{"enabled": false}` switches it off.                  |
| `DELETE /v1/email-rules/{id}`                  | Delete a rule                                                         |
| `POST /v1/email-rules/{id}/preview`            | [Preview](#preview-and-test-a-rule) the email; nothing is sent        |
| `POST /v1/email-rules/preview`                 | Preview a rule that isn't saved yet                                   |
| `POST /v1/email-rules/{id}/test`               | Send one [test email](#preview-and-test-a-rule) to a workspace member |

### Recipients [#recipients]

`to`, `cc` and `bcc` render to a list of addresses:

* Separate the entries with `,`, `;` or line breaks.
* Each entry is an address or `Name <address>`. Quote a name that may contain a comma: `"{{ customer.name }}" <{{ customer.email }}>`.
* Empty entries are ignored, and an address that appears more than once is kept once: the first occurrence across `to`, `cc` and `bcc` wins.
* One email takes at most 50 recipients, `to`, `cc` and `bcc` together.

A loop can build the list from your data; the empty entry after the last comma is ignored:

```jinja
{% for contact in customer.contacts %}{{ contact.email }}, {% endfor %}
```

| Problem                   | The email fails with                                 |
| ------------------------- | ---------------------------------------------------- |
| An entry isn't an address | `email_address_invalid`; the message names the entry |
| `to` has no address       | `email_recipient_missing`                            |
| More than 50 recipients   | `email_too_many_recipients`                          |

The rendered `reply_to` and the sender are checked the same way.

### Subject and body [#subject-and-body]

Rule fields render with the [template language](/docs/template-language), in the context of the document they send:

| Variable             | Description                                                                                                                                      |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Data keys and `data` | The render's data, as in the template                                                                                                            |
| `render`             | `id`, `created_at`, `template_id` and `test`, plus `invoice` and `einvoice` when the render computed them (see [E-invoicing](/docs/e-invoicing)) |
| `files`              | The render's files, in output order: `filename`, `format`, `bytes` and `pages` (0 for images)                                                    |
| `download_links`     | Where the links to linked files go, see [Attachments and download links](#attachments-and-download-links)                                        |

`files` and `download_links` take precedence over data keys with the same names; use `data.files` to reach such a key.

```jinja
<p>Dear {{ customer.name }},</p>
<p>Please find invoice {{ number }} attached.</p>
{{ download_links }}
<p>Kind regards<br>Example GmbH</p>
```

* `html` escapes values like an HTML template. Mark HTML you trust with `safe`. The other fields aren't escaped.
* `subject`, `from_name` and `reply_to` become one line: line breaks and control characters turn into spaces, and they are cut to 255 characters. Data can't add email headers.
* `strict_undefined: true` in the render request applies to the rules too.
* After rendering, `html` can be up to 512 KiB, `text` up to 256 KiB, and `to`, `cc` and `bcc` up to 16 KiB each. A larger field fails the email with `email_too_large`.
* A template error fails the email with `email_template_error`; its `error` gives the `field`, `line` and `column`.

### Conditions [#conditions]

`condition` is an expression in the same context, written without `{{ }}`:

```jinja
send_invoice and customer.email
```

When it is false, the email is recorded as `skipped` with `skip_reason: "condition_false"`, and no event is sent. An empty condition always sends.

### Attachments and download links [#attachments-and-download-links]

| `attachments`    | What the email carries                                                                                                                                                  |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `attach_or_link` | The default. Each file is attached while the attached files fit the connection's attachment budget and the provider accepts the file type; any other file is linked.    |
| `attach`         | Every file is attached. A file over the budget fails the email with `email_attachment_too_large`, a type the provider refuses with `email_attachment_type_not_allowed`. |
| `link`           | A download link for every file                                                                                                                                          |
| `none`           | No files                                                                                                                                                                |

* Files are taken in the render's output order, after `attach_formats` has filtered them.
* Brevo doesn't accept WebP attachments, so `attach_or_link` links WebP files.
* A link is a signed URL that downloads the file. It is valid until the file's retention ends, and at most 7 days after the email was created.
* The links appear where `html` or `text` uses `{{ download_links }}`: a list of links named by file name in `html`, and one `filename: URL` line per file in `text`. Without `{{ download_links }}`, the list is added at the end of the body. When nothing is linked, the placeholder is removed.

Each email lists its files in `attachments`, with `mode` set to `attached` or `linked`. Attaching and linking need the render's hosted file, see [Zero retention and hosted files](#zero-retention-and-hosted-files).

### Brevo templates [#brevo-templates]

With a Brevo connection, a rule can send one of your Brevo account's templates instead of its own body:

```json
"provider_template": { "id": 12, "params": "{\"number\": {{ number | tojson }}, \"name\": {{ customer.name | tojson }}}" }
```

* `id` is the template's ID in Brevo, a whole number from 1.
* `params` is template source that must render to a JSON object, up to 64 KiB of source. Brevo receives it as the template's params. Anything else fails the email with `email_template_error`.
* `html` and `text` can be empty. `subject` is optional; an empty one uses the Brevo template's subject.
* Attachments work as usual. Other providers can't send provider templates.

## Choose rules per render [#choose-rules-per-render]

`delivery.email` on `POST /v1/renders`, the convenience endpoints and `POST /v1/batches` decides which rules a render uses:

| `delivery.email`         | Rules                                                                                           |
| ------------------------ | ----------------------------------------------------------------------------------------------- |
| absent or `null`         | Every enabled rule of the template                                                              |
| `false`                  | None                                                                                            |
| a list of 1 to 5 entries | Only these rules. Each entry names an enabled rule of the render's template in `rule_id`, once. |

An entry can replace the rule's recipients with literal addresses: `to` (1 to 50 entries), `cc` and `bcc` (`[]` for none). Each entry is one address or `Name <address>`, and the three lists together hold at most 50.

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/renders \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Idempotency-Key: invoice-R-2026-0042" \
  -H "Content-Type: application/json" \
  -d '{
    "input": { "type": "template", "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C" },
    "data": { "number": "R-2026-0042", "send_invoice": true, "customer": { "name": "Erika Muster", "email": "erika.muster@customer.example" } },
    "mode": "async",
    "delivery": {
      "email": [
        { "rule_id": "emr_01J9ZK3M7Q8V5W2X4Y6Z8A0B1E", "to": ["Erika Muster <erika.muster@customer.example>"], "cc": [] }
      ]
    }
  }'
```

`false` is always accepted. A list fails:

* with `400 validation_error` when the render has no template, or for an unknown rule, another template's rule, a switched-off rule, a rule listed twice, an invalid address or more than 50 recipients; `errors[]` points to `/delivery/email/<i>/…`
* with `402 plan_feature_unavailable` when the plan doesn't include email delivery
* with `422 email_needs_hosted_file` for a zero-retention render

A batch resolves its rules once, when it is submitted, and every item sends its own emails. The Batch object counts them in `emails`: `sent`, `failed`, `unknown`, `pending`, `skipped` and `canceled`. See [Batches](/docs/batches).

## Live and test renders [#live-and-test-renders]

| Render                      | Emails                                                                                 |
| --------------------------- | -------------------------------------------------------------------------------------- |
| Live render                 | Go to the rendered recipients and count toward the [daily cap](#daily-recipient-cap)   |
| Test render (test key)      | Follow the connection's `test_mode`. The addresses in the data never receive anything. |
| Rule test                   | One email to the member address you name, with the subject prefixed `[Test] `          |
| Preview, signed-link render | None                                                                                   |

| `test_mode` | What a test render's email does                                                                                      | Status                                     | Event        |
| ----------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------ |
| `sandbox`   | Brevo checks the message without delivering it. Brevo only, and its default.                                         | `sandboxed`                                | `email.sent` |
| `redirect`  | Goes only to `test_recipient`, without `cc` and `bcc`, with the subject prefixed `[Test] `                           | `sent`                                     | `email.sent` |
| `skip`      | No provider call. The email is recorded with what would have been sent. The default for every provider except Brevo. | `skipped`, with `skip_reason: "test_mode"` | none         |

In `sandbox` and `redirect` mode, the rendered recipients are still checked, so a test render fails the same way as a live render with the same data. Emails of test renders never count toward the daily cap, and test renders are free.

### Preview and test a rule [#preview-and-test-a-rule]

`POST /v1/email-rules/{id}/preview` renders a watermarked preview of the template with the rule, and returns the render object with `email_preview`: the rendered fields, the recipients as parsed and the files as they would be attached or linked. Nothing is sent and nothing is billed. For a rule that isn't saved yet, use `POST /v1/email-rules/preview` with the `template_id` and the rule's fields in `rule`, including `connection_id`.

`POST /v1/email-rules/{id}/test` creates a free, watermarked test render whose only email is the rule's, sent to `to` with the subject prefixed `[Test] `. `to` must be the verified email address of an active member of your workspace.

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/email-rules/emr_01J9ZK3M7Q8V5W2X4Y6Z8A0B1E/test \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "alex@example.com",
    "data": { "number": "R-2026-0042", "send_invoice": true, "customer": { "name": "Erika Muster", "email": "erika.muster@customer.example" } }
  }'
```

The answer is `202` with the test render. Once it has finished, its `email` shows the test email.

Both endpoints:

* render the template's live version, or its draft when nothing is published
* take the data from `data`, else from the dataset `dataset_id` (`ds_…`), else from the template's default dataset, else `{}`
* work for switched-off rules, and take unsaved fields in `rule` instead of the saved ones

The preview's `email_preview`:

```json
"email_preview": {
  "send": true,
  "skip_reason": null,
  "error": null,
  "from_email": "billing@example.com",
  "from_name": "Example Billing",
  "reply_to": "accounts@example.com",
  "to": [{ "email": "erika.muster@customer.example", "name": "Erika Muster" }],
  "cc": [{ "email": "ap@customer.example", "name": null }],
  "bcc": [],
  "recipient_errors": [],
  "subject": "Invoice R-2026-0042",
  "html": "<p>Dear Erika Muster,</p><p>please find invoice R-2026-0042 attached.</p>",
  "text": "",
  "attachments": [
    { "file_id": "file_01J9ZM4T6W8Y0A2C4E6G8J0K2M", "filename": "invoice-R-2026-0042.pdf", "bytes": 48213, "mode": "attached" }
  ],
  "provider_template": null
}
```

| Field               | Description                                                                                                                                                                             |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `send`              | Whether the rule would send                                                                                                                                                             |
| `skip_reason`       | `condition_false` when the condition is false                                                                                                                                           |
| `error`             | A template, recipient or attachment error that would fail the email                                                                                                                     |
| `to`, `cc`, `bcc`   | The recipients as parsed, each with `email` and `name`                                                                                                                                  |
| `recipient_errors`  | Every recipient entry that isn't an address                                                                                                                                             |
| `html`, `text`      | The rendered body. `html` and `text` still hold the placeholder of `{{ download_links }}`, which the links replace when the email is sent. An empty `text` is derived from `html` then. |
| `attachments`       | The files, each `attached` or `linked`                                                                                                                                                  |
| `provider_template` | The Brevo template and its rendered `params`                                                                                                                                            |

When the preview doesn't finish in time, the answer is `202` with `email_preview: null`.

## Email sends [#email-sends]

Every email of a render is an email send (`ems_…`):

| Status      | Meaning                                                                                                                                            |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`   | Waiting to be sent                                                                                                                                 |
| `sending`   | Being handed to your provider                                                                                                                      |
| `retrying`  | An attempt failed and another follows; `error` says why                                                                                            |
| `sent`      | Your provider accepted the email. This doesn't mean it has reached the inbox.                                                                      |
| `sandboxed` | Brevo's sandbox accepted the email; nothing was delivered                                                                                          |
| `skipped`   | Not sent: the condition was false (`condition_false`), or a test render's connection has `test_mode: "skip"` (`test_mode`)                         |
| `failed`    | Not sent; `error` says why                                                                                                                         |
| `unknown`   | Your provider may have accepted the email, for example because the connection broke after the request was sent. It is never retried automatically. |
| `canceled`  | Stopped before sending: the rule was switched off or deleted, the connection was deleted, the batch was canceled or the workspace was suspended    |

The render object lists its emails in `email`, in the order they were created:

```json
"email": [
  {
    "id": "ems_01J9ZM1X3F7R8K2C4V6B8N0P2T",
    "rule_id": "emr_01J9ZK3M7Q8V5W2X4Y6Z8A0B1E",
    "status": "sent",
    "to": ["erika.muster@customer.example"],
    "provider_message_id": "<202610011015.12345678901@smtp-relay.mailin.fr>",
    "error": null,
    "sent_at": "2026-10-01T10:15:02Z"
  }
]
```

The emails are created when the finished render is processed. A sync response therefore lists none yet, and the `render.succeeded` event shows them as they were created: `pending`, or `skipped` or `failed` straight away. `GET /v1/renders/{id}` shows their current status.

`GET /v1/email-sends/{id}` returns the whole Email Send:

```json
{
  "id": "ems_01J9ZM1X3F7R8K2C4V6B8N0P2T",
  "object": "email_send",
  "render_id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q",
  "batch_id": null,
  "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
  "rule_id": "emr_01J9ZK3M7Q8V5W2X4Y6Z8A0B1E",
  "connection_id": "emc_01J9ZK3M7Q8V5W2X4Y6Z8A0B1F",
  "status": "sent",
  "skip_reason": null,
  "mode": "live",
  "test": false,
  "sandbox": false,
  "to": ["erika.muster@customer.example"],
  "cc_count": 1,
  "bcc_count": 0,
  "subject": "Invoice R-2026-0042",
  "attachments": [
    { "file_id": "file_01J9ZM4T6W8Y0A2C4E6G8J0K2M", "filename": "invoice-R-2026-0042.pdf", "bytes": 48213, "mode": "attached" }
  ],
  "provider": "brevo",
  "provider_message_id": "<202610011015.12345678901@smtp-relay.mailin.fr>",
  "attempts": 1,
  "error": null,
  "delivery": { "delivered": 2, "deferred": 0, "bounced": 0, "blocked": 0, "complained": 0, "invalid": 0 },
  "resend_of": null,
  "created_at": "2026-10-01T10:15:01Z",
  "sent_at": "2026-10-01T10:15:02Z"
}
```

| Field                                                              | Description                                                                                                                                                     |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                                               | The email's ID (`ems_…`)                                                                                                                                        |
| `render_id`, `batch_id`, `template_id`, `rule_id`, `connection_id` | Where the email comes from                                                                                                                                      |
| `status`, `skip_reason`                                            | See the table above                                                                                                                                             |
| `mode`                                                             | `live`; for test renders the connection's `test_mode` (`sandbox`, `redirect` or `skip`); `redirect` for a rule test                                             |
| `test`                                                             | `true` for the emails of test renders                                                                                                                           |
| `sandbox`                                                          | `true` when the email went to Brevo's sandbox                                                                                                                   |
| `to`                                                               | The `to` addresses. `cc` and `bcc` appear only as counts, `cc_count` and `bcc_count`.                                                                           |
| `subject`                                                          | The rendered subject                                                                                                                                            |
| `attachments`                                                      | The files: `file_id`, `filename`, `bytes` and `mode` (`attached` or `linked`)                                                                                   |
| `provider`, `provider_message_id`                                  | The provider and its ID for the message. For SMTP it is the `Message-ID` we set; Microsoft 365 returns none.                                                    |
| `attempts`                                                         | Attempts so far                                                                                                                                                 |
| `error`                                                            | `code` and `message`. Provider errors add `provider_status` and `provider_code`; template errors add `field`, `line` and `column`.                              |
| `delivery`                                                         | Brevo's delivery events per kind: `delivered`, `deferred`, `bounced` (soft and hard bounces), `blocked`, `complained` and `invalid`. Zeros for other providers. |
| `resend_of`                                                        | The email this one repeats                                                                                                                                      |
| `created_at`, `sent_at`                                            | When the email was created, and when the provider accepted it                                                                                                   |

| Endpoint                              | Description                                                                                                                                                                                   |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/email-sends`                 | List emails, newest first. Filter by `render_id`, `batch_id`, `template_id`, `rule_id`, `connection_id`, `status` and `created_after`; page with `limit` (1 to 100, default 50) and `cursor`. |
| `GET /v1/email-sends/{id}`            | Retrieve an email                                                                                                                                                                             |
| `GET /v1/email-sends/{id}/events`     | Brevo's [delivery events](#brevo-delivery-events) of an email                                                                                                                                 |
| `GET /v1/email-sends/stats`           | Today's recipients against the [daily cap](#daily-recipient-cap), and the last 30 days by status                                                                                              |
| `POST /v1/email-sends/{id}/resend`    | [Send an email again](#resend)                                                                                                                                                                |
| `POST /v1/batches/{id}/resend-emails` | Resend a batch's failed emails                                                                                                                                                                |

Emails and their delivery events are kept for 30 days. Test keys see only the emails of test renders. If request logging is off in your workspace settings, an email's body is deleted as soon as the email is final; its addresses and subject stay.

Every message carries the header `X-Dda-Send-Id` with the email's ID, so you can find it in your provider's logs.

### Retries and failures [#retries-and-failures]

Each attempt calls your provider once, and an email is never sent twice by our retries: a call that may have reached the provider ends as `unknown` instead of being repeated.

| Outcome                                                                                                | Status                                    | Next                                    |
| ------------------------------------------------------------------------------------------------------ | ----------------------------------------- | --------------------------------------- |
| The provider accepted the email                                                                        | `sent`, or `sandboxed` in Brevo's sandbox | `email.sent`                            |
| The provider couldn't be reached, refused our IP address or applied its rate limit                     | `retrying`                                | Another attempt                         |
| The provider refused the message or the credentials, or the email can't be built                       | `failed`                                  | `email.send_failed`                     |
| The provider may have accepted it, for example because the connection broke after the request was sent | `unknown`                                 | `email.send_failed`; no automatic retry |

| Attempt | After the previous attempt |
| ------- | -------------------------- |
| 2       | 1 minute                   |
| 3       | 5 minutes                  |
| 4       | 15 minutes                 |
| 5       | 1 hour                     |
| 6       | 4 hours                    |

* A rate limit waits until the provider's limit resets, and the wait doesn't count as an attempt.
* After the sixth attempt, or when the next one would come later than 24 hours after the email was created, the email fails with the last error.
* An email that couldn't be attempted within 24 hours fails with `email_send_expired`.
* If our service stops while it hands an email to the provider, the email becomes `unknown` after 10 minutes.

### Resend [#resend]

```bash
curl -X POST https://api-eu.dynamicdocumentapi.com/v1/email-sends/ems_01J9ZM1X3F7R8K2C4V6B8N0P2T/resend \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Idempotency-Key: resend-ems_01J9ZM1X3F7R8K2C4V6B8N0P2T"
```

* `failed`, `unknown` and `sent` emails can be sent again. The answer is `201` with a new email that has the same content, recipients and files, `resend_of` set to the original and its own events.
* A resend counts toward today's daily cap. It renders nothing, so no render is billed.
* `409 email_not_resendable`: the email has another status; or it failed before it reached your provider, because of its template, recipients or files, which a copy would repeat (fix the rule or the data and render again); or its body wasn't kept because request logging is off.
* `409 email_file_expired`: a file the email attaches or links is no longer hosted.
* Before you resend an `unknown` email, search your provider's logs for its `X-Dda-Send-Id`, so the recipients don't get it twice.

For a batch, `POST /v1/batches/{id}/resend-emails` with `{"statuses": ["failed"]}` (the default) or `{"statuses": ["failed", "unknown"]}` resends the newest email of each item and rule whose status is listed. It answers with the number resent and the emails it skipped, at most 100 of them:

```json
{ "resent": 12, "skipped": [{ "send_id": "ems_01J9ZM1X3F7R8K2C4V6B8N0P2T", "code": "email_file_expired" }] }
```

## Events and webhooks [#events-and-webhooks]

| Event               | Sent when                                                                                       |
| ------------------- | ----------------------------------------------------------------------------------------------- |
| `email.sent`        | Your provider accepted an email (`sent`), or Brevo's sandbox did (`sandboxed`)                  |
| `email.send_failed` | An email ended `failed` or `unknown`, including emails that failed as soon as they were created |
| `email.delivered`   | Brevo reported the email delivered to a recipient                                               |
| `email.bounced`     | Brevo reported a soft or hard bounce, a block or an invalid address                             |
| `email.complained`  | Brevo reported that a recipient marked the email as spam                                        |

Subscribe a [webhook endpoint](/docs/webhooks) to these types. The endpoint's template filter applies, and the items of a batch send these events too. `skipped` and `canceled` emails send no event. `data.object` is the Email Send:

```json
{
  "id": "evt_01J9ZM4T6W8Y0A2C4E6G8J0K2M",
  "type": "email.send_failed",
  "created_at": "2026-10-01T10:15:02Z",
  "workspace_id": "ws_01J9ZK0P2R4T6V8X0Z2B4D6F8H",
  "region": "eu",
  "data": {
    "object": {
      "id": "ems_01J9ZM1X3F7R8K2C4V6B8N0P2T",
      "object": "email_send",
      "render_id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q",
      "rule_id": "emr_01J9ZK3M7Q8V5W2X4Y6Z8A0B1E",
      "status": "failed",
      "error": {
        "code": "email_daily_cap_reached",
        "message": "The workspace's daily limit of 15,000 email recipients is reached; it resets at 00:00 UTC."
      }
    }
  }
}
```

The Email Send above is shortened. `email.delivered`, `email.bounced` and `email.complained` also carry `data.event` with `event`, `recipient`, `reason` and `occurred_at`.

### Brevo delivery events [#brevo-delivery-events]

Only Brevo reports what happens after it accepts an email. When you create a Brevo connection or change its key, a transactional webhook is added to your Brevo account for this purpose. The connection's `events` shows whether it is `registered` or `failed`; a failed registration doesn't affect sending and is retried daily. Mail that you send through the same Brevo account from elsewhere is ignored and not stored.

`GET /v1/email-sends/{id}/events` lists an email's events, oldest first:

```json
{
  "object": "list",
  "data": [
    { "event": "delivered", "recipient": "erika.muster@customer.example", "reason": "", "occurred_at": "2026-10-01T10:15:04Z" }
  ],
  "has_more": false,
  "next_cursor": null
}
```

| `event`                                            | Webhook event      |
| -------------------------------------------------- | ------------------ |
| `delivered`                                        | `email.delivered`  |
| `deferred`                                         | none               |
| `soft_bounce`, `hard_bounce`, `blocked`, `invalid` | `email.bounced`    |
| `spam`                                             | `email.complained` |

`reason` is Brevo's reason for bounces and blocks, and may be empty. The email's `delivery` counts the events. The other providers don't report delivery events, so their emails end at `sent`.

## Limits [#limits]

| Limit                                 | Value                                                                                                                                     |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Connections per workspace             | 10                                                                                                                                        |
| Rules per template, and so per render | 5                                                                                                                                         |
| Recipients per email                  | 50, `to`, `cc` and `bcc` together, without duplicates                                                                                     |
| Recipients per day                    | The [daily recipient cap](#daily-recipient-cap)                                                                                           |
| Rule sources                          | 16 KiB per field; `html` 256 KiB; Brevo template `params` 64 KiB                                                                          |
| Rendered fields                       | `subject`, `from_name` and `reply_to`: one line of up to 255 characters; `html` 512 KiB; `text` 256 KiB; `to`, `cc` and `bcc` 16 KiB each |
| Files attached per email              | The connection's attachment budget, see [Providers](#providers)                                                                           |
| Sending rate                          | `max_per_second` per connection: 1 to 100, default 10                                                                                     |
| Attempts per email                    | 6, within 24 hours                                                                                                                        |
| Download links                        | Valid until the file's retention ends, at most 7 days                                                                                     |
| Email history                         | 30 days                                                                                                                                   |

### Daily recipient cap [#daily-recipient-cap]

A workspace can email a limited number of recipients per day. The cap equals the number of renders your plan includes per month:

| Plan          | Recipients per day                  |
| ------------- | ----------------------------------- |
| Free, Starter | none: email delivery isn't included |
| Growth        | 15,000                              |
| Pro           | 50,000                              |
| Scale         | 200,000                             |
| Enterprise    | 1,000,000                           |

* The cap counts the recipients of live emails: `to`, `cc` and `bcc`, after duplicates are removed.
* An email is counted once, at its first attempt. A resend is a new email and counts again.
* Emails of test renders and rule tests, and skipped emails, don't count.
* The count runs per UTC day and region, and resets at 00:00 UTC.
* An email that would take the count over the cap fails with `email_daily_cap_reached`, without contacting your provider. Resend it after the reset.
* Owners and admins get an email when the cap is reached, at most once a day.
* A render keeps the cap it was submitted with, unless the current cap is higher.

`GET /v1/email-sends/stats` shows today's count, and the dashboard's **Email** page shows it too:

```json
{
  "object": "email_stats",
  "today": { "recipients": 1204, "limit": 15000, "resets_at": "2026-10-02T00:00:00Z" },
  "last_30_days": { "sent": 18423, "sandboxed": 41, "skipped": 305, "failed": 12, "unknown": 1, "canceled": 0 }
}
```

> **Why there is a cap**
>
> Anyone who can render a template with email rules can choose its recipients through the data. The cap limits how many addresses a leaked API key could reach. Keep keys secret, and restrict them to the templates they need (see [Authentication](/docs/authentication)).

## Zero retention and hosted files [#zero-retention-and-hosted-files]

Attaching and linking read the render's hosted file:

* **Zero-retention renders** (`delivery.retention: "none"`, or the workspace default) keep no file, so they never send email. A `delivery.email` list fails with `422 email_needs_hosted_file`; without a list, the render carries the warning `email_skipped_zero_retention` when the template has enabled rules. In a zero-retention workspace, rule tests fail with `422 email_needs_hosted_file` too.
* **Renders with `hosted: false`** keep their hosted copy only until their uploads and emails are done, so their emails can't link files: `attach_or_link` attaches or fails, and `link` fails the email with `email_needs_hosted_file`. While one of their emails is `pending`, `sending`, `retrying`, `failed` or `unknown`, the copy stays, so the email can still be resent. See [Your own storage](/docs/storage).
* **Expired files** can't be attached or linked. An attempt after the file's retention ended fails with `email_needs_hosted_file`, and a resend with `409 email_file_expired`.

## Errors and warnings [#errors-and-warnings]

Request errors:

| Status | Code                       | When                                                                                                                                                                                                                                             |
| ------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `validation_error`         | A connection or rule field is invalid, the workspace already has 10 connections or the template 5 rules, a `delivery.email` list is invalid, or a test address isn't the verified address of a workspace member. `errors[]` points to the field. |
| `402`  | `plan_feature_unavailable` | The plan doesn't include email delivery: creating, changing or testing connections, creating or changing rules other than switching them off, previews, rule tests, resends and `delivery.email` lists                                           |
| `403`  | `feature_not_enabled`      | Email delivery isn't enabled for your workspace yet: creating, changing or testing connections, and creating or changing rules other than switching them off. This check comes before the plan's `402`.                                          |
| `409`  | `email_connection_in_use`  | The connection you deleted is used by rules; `rules` lists them with `id`, `name`, `template_id` and `template_name`                                                                                                                             |
| `409`  | `email_not_resendable`     | The email can't be resent, see [Resend](#resend)                                                                                                                                                                                                 |
| `409`  | `email_file_expired`       | A file of the email you resent is no longer hosted                                                                                                                                                                                               |
| `422`  | `email_template_error`     | A rule field doesn't compile; `errors[]` gives `path`, `line` and `column`                                                                                                                                                                       |
| `422`  | `email_needs_hosted_file`  | A `delivery.email` list on a zero-retention render, or a rule test in a zero-retention workspace                                                                                                                                                 |

Render warnings:

| Code                           | Meaning                                                                                                    |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `email_skipped_plan`           | The template has enabled email rules, but the plan doesn't include email delivery. No email was sent.      |
| `email_skipped_zero_retention` | The template has enabled email rules, but zero-retention renders keep no file to email. No email was sent. |

Saving a rule can return the warning `email_sender_unverified`, see [Senders and verification](#senders-and-verification).

An email's `error.code`:

| Code                                | Status                    | Meaning                                                                              |
| ----------------------------------- | ------------------------- | ------------------------------------------------------------------------------------ |
| `email_template_error`              | `failed`                  | A rule field failed to render; `field`, `line` and `column` say where                |
| `email_too_large`                   | `failed`                  | A rendered field is over its limit                                                   |
| `email_recipient_missing`           | `failed`                  | `to` rendered no address                                                             |
| `email_address_invalid`             | `failed`                  | A recipient, the sender or the reply-to address isn't a valid address                |
| `email_too_many_recipients`         | `failed`                  | More than 50 recipients                                                              |
| `email_attachment_too_large`        | `failed`                  | With `attach`, the files don't fit the attachment budget                             |
| `email_attachment_type_not_allowed` | `failed`                  | With `attach`, the provider refuses a file type                                      |
| `email_needs_hosted_file`           | `failed`                  | A file can't be linked or attached because no hosted copy exists                     |
| `email_provider_rejected`           | `failed`                  | The provider refused the message, for example its sender, a recipient or the content |
| `email_provider_auth_failed`        | `failed`                  | The provider refused the credentials. Update them and test the connection.           |
| `email_provider_unavailable`        | `retrying`, then `failed` | The provider couldn't be reached                                                     |
| `email_ip_not_authorized`           | `retrying`, then `failed` | The provider refused our IP address. [Authorise it](#authorise-our-ip-addresses).    |
| `email_rate_limited`                | `retrying`, then `failed` | The provider's rate limit or quota                                                   |
| `email_outcome_unknown`             | `unknown`                 | The provider may have accepted the email; check its logs before you resend           |
| `email_daily_cap_reached`           | `failed`                  | The [daily recipient cap](#daily-recipient-cap) is reached; resend after 00:00 UTC   |
| `email_disabled`                    | `failed`                  | Sending email is switched off for your workspace. Contact support.                   |
| `email_credentials_removed`         | `failed`                  | The connection's credentials were deleted after a downgrade. Enter them again.       |
| `email_send_expired`                | `failed`                  | The email couldn't be attempted within 24 hours                                      |
| `internal_error`                    | `failed`                  | The email couldn't be prepared on our side                                           |

See [Errors](/docs/errors) for every other code.

## Plans and billing [#plans-and-billing]

Email delivery is included in the Growth, Pro, Scale and Enterprise plans.

Emails aren't billed: there is no charge per email or per recipient, and your provider bills you for the emails under your own plan with them. The render is billed as usual, whether it sends emails or not (see [Plans and limits](/docs/plans-and-limits)). Test renders, previews and rule tests are free, and a resend uses no render.

On Free and Starter, and after a downgrade to them:

* Connections and rules are kept, read-only. Reading them, deleting them and switching rules off keep working; everything else fails with `402 plan_feature_unavailable`.
* New renders skip the template's rules with the warning `email_skipped_plan`, and a `delivery.email` list fails with `402`.
* Renders submitted before the downgrade still send their emails.
* The credentials of every connection are deleted 30 days after the downgrade, and `credentials_delete_at` shows the date. Owners and admins get an email at the downgrade and 7 days before the deletion. Moving back to Growth or higher before that date keeps them.
* After the deletion, `credentials.configured` is `false` and the connection is `failing` with `email_credentials_removed`. Its emails fail with the same code until you enter the credentials again.

Before you change plans, the dashboard lists the email rules that would stop sending.

## Related pages [#related-pages]

* [Renders](/docs/renders) for `delivery`, the render object and zero-retention delivery
* [Webhooks](/docs/webhooks) to receive the `email.*` events and verify their signatures
* [Template language](/docs/template-language) for the syntax of rule fields and conditions
* [Authentication](/docs/authentication) for test keys, scopes and template allowlists
* [Batches](/docs/batches) for one email per item
* [Your own storage](/docs/storage) for `hosted: false` and storage destinations
* [Plans and limits](/docs/plans-and-limits) for what each plan includes
* [Errors](/docs/errors) for every other error code
* [API reference](/docs/api-reference) for the full schemas
