Skip to main content
Move your email sending from an existing provider — SendGrid, Mailgun, Amazon SES, Postmark, or any other ESP — to Telnyx Email. This guide maps the concepts you already use to their Telnyx equivalents, then walks the migration end to end. All requests use the production base URL 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 blocksPOST /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.queued and email.sent are separate stages. email.queued fires when the API accepts and persists the message; email.sent fires later, when the message is successfully injected into the outbound MTA. Most ESPs don’t split these. SendGrid, for example, has no event equivalent to email.sent — its earliest delivery event is processed (“accepted and can deliver the message”), which is closer to Telnyx’s email.queued (acceptance) than to email.sent (MTA injection). Treat email.queued as acceptance only: a message can sit in queued and still fail before it is ever sent.
  • email.failed only applies to an accepted message. A request that fails validation is rejected synchronously with 422 and never creates a message, so it produces no lifecycle webhook at all. Handle input errors from the HTTP response; use email.failed only for failures after acceptance.
  • email.bounced covers four underlying causes. Hard bounces, retry expiration, administrative bounces, and out-of-band bounces all surface as email.bounced, distinguished by a bounce_category of permanent, transient, admin, or oob. If your current code branches on separate bounce and drop events, collapse that logic onto email.bounced + bounce_category.
  • email.deferred is not terminal. A deferred message is still in flight and will retry. Don’t treat it as a failure or trigger a resend.
If you’re coming from SendGrid specifically, the event names map like this:
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.
The polling API (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 under personalizations and bodies under content. Telnyx flattens both:
Key differences:
  • to, cc, and bcc are top-level arrays. Each item is either a plain email string or an {email, name} object — both forms work.
  • from accepts a plain string or {email, name}.
  • Bodies are html_body and text_body, not a content array.
  • A successful send returns 202 Accepted with "status": "queued" — not 200. Update any response-code assertions.
  • html_body, text_body, headers, tags, and metadata are write-only — they are not echoed back in responses.
Mailgun: replace the multipart 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.ToAddressesto, Message.Body.Html.Datahtml_body, Message.Body.Text.Datatext_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 an Idempotency-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: true header; the header is omitted for first-time requests and for error responses.
  • Reusing a key with a different request body returns 422 with code 10027.
  • Sending the same key while the original request is still processing returns 409 with code 10036.
  • An empty, duplicate, malformed, or overlong key returns 400 with code 10015.
  • Idempotency is enforced at the Telnyx Edge and fails closed — if that protection is unavailable for a keyed request, you get 503 with code 10016. 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.
Full details, including the batch behavior, are in Send Email and Error Codes.

Batch sending

SendGrid’s personalizations 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.
  • 202 means every message succeeded. 207 means partial (or total) failure — the response carries a data array of successes and an errors array 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, and message — note message, not detail. Match failures back to inputs by index.
  • An Idempotency-Key header applies to the whole batch request, not to individual messages — there are no per-message keys inside messages. 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 failed index entries, 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
  • events is 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-ed25519 and telnyx-timestamp headers 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 at data.event_type and the message snapshot at data.payload.
If you don’t have a public HTTPS endpoint, skip webhooks entirely and poll instead:
curl
Polling is cursor-based: read 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.
Telnyx creates the template and its content in one request — there’s no separate template/version step. Fields are 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
Then send with it by passing 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.
Mailgun: Mailgun templates also use Handlebars — the same rewrite to Liquid applies. Postmark: Mustachio templates map cleanly to Liquid for basic interpolation.

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
Export each SendGrid suppression list (bounces, blocks, spam reports, unsubscribes, invalid emails) and import them as-is. The importer maps SendGrid’s type column onto the Telnyx reason taxonomy: bounceshard_bounce, spam_reportsspam_complaint, unsubscribesunsubscribe, invalid_emailsinvalid, and blocksmanual_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
Save the returned id, fetch the generated records with GET /v2/email_domains/{domain_id}/dns_records, publish them at your DNS provider, then trigger validation:
curl
Telnyx generates its own DKIM keypair, so you’ll publish a new DKIM record alongside your existing ESP’s. Both can coexist during the migration — keep the old records live until you’ve fully cut over. See Email Domains & DKIM.
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
Expect 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.