https://api.telnyx.com/v2 and an Authorization: Bearer YOUR_API_KEY header.
Send a message
Send an email withPOST /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
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. Providetemplate_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.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
Setscheduled_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 withPATCH /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.DELETE /email_messages/{email_id}/schedule:
curl
200 with the message in "status": "cancelled". The scheduled_at value persists on the record even after cancellation.
Test delivery outcomes in sandbox mode
Setsandbox_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
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 domaintest.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.
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 optionalIdempotency-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
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 returns202 Accepted with the message in data:
status— the message lifecycle status (see below).id— the message UUID. Use it withGET /email_messages/{id},GET /email_messages/{id}/events, andDELETE /email_messages/{id}/schedule.created_at— when the message was created.scheduled_at— present when a future send was scheduled.tagsandmetadata— 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-separatedkey=valuepairs. A message matches when its metadata contains all supplied pairs (containment semantics).
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 inrecipient_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 withGET /email_messages/{id}/events or GET /email_events.
Suppressed recipients
When one or more recipients are suppressed at send time, the202 response includes a top-level suppressed array describing each suppressed recipient. The message is still created for the non-suppressed recipients (if any).
422 with a recipient_suppressed error and the suppressed array (see Errors).
Tracking
Open, click, and unsubscribe tracking are configured through thetracking 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.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
Whenopen_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
Whenclick_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
Whenunsubscribe_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
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).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:
Batch sending
Send up to 1,000 messages in a single request withPOST /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
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,errorsis empty and every item is indata. When one or more items fail, the response contains adataarray for successes (which may be empty) and anerrorsarray for failures. Request-wide gates can instead reject the whole batch with 4xx or 5xx before item processing.
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):
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.