https://api.telnyx.com/v2 and an Authorization header with the Bearer authentication scheme.
Concept mapping
Every ESP exposes the same core primitives under different names. Here’s how they map to Telnyx:There is no
POST /v2/suppressions endpoint on Telnyx. Suppressions live under email blocks — POST /v2/email_blocks for a single manual block, and POST /v2/email_blocks/import for a bulk CSV migration from your old provider. See Suppressions & Unsubscribes.Event-type mapping
Telnyx publishes an event for each delivery-lifecycle transition. If you already handle ESP events, this is the mapping you need:
Several Telnyx behaviors differ from most ESPs and are worth handling explicitly:
email.queuedandemail.sentare separate stages.email.queuedfires when the API accepts and persists the message;email.sentfires later, when the message is successfully injected into the outbound MTA. Most ESPs don’t split these. SendGrid, for example, has no event equivalent toemail.sent— its earliest delivery event isprocessed(“accepted and can deliver the message”), which is closer to Telnyx’semail.queued(acceptance) than toemail.sent(MTA injection). Treatemail.queuedas acceptance only: a message can sit inqueuedand still fail before it is ever sent.email.failedonly applies to an accepted message. A request that fails validation is rejected synchronously with422and never creates a message, so it produces no lifecycle webhook at all. Handle input errors from the HTTP response; useemail.failedonly for failures after acceptance.email.bouncedcovers four underlying causes. Hard bounces, retry expiration, administrative bounces, and out-of-band bounces all surface asemail.bounced, distinguished by abounce_categoryofpermanent,transient,admin, oroob. If your current code branches on separate bounce and drop events, collapse that logic ontoemail.bounced+bounce_category.email.deferredis not terminal. A deferred message is still in flight and will retry. Don’t treat it as a failure or trigger a resend.
If your SendGrid integration keys off
processed as “we sent it,” note that the equivalent Telnyx signal is email.queued, not email.sent. Subscribe to email.sent as well if you want to know when the message actually reached the outbound MTA — SendGrid gives you no such event today.Telnyx additionally has an
injection_timeout recipient status for the ambiguous case where the MTA handoff timed out and Telnyx cannot yet tell whether the message was accepted. Do not resend on this status — retrying risks a duplicate. If the MTA did accept the message, a later callback reconciles the recipient from injection_timeout to sent.GET /v2/email_events) uses bare event names — delivered, bounced, opened — while webhook subscriptions use the email.-prefixed types above. Filter accordingly: ?event_type=delivered,bounced.
Migrating from SendGrid
SendGrid has the largest install base, so here it is in detail. Notes for Mailgun, SES, and Postmark follow each section.API concepts
Sending a single email
SendGrid nests recipients underpersonalizations and bodies under content. Telnyx flattens both:
to,cc, andbccare top-level arrays. Each item is either a plain email string or an{email, name}object — both forms work.fromaccepts a plain string or{email, name}.- Bodies are
html_bodyandtext_body, not acontentarray. - A successful send returns
202 Acceptedwith"status": "queued"— not200. Update any response-code assertions. html_body,text_body,headers,tags, andmetadataare write-only — they are not echoed back in responses.
from/to/html/text form fields with the JSON payload above; o:tag becomes tags, and v:my-var custom variables become metadata.
Amazon SES: Destination.ToAddresses → to, Message.Body.Html.Data → html_body, Message.Body.Text.Data → text_body.
Postmark: From/To/HtmlBody/TextBody map almost one to one; lowercase the field names and wrap to in an array.
Idempotent retries
Telnyx supports optional idempotency on send via anIdempotency-Key HTTP header, so a retried request doesn’t produce a duplicate email:
curl
- Generate a unique UUID v4 per logical request and reuse it only when retrying that same operation with the same body.
- Keys are retained for 24 hours, and only successful responses are replayed. A replayed response carries the
Idempotent-Replayed: trueheader; the header is omitted for first-time requests and for error responses. - Reusing a key with a different request body returns
422with code10027. - Sending the same key while the original request is still processing returns
409with code10036. - An empty, duplicate, malformed, or overlong key returns
400with code10015. - Idempotency is enforced at the Telnyx Edge and fails closed — if that protection is unavailable for a keyed request, you get
503with code10016. Retry with the same key.
idempotency_key is not a request-body field. A body field by that name is ignored — only the Idempotency-Key HTTP header takes effect. A request sent without the header is not deduplicated, so an unkeyed retry after a timeout or a 5xx can produce a duplicate email.Batch sending
SendGrid’spersonalizations array sends one request with many recipient blocks. Telnyx uses an explicit messages array where each item is a complete send payload:
- The batch limit is 50 messages per request. An empty array or more than 50 items returns
400. If you currently send 1,000-recipient personalization blocks, chunk them into groups of 50. 202means every message succeeded.207means partial (or total) failure — the response carries adataarray of successes and anerrorsarray of failures.- Batch item errors use a different shape from single-send errors: each entry has
index(zero-based position in your request array),code, andmessage— notemessage, notdetail. Match failures back to inputs byindex. - An
Idempotency-Keyheader applies to the whole batch request, not to individual messages — there are no per-message keys insidemessages. Reuse the key only when retrying the identical batch body. On a partial (207) result, do not replay the original batch: build a new request containing only the failedindexentries, with a new key.
Webhooks
The biggest structural difference: SendGrid’s Event Webhook is configured once per account, and Telnyx webhooks are scoped to a sending domain. If you send from three domains and want events from all three, create three subscriptions.curl
eventsis a required allowlist with at least one entry — there is no subscribe-to-everything default. Events whose type isn’t listed are never delivered to that URL.- Telnyx signs deliveries with Ed25519, not the HMAC scheme SendGrid uses. Verify the
telnyx-signature-ed25519andtelnyx-timestampheaders against the raw request body. Your existing SendGrid verification code will not carry over — see Webhooks & Events for verified examples in Node, Python, and Go. - SendGrid posts a JSON array of events per request. Telnyx posts a single event per request under
data, with the event type atdata.event_typeand the message snapshot atdata.payload.
curl
meta.page_cursor and pass it as page_cursor on the next request. The field is omitted entirely — not null — once you reach the end of the results.
Templates
SendGrid uses Handlebars ({{name}} with {{#if}} blocks); Telnyx uses Liquid. Simple variable interpolation looks identical, but conditionals, loops, and filters need rewriting.
name, subject, html_body, text_body, and an optional variables array; when you omit variables, Telnyx extracts them from the subject and bodies automatically. Creation returns 201.
Liquid syntax is validated at create time, so a malformed template fails immediately with 422 rather than at send time.
Preview a template without sending:
curl
template_id and template_variables. When you use a template, subject on the send is optional — the template’s subject renders instead.
Three different fields are easy to confuse when porting templates. They are not interchangeable:
The render and send paths do not behave the same way for a malformed value.
POST /v2/email_templates/{id}/render tolerates a non-object and previews with {}; POST /v2/email_messages rejects it with 422. A mis-serialized value — for example sending "{\"first_name\":\"Ada\"}" as a JSON string rather than an object — will preview fine but fail at send time. Send an object, not a stringified object.Suppressions
Migrate your existing list in bulk rather than replaying it address by address.POST /v2/email_blocks/import accepts a CSV and auto-detects the provider from the header row — SendGrid, Mailgun, and Amazon SES exports are recognized natively:
curl
type column onto the Telnyx reason taxonomy: bounces → hard_bounce, spam_reports → spam_complaint, unsubscribes → unsubscribe, invalid_emails → invalid, and blocks → manual_block.
The endpoint returns 202 with an async job; poll GET /v2/email_blocks/import/{id} until status is completed or failed. Limits: 25 MiB decoded and 250,000 rows.
Import your suppression list before your first production send. Sending to addresses that bounced or complained at your previous ESP is the fastest way to damage a new domain’s reputation.
Migration checklist
1
Get a Telnyx API key
Create a key in the Mission Control Portal under Auth → API Keys. Send it as
Authorization: Bearer YOUR_API_KEY on every request. If you plan to bypass overridable suppressions with ignore_suppression, the key needs the email:override scope (granted by full_access or email:admin).2
Register and verify your sending domain
curl
id, fetch the generated records with GET /v2/email_domains/{domain_id}/dns_records, publish them at your DNS provider, then trigger validation:curl
3
Import your suppression list
Import your ESP’s bounce, complaint, and unsubscribe exports via
POST /v2/email_blocks/import before sending anything. If you use subscription groups, recreate them with POST /v2/email_unsubscribe_groups and add members with POST /v2/email_unsubscribe_groups/{id}/suppressions, then pass the matching group_id on sends.4
Send a test email
curl
202 with "status": "queued". Save the id and confirm the timeline with GET /v2/email_messages/{id}/events.5
Set up event tracking
Create a webhook per sending domain with an explicit
events allowlist, and implement Ed25519 signature verification against the raw request body. Or skip webhooks and poll GET /v2/email_events. Use GET /v2/email_events/stats for the aggregate counts and rates that replace your ESP’s stats dashboard.6
Port your templates
Recreate each template with
POST /v2/email_templates, rewriting Handlebars or Mustachio syntax to Liquid. Verify the output with POST /v2/email_templates/{id}/render before switching production traffic, then map your old template IDs to the new Telnyx UUIDs in your application config.7
Cut over batch sending
Chunk any bulk sends into batches of 50 or fewer, switch to the
messages array shape, and handle 207 partial-success responses by matching the errors[].index back to your input array.8
Warm up and ramp
A new sending domain has no reputation history at the mailbox providers, even if your old one did. Ramp volume gradually rather than moving 100% of traffic on day one, and watch bounce and complaint rates via
GET /v2/email_events/stats. See Deliverability and Domain Warm-up.Differences to plan for
Running both providers in parallel during cutover is the safest path: keep your old ESP’s DNS records published, route a small percentage of traffic to Telnyx, compare delivery and bounce rates via
GET /v2/email_events/stats, then ramp. Full endpoint details are in the API reference, and error codes are catalogued in Error Codes.