> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending Email with the Telnyx API

> Complete guide to sending email with the Telnyx Email API — full payload options, HTML and attachments, scheduling, idempotency, tracking, batch sending, and error handling.

The Telnyx Email API is a full email platform: transactional and marketing sending with attachments, templates, scheduling, and batch operations — plus open/click/unsubscribe tracking, automatic suppressions, group-scoped unsubscribe management, and a complete event lifecycle from queued to delivered.

This guide covers the sending surface in depth. For receiving email, managing inboxes, or tracking events, see the [Email API Overview](/docs/messaging/email/overview).

All requests use the production base URL `https://api.telnyx.com/v2` and an `Authorization: Bearer YOUR_API_KEY` header.

## Send a message

Send an email with `POST /email_messages`. The only required fields are `from` and `to`; `subject` is required unless you're sending with `template_id`.

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "from": {
      "email": "sender@example.com",
      "name": "Telnyx Notifications"
    },
    "to": [
      {
        "email": "recipient@example.com",
        "name": "Ada Lovelace"
      }
    ],
    "subject": "Welcome",
    "html_body": "<h1>Welcome, Ada!</h1><p>Thanks for signing up.</p>",
    "text_body": "Welcome, Ada! Thanks for signing up."
  }'
```

### Address fields

| Field       | Type             | Notes                                                                   |
| ----------- | ---------------- | ----------------------------------------------------------------------- |
| `from`      | string \| object | A plain email string or `{email, name}`.                                |
| `from_name` | string           | Optional display name when `from` is a string; overrides `from.name`.   |
| `to`        | array (min 1)    | Each item is a string or `{email, name}`.                               |
| `cc`        | array            | Same item shape as `to`.                                                |
| `bcc`       | array            | Same item shape as `to`.                                                |
| `reply_to`  | string \| object | Reply-to address. When provided as an object, only the email is stored. |

### Body, headers, and attachments

| Field         | Type                                   | Notes                                                                                            |
| ------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `subject`     | string                                 | Required unless `template_id` is supplied.                                                       |
| `html_body`   | string                                 | HTML body. Omitted from create and list responses; returned by `GET /email_messages/{id}`.       |
| `text_body`   | string                                 | Plain-text body. Omitted from create and list responses; returned by `GET /email_messages/{id}`. |
| `headers`     | object of string keys to string values | Custom headers. Write-only — not returned in responses.                                          |
| `tags`        | array of strings                       | Tags for categorization. Write-only.                                                             |
| `metadata`    | object                                 | Custom metadata. Write-only.                                                                     |
| `attachments` | array                                  | See below.                                                                                       |

Send an attachment by base64-encoding its content:

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "from": "sender@example.com",
    "to": ["recipient@example.com"],
    "subject": "Invoice attached",
    "text_body": "Your invoice is attached.",
    "attachments": [
      {
        "filename": "invoice.pdf",
        "content_type": "application/pdf",
        "content": "JVBERi0xLjQK..."
      }
    ]
  }'
```

**Request** `attachments[]` fields:

| Field          | Default                    | Notes                                                                                   |
| -------------- | -------------------------- | --------------------------------------------------------------------------------------- |
| `filename`     | `"attachment"`             | Attachment file name.                                                                   |
| `content_type` | `application/octet-stream` | MIME type.                                                                              |
| `content`      | `""`                       | Base64-encoded file content.                                                            |
| `disposition`  | `"attachment"`             | Use `inline` for images referenced from the HTML body.                                  |
| `content_id`   | `null`                     | Content-ID for an inline attachment, referenced as `cid:<content_id>` in the HTML body. |

**Response** `attachments[]` returns `filename`, `content_type`, `url`, `sha256`, `size_bytes`, `disposition`, and `content_id`. The base64 `content` you submitted is never returned — fetch the stored file from `url` instead.

## Templates

Send with a stored template instead of inline bodies. Provide `template_id` and optional `template_variables` for Liquid rendering. When you use a template, `subject` is optional — the template's subject is rendered; if the template has no subject or renders empty, the request returns `400`.

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "from": "sender@example.com",
    "to": ["recipient@example.com"],
    "template_id": "7a7c1a2b-1111-4c72-8c21-2bbf3d40c123",
    "template_variables": {
      "first_name": "Ada"
    }
  }'
```

<Callout type="warning">
  Non-object `template_variables` values may cause a 422 validation error on message creation. Pass an object.
</Callout>

## Schedule a send

Set `scheduled_at` to an ISO 8601 timestamp in the future to schedule the message. The response returns `202` with `"status": "scheduled"` and a `scheduled_at` field.

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ***" \
  -d '{
    "from": "sender@example.com",
    "to": ["recipient@example.com"],
    "subject": "Scheduled reminder",
    "text_body": "This was scheduled.",
    "scheduled_at": "2026-08-01T09:00:00Z"
  }'
```

<Callout type="info">
  Invalid or past `scheduled_at` timestamps are silently ignored and the email is sent immediately.
</Callout>

<Callout type="warning">
  `send_at` is a deprecated alias for `scheduled_at`. It is still accepted on requests for backward compatibility, but responses always return `scheduled_at`. When both are supplied, `scheduled_at` wins. Use `scheduled_at` in new integrations.
</Callout>

Cancel a scheduled message before it sends with `DELETE /email_messages/{email_id}/schedule`:

```bash curl theme={null}
curl -X DELETE https://api.telnyx.com/v2/email_messages/{email_id}/schedule \
  -H "Authorization: Bearer ***"
```

A successful cancel returns `200` with the message in `"status": "cancelled"`. The `scheduled_at` value persists on the record even after cancellation.

## Idempotency

Pass an optional `Idempotency-Key` HTTP header to safely retry a send without creating duplicates. Generate a unique UUID v4 for each logical request, then reuse the same key only when retrying that operation with the same request body. Keys are retained for up to 24 hours, and only successful responses are replayed. Do not include sensitive data in the key.

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9326" \
  -d '{
    "from": "sender@example.com",
    "to": ["recipient@example.com"],
    "subject": "Order confirmation",
    "text_body": "Order #12345 confirmed."
  }'
```

The first successful request returns its normal status and response body. A retry with the same key and request returns the stored status and body and adds this response header:

```http theme={null}
Idempotent-Replayed: true
```

The header is omitted for first-time requests and error responses. Reusing a key with a different request returns `422` with code `10027`; sending the same key while the original request is still running returns `409` with code `10036`. Empty, duplicate, malformed, or overlong key headers return `400` with code `10015`. If Edge cannot provide idempotency protection for a keyed request, it fails closed with `503` and code `10016`.

<Callout type="warning">
  `idempotency_key` is not a request-body field. Put the key only in the `Idempotency-Key` HTTP header.
</Callout>

## Response

A successful send returns `202 Accepted` with the message in `data`:

```json theme={null}
{
  "data": {
    "record_type": "email_message",
    "id": "b0c7e8cb-6227-4c74-9f32-c7f80c30934b",
    "status": "queued",
    "from": { "email": "sender@example.com" },
    "to": [{ "email": "recipient@example.com" }],
    "subject": "Welcome",
    "created_at": "2026-07-06T12:00:00.000000Z"
  }
}
```

Key response fields:

* `status` — the **message** lifecycle status (see below).
* `id` — the message UUID. Use it with `GET /email_messages/{id}`, `GET /email_messages/{id}/events`, and `DELETE /email_messages/{id}/schedule`.
* `created_at` — when the message was created.
* `scheduled_at` — present when a future send was scheduled.
* `recipient_statuses` — a map of per-recipient status to count, present once recipient rows exist.
* `sandbox` — present when the message was created in sandbox mode.

<Callout type="info">
  Message status, recipient status, and event types are **three different taxonomies**. A message is a request; each recipient has its own delivery outcome; engagement signals such as opens and clicks are events, never message statuses.
</Callout>

#### Message status

The parent message status is request-lifecycle only — it does not describe delivery.

| Status      | Meaning                                                                                                           |
| ----------- | ----------------------------------------------------------------------------------------------------------------- |
| `queued`    | Accepted and waiting to be processed.                                                                             |
| `scheduled` | Held for a future `scheduled_at` time.                                                                            |
| `sending`   | Being injected into the delivery pipeline.                                                                        |
| `completed` | Every recipient has reached a terminal state (not necessarily success).                                           |
| `cancelled` | A scheduled message was cancelled before sending.                                                                 |
| `sandbox`   | Created in sandbox mode; never actually delivered.                                                                |
| `failed`    | The message failed before delivery injection, such as when a scheduled send exceeds the daily limit at fire time. |

#### Recipient status

Each recipient carries its own delivery outcome. These are surfaced in `recipient_statuses` and through the recipients endpoints. The Events API uses a separate event-type taxonomy — several recipient statuses map lossily to a different stored event type (for example, `expired`, `gw_reject`, and `injection_timeout` all produce a `failed` stored event). For exact recipient status, use `recipient_statuses` or the recipient endpoints.

| Status              | Terminal | Meaning                                                                   |
| ------------------- | -------- | ------------------------------------------------------------------------- |
| `queued`            | No       | Accepted, not yet injected.                                               |
| `sending`           | No       | Injection in progress.                                                    |
| `sent`              | No       | Accepted by the delivery gateway.                                         |
| `deferred`          | No       | Temporary failure (4xx); will be retried.                                 |
| `delivered`         | Yes      | The receiving mail server accepted the message.                           |
| `bounced`           | Yes      | Permanently rejected by the receiving server (5xx or out-of-band bounce). |
| `failed`            | Yes      | Terminal system or operator-caused non-delivery.                          |
| `expired`           | Yes      | Retries were abandoned after the maximum queue age.                       |
| `gw_reject`         | Yes      | The delivery gateway refused the recipient before queueing.               |
| `cancelled`         | Yes      | Cancelled before delivery.                                                |
| `injection_timeout` | Yes      | The injection attempt timed out with an ambiguous result.                 |

#### Event types

Events record what happened and when. Query them with `GET /email_messages/{id}/events` or `GET /email_events`.

| Event type                                                       | Meaning                                          |
| ---------------------------------------------------------------- | ------------------------------------------------ |
| `queued`, `scheduled`, `sending`, `sent`, `cancelled`, `sandbox` | Lifecycle progress.                              |
| `delivered`, `deferred`, `bounced`, `failed`, `rejected`         | Delivery outcomes.                               |
| `opened`, `clicked`, `unsubscribed`, `complained`                | Engagement and feedback signals.                 |
| `daily_limit_exceeded`                                           | The account's daily send limit blocked the send. |

### Suppressed recipients

When one or more recipients are suppressed at send time, the `202` response includes a top-level `suppressed` array describing each suppressed recipient. The message is still created for the non-suppressed recipients (if any).

```json theme={null}
{
  "data": {
    "record_type": "email_message",
    "id": "...",
    "status": "queued"
  },
  "suppressed": [
    {
      "to": "suppressed@example.com",
      "reason": "hard_bounce",
      "scope": "global",
      "override_allowed": false
    }
  ]
}
```

If **all** recipients are suppressed, the request returns `422` with a `recipient_suppressed` error and the `suppressed` array (see [Errors](#errors)).

## Tracking

Open, click, and unsubscribe tracking are configured through the `tracking` object on an email domain:

```bash curl theme={null}
curl -X PATCH https://api.telnyx.com/v2/email_domains/{domain_id} \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ***" \
  -d '{
    "tracking": {
      "open_tracking": true,
      "click_tracking": true,
      "unsubscribe_tracking": true
    }
  }'
```

<Callout type="warning">
  **Tracking is scoped to the sending profile, not to an individual domain.** The domain endpoint is the API surface, but the setting is stored on your account's default sending profile. Every domain that shares that profile shares one tracking configuration, so updating tracking through one domain changes it for all of them. Registering a domain with `tracking` in `POST /email_domains` therefore only works for the first domain on the account — once a profile exists, the request is rejected with a validation error and you must use `PATCH /email_domains/{id}` instead.
</Callout>

Defaults for a new domain: `open_tracking: false`, `click_tracking: false`, `unsubscribe_tracking: true`. Open and click tracking are opt-in; one-click unsubscribe is on by default because Gmail and Yahoo bulk-sender rules require RFC 8058 unsubscribe support — disable it only if you handle unsubscribes yourself.

The tracking endpoints themselves are public (no auth) because email clients and browsers hit them directly.

### Open tracking

When `open_tracking` is enabled, messages with an HTML body get a 1×1 transparent tracking pixel injected before the closing `</body>` tag (or appended to the HTML when there is no `</body>`). When the recipient's email client loads the image, Telnyx records an `email.opened` event.

### Click tracking

When `click_tracking` is enabled, Telnyx rewrites `href` links in the HTML body to point through a tracking redirect. When a recipient clicks, Telnyx records an `email.clicked` event and redirects (HTTP 302) to the original URL. Links already pointing to the tracking service are not double-wrapped.

### Unsubscribe tracking

When `unsubscribe_tracking` is enabled (the default), Telnyx adds `List-Unsubscribe` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058) headers to outgoing messages, with a signed, unguessable unsubscribe URL. A recipient can unsubscribe via a link (`GET`) or one-click (`POST`, sent automatically by supporting email clients). On unsubscribe, Telnyx records an `email.unsubscribed` event and creates a suppression for the recipient.

### Tracking events

Tracking events — `email.opened`, `email.clicked`, and `email.unsubscribed` — are stored as message events and delivered to configured webhooks. Query them per message:

```bash curl theme={null}
curl https://api.telnyx.com/v2/email_messages/{id}/events \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Or list events across the account:

```bash curl theme={null}
curl https://api.telnyx.com/v2/email_events \
  -H "Authorization: Bearer YOUR_API_KEY"
```

<Callout type="info">
  The `email.opened` event payload includes a `first_open` boolean that is `true` for the first open and `false` for subsequent opens. The `email.clicked` payload includes the clicked `url`, and `email.unsubscribed` includes the `method` (`link` or `one_click`).
</Callout>

A dedicated deep-dive on email webhooks is coming soon. Until then, event data is available via the endpoints above.

## Errors

API errors return structured JSON. See the full [Error Codes reference](/docs/messaging/email/error-codes) for all codes and troubleshooting. The standard error envelope:

```json theme={null}
{
  "errors": [
    {
      "code": "10015",
      "title": "Bad Request",
      "detail": "subject is required when not using a template"
    }
  ]
}
```

### Common error codes

| HTTP | Code                   | Meaning                                                                                                                                                                                                                                                                                                |
| ---- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400  | `10015`                | Validation failed — missing or invalid fields.                                                                                                                                                                                                                                                         |
| 400  | `10015`                | Invalid `Idempotency-Key` header — empty, duplicate, malformed, or overlong.                                                                                                                                                                                                                           |
| 401  | `10006`                | Authentication failed — check your API key.                                                                                                                                                                                                                                                            |
| 403  | `10007`                | Forbidden — the Email API's general sending-policy error. Covers an unverified, degraded, or suspended sending domain, a domain with no active DKIM key, a sender that doesn't match the domain's sending profile, and trial-account recipient restrictions. Inspect `detail` for the specific reason. |
| 404  | `10001`                | Not found — the message, domain, or resource doesn't exist.                                                                                                                                                                                                                                            |
| 409  | `10036`                | A request with the same idempotency key is still being processed.                                                                                                                                                                                                                                      |
| 413  | `10015`                | Request body exceeds 8,000,000 bytes.                                                                                                                                                                                                                                                                  |
| 422  | `10015`                | Validation error (changeset).                                                                                                                                                                                                                                                                          |
| 422  | `10027`                | The idempotency key was already used for a different request.                                                                                                                                                                                                                                          |
| 422  | `recipient_suppressed` | All recipients suppressed. Returns a top-level `suppressed` array.                                                                                                                                                                                                                                     |
| 429  | `reputation_suspended` | Sending suspended — the sending domain's reputation band is `poor`.                                                                                                                                                                                                                                    |
| 503  | `10016`                | Service unavailable — an upstream dependency or Edge idempotency protection is unavailable.                                                                                                                                                                                                            |

`recipient_suppressed` (422) uses a non-standard envelope with the `suppressed` array alongside `errors`:

```json theme={null}
{
  "errors": [
    {
      "code": "recipient_suppressed",
      "title": "Recipient Suppressed",
      "detail": "All recipients are suppressed. The email was not sent."
    }
  ],
  "suppressed": [
    {
      "to": "suppressed@example.com",
      "reason": "hard_bounce",
      "scope": "global",
      "override_allowed": false
    }
  ]
}
```

`reputation_suspended` (429) uses a string code rather than a numeric Telnyx code:

```json theme={null}
{
  "errors": [
    {
      "code": "reputation_suspended",
      "title": "Sending Suspended",
      "detail": "Sender domain reputation is too low. Sending has been suspended. Please contact support to resolve deliverability issues."
    }
  ]
}
```

## Batch sending

Send up to 50 messages in a single request with `POST /email_messages/batch`. Each item in the `messages` array is a full `CreateEmailRequest` payload.

```bash curl theme={null}
curl -X POST https://api.telnyx.com/v2/email_messages/batch \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 20dbec69-bc70-4fed-aec7-2a70af37a8a6" \
  -d '{
    "messages": [
      {
        "from": "sender@example.com",
        "to": ["recipient1@example.com"],
        "subject": "Hello 1",
        "text_body": "Message 1"
      },
      {
        "from": "sender@example.com",
        "to": ["recipient2@example.com"],
        "subject": "Hello 2",
        "text_body": "Message 2"
      }
    ]
  }'
```

The `Idempotency-Key` applies to the entire batch request. Reuse it only when retrying the same batch body; do not add per-message idempotency keys inside `messages`.

* **`207`** — all batch responses use `207 Multi-Status`. When all messages succeed, `errors` is empty and every item is in `data`. When one or more fail, the response contains a `data` array for successes (which may be empty) and an `errors` array for failures.

The `errors` array for a batch uses a per-item shape — each entry has `index` (zero-based position in the request array), `code`, and `message` (not `detail`):

```json theme={null}
{
  "data": [
    {
      "record_type": "email_message",
      "id": "11111111-1111-1111-1111-111111111111",
      "status": "queued"
    }
  ],
  "errors": [
    {
      "index": 1,
      "code": "bad_request",
      "message": "from, to, and subject are required"
    }
  ],
  "meta": {
    "total": 2,
    "succeeded": 1,
    "failed": 1
  }
}
```

Batch item error codes: `bad_request`, `unprocessable_entity`, `not_found`, `forbidden`, `service_unavailable`, `validation_error`, `recipient_suppressed`, and `reputation_suspended`. `unprocessable_entity` is returned when an individual message exceeds the size limits — the rest of the batch still processes.
