Skip to main content
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. 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.
curl

Address fields

Body, headers, and attachments

Send an attachment by base64-encoding its content:
curl
Request attachments[] fields: 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.
curl
Non-object template_variables values may cause a 422 validation error on message creation. Pass an object.
A template can also enforce its own safety controls: strict_variables: true makes a send that is missing a required variable fail with 422 naming the variable (nothing is sent), and autoescape: true HTML-escapes rendered html_body expression output. See Strict variable validation and HTML autoescaping in the Templates guide.

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.
curl
Invalid ISO 8601 or non-future scheduled_at values are rejected with HTTP 422; they are never converted into immediate sends.
In batch sends, an invalid or non-future scheduled_at does not fail the whole batch: the invalid item is reported in the 207 per-item errors array while the other items continue.
A scheduled send is not guaranteed to send. Quota and sending-policy checks run when the message fires, not when you submit it. If the account daily limit is exhausted then, the message is marked failed with a daily_limit_exceeded event. If the sender domain’s warm-up ramp rejects it, every recipient is terminalized as failed with reason sender_domain_graduation_rejected; if a fail-closed graduation dependency failure exhausts its retries, the reason is sender_domain_graduation_unavailable. Both graduation outcomes record and publish one recipient-scoped email.failed event per recipient. Because every recipient is terminal, the parent message can roll up to completed through the normal lifecycle even though nothing was sent. No 429 is returned for a scheduled send — there is no HTTP request in flight at fire time. Detect these outcomes via GET /email_events, the per-message events endpoint, or recipient-scoped email.failed webhooks. See Rate Limits & Quotas.
send_at is a deprecated alias for scheduled_at. It is still accepted on requests for backward compatibility, but responses always return scheduled_at. A non-null scheduled_at takes precedence over send_at; if scheduled_at is omitted or null, send_at is used. Use scheduled_at in new integrations.

Reschedule before send

Move a scheduled message to a new future delivery time with PATCH /email_messages/{email_id}/schedule. Only the delivery time changes — the message ID, content, recipients, tags, and metadata remain unchanged.
curl
Rescheduling emits no dedicated rescheduled event. The message’s scheduled_at value in the response is the only confirmation of the new delivery time.
Cancel a scheduled message before it sends with DELETE /email_messages/{email_id}/schedule:
curl
A successful cancel returns 200 with the message in "status": "cancelled". The scheduled_at value persists on the record even after cancellation.

Test delivery outcomes in sandbox mode

Set sandbox_mode: true to validate and accept a message without delivering it: the message is created and events are recorded, but nothing is injected into the MTA or the outbound pipeline. Sandbox messages are non-billable, consume no daily-send-limit quota, and feed no delivery-reputation signals — no impact on your billing, quota, or reputation.
curl
The response is the normal 202 with "status": "sandbox", and the message appears in GET /email_messages and the events endpoints like any other message.

Reserved test recipients

The reserved domain test.telnyx.com hosts eight deterministic test addresses. In sandbox mode, each address produces a fixed, recipient-scoped lifecycle chain — the same outcome every time: For the complaint simulation, complained is the final event, while the recipient’s persisted delivery status remains sent; complaint is an engagement/feedback signal, not a recipient delivery status. Hard-bounce and complaint simulations also exercise the normal automatic-suppression pipeline, so you can test suppression-driven flows deterministically and repeatedly. The constraints are strict:
  • Sandbox only. These deterministic chains apply only to sends with sandbox_mode: true. Non-sandbox sends to these addresses use the normal delivery path like any other recipient.
  • Exact domain. Matching is case-insensitive for the local part and domain, but the domain must be exactly test.telnyx.com — subdomains and suffixes do not match.
  • Only these addresses are simulated. In a mixed sandbox send, other recipients retain ordinary sandbox behavior: accepted, no delivery attempted, no deterministic chain.
Use these addresses to exercise your webhook handling, event polling, and failure-state UI without sending real mail — for example, subscribe to email.sent and email.bounced and send to hard-bounce@test.telnyx.com to verify your bounce path end-to-end.
Sandbox mode is available on batch sends too — set the batch-level sandbox_mode envelope field, which overrides every per-message value. See Batch sending.

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.
curl
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:
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.
idempotency_key is not a request-body field. Put the key only in the Idempotency-Key HTTP header.

Response

A successful send returns 202 Accepted with the message in data:
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.
  • tags and metadata — the values you supplied, stored on the message and returned on every message response.
  • 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.

Retrieve and filter by tags and metadata

tags and metadata are stored on the message and returned on message responses. List messages having any of a set of tags, or whose metadata contains a set of key/value pairs:
curl
curl
  • filter[tags] — comma-separated tags. Matching is exact and case-sensitive after trimming, and a message matches when it has any of the supplied tags (OR semantics).
  • filter[metadata] — a JSON object or comma-separated key=value pairs. A message matches when its metadata contains all supplied pairs (containment semantics).
Malformed filters fail closed with 400: empty tag segments, metadata pairs without =, empty keys, and non-object JSON all reject the request.
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.

Message status

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

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.

Event types

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

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).
If all recipients are suppressed, the request returns 422 with a recipient_suppressed error and the suppressed array (see Errors).

Tracking

Open, click, and unsubscribe tracking are configured through the tracking object on an email domain:
curl
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.
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:
curl
Or list events across the account:
curl
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).
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 for all codes and troubleshooting. The standard error envelope:

Common error codes

recipient_suppressed (422) uses a non-standard envelope with the suppressed array alongside errors:
reputation_suspended (429) uses a string code rather than a numeric Telnyx code:
domain_graduation_limit_exceeded (429) is returned when the sender domain is still in warm-up and the send would exceed the domain’s daily ramp allowance. The Retry-After response header carries the seconds to the domain’s midnight UTC reset — this is the only Email API 429 that sets it. The meta fields are present only when known:
The send is rejected before message creation — no message record, no recipients, no events, no billing. The account daily quota is checked first; the domain ramp applies only to sends that fit the account quota. See Rate Limits & Quotas for the ramp’s ordering, counting, and retry rules, and Error Codes for the complete reference.

Batch sending

Send up to 1,000 messages in a single request with POST /email_messages/batch. Each item in the messages array mirrors the single-send message schema — same fields and validation — except the single-send-only reply and forward threading fields (in_reply_to_message_id, reply_to_all, and forward_of_message_id), which are not accepted on batch items.
curl
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. sandbox_mode on the batch envelope applies to every message in the batch and is the authoritative value: it overwrites each message’s own sandbox_mode before processing, including the false default when the envelope omits the field. A per-message sandbox_mode: true inside a non-sandbox batch is therefore a real send — set the envelope field (not the per-message field) to run batch items in sandbox mode. A sandbox batch gets the same deterministic test-recipient outcomes, consumes no daily-send-limit quota, and is non-billable.
  • 207 — returned after request-wide admission checks pass and item processing runs. When all messages succeed, errors is empty and every item is in data. When one or more items fail, the response contains a data array for successes (which may be empty) and an errors array for failures. Request-wide gates can instead reject the whole batch with 4xx or 5xx before item processing.
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):
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. For batch rate limits, request-size budgets, retry patterns, and template-based personalization at scale, see the Batch Sending guide.