https://api.telnyx.com/v2 and an Authorization: Bearer *** header.
Suppression model
A suppression (block) is a record that prevents email from being sent to a recipient address. Telnyx creates suppressions automatically in response to deliverability signals, and you can create them manually via the API. Every suppression has a reason — the canonical enum string that describes why the block exists — and a source that records how it was created.Reasons
The three non-overridable reasons (
hard_bounce, spam_complaint, invalid) can never be bypassed by ignore_suppression — Telnyx will not deliver to those recipients regardless of the flag. The two overridable reasons (unsubscribe, manual_block) can be bypassed when you send with ignore_suppression: true and an API key that has the email:override scope. See Send-time behavior.
Sources
How auto-suppressions are created
Telnyx’s delivery event pipeline classifies incoming KumoMTA log records and enqueues a suppression worker for suppressible events:- Spam complaint — an ARF
Feedbackrecord creates aspam_complaintsuppression. - Hard bounce — a
BounceorOOBrecord with a5.1.xor5.2.xenhanced status code, or a5xxSMTP code with no enhanced code, creates ahard_bouncesuppression. - Soft-bounce escalation — repeated
TransientFailure(deferred) records for the same recipient increment a counter; when the threshold is breached, Telnyx escalates to ahard_bouncesuppression. - Expiration — a KumoMTA
Expirationrecord (system gave up retrying) creates ahard_bouncesuppression. - Unsubscribe — a recipient unsubscribes via the RFC 8058 one-click flow or the unsubscribe link, creating an
unsubscribesuppression (see RFC 8058 one-click unsubscribe).
AdminBounce records, 5.7.x enhanced codes (sender-side policy), and other unclassified records are not suppressed.
The public API can only create
manual_block suppressions. Any reason value you send to POST /email_blocks is ignored — the server forces reason: manual_block and source: manual. Auto-suppressions (hard_bounce, spam_complaint, unsubscribe) are created by Telnyx internally. CSV import is the one other path that can produce manual_block rows, with source: import.Scope
A suppression’s scope determines which sends it blocks. Scope is derived server-side from the suppression’sdomain_id and from fields — it’s never trusted from the client. There are exactly three scopes:
At send time, Telnyx checks the recipient against all matching active suppressions. Account-scoped blocks match across all domains, but a non-null
from still restricts matching to that sender. Domain-scoped blocks match when the send’s domain matches. Address-scoped blocks match when both domain and sender match.
group_id is a filter, not a scope
group_id is not a fourth scope — it’s an independent association that narrows which sends a suppression applies to, and it combines with whichever of the three scopes the record already has. A suppression with group_id: null applies to every send regardless of group; a suppression with group_id set applies only to sends that carry the same group_id. See Unsubscribe groups.
Manage suppressions
List suppressions
GET /email_blocks returns the account’s suppressions with offset or cursor pagination, sort, and filters.
curl
Offset-mode response (default):
page[after] or page[before]):
meta always carries page_size, next_cursor, previous_cursor, has_next, and has_previous. All five keys are always present in the response; when there is no next or previous page, the corresponding cursor is null (not omitted).
Retrieve a suppression
GET /email_blocks/{id} returns a single suppression by ID.
curl
200 with the suppression in data, or 404 if the ID doesn’t exist or belongs to another account.
Create a manual suppression
POST /email_blocks creates a manual_block suppression. The server forces reason: manual_block and source: manual regardless of any values you send.
curl
expires_at is not currently enforced. The field is accepted, stored, and returned, but no expiration process runs against it: the send-time suppression check matches on status = "active" alone and never compares expires_at to the current time. A suppression with a past expires_at keeps blocking sends. Treat expires_at as informational metadata and remove suppressions explicitly with DELETE /email_blocks/{id} when you want them to stop applying.200 with the existing record and creates no new audit event.
If the matching record was previously removed, the create reactivates it instead: the existing row flips back to status: "active", the request’s expires_at is re-applied, a fresh created audit event is appended, and the API returns 201. The row is reused rather than duplicated, so the audit trail reads created → removed → created on the same suppression ID.
Delete a suppression
DELETE /email_blocks/{id} soft-deletes a suppression. The record is retained with status: "removed" and a removed audit event is appended.
curl
200 with the suppression (now status: "removed"). Deleting an already-removed suppression is idempotent — returns 200 with no new event.
Suppression audit events
GET /email_blocks/{id}/events lists the audit trail for a suppression. Each event records a lifecycle transition.
curl
Uses offset pagination (
page[number] default 1, page[size] default 50, max 100). Events are ordered by occurred_at descending.
CSV import & export
Migrate an existing suppression list from SendGrid, Mailgun, or Amazon SES, or import a generic CSV. Export your Telnyx suppressions as CSV for backup or transfer.Import a CSV
POST /email_blocks/import accepts a multipart upload with a CSV file and returns 202 with an async import job. The provider format is auto-detected from the CSV header row.
curl
Limits: decoded CSV up to 25 MiB and 250,000 data rows. Exceeding either returns
413.
All imported suppressions are account-scoped (scope: account, domain_id: null, from: null).
Provider auto-detection
The importer detects the provider from the CSV header row (normalized to lowercase, trimmed):
Detection order is SendGrid → Mailgun → SES → generic. An undetectable or header-only CSV returns
400.
Rows with a missing or blank email address, or an unmappable reason, are skipped and recorded in the import’s errors map (keyed by row number, 1-indexed with the header as row 1).
Poll an import job
GET /email_blocks/import/{id} returns the current state of an import job. Poll this until status is completed or failed.
curl
When
status is completed, the response includes processed_rows, created_count, existing_count, skipped_count, and error_count. Rejected rows are counted in skipped_count and detailed in an errors object that maps row numbers to skip reasons. error_count tracks a separate failure class that the importer does not currently populate, so it stays 0 even when rows were rejected:
Use
skipped_count and errors — not error_count — to detect rows the importer rejected. A row with a missing/blank email address or an unmappable reason increments skipped_count and gets an entry in errors; error_count remains 0.block_ttl_days only writes an expires_at value onto imported manual_block rows; because expires_at is not enforced, those rows keep blocking after the TTL date passes until you delete them.
Export suppressions as CSV
GET /email_blocks/export streams the account’s suppressions as a chunked CSV response with Content-Type: text/csv. Filters affect which rows are exported; sort and page parameters are accepted but ignored.
curl
id,to,from,reason,source,scope,status,domain_id,created_at,updated_at,expires_at,group_id.
Export → import is lossy — it is not a round-trip. The export writes all twelve columns, but the generic importer only consumes
to/to_address/email and reason. Everything else in the file is discarded: from, domain_id, group_id, source, status, id, and the exported expires_at are all dropped on re-import.Consequences of re-importing your own export:- Domain- and address-scoped suppressions are broadened to account scope. Every imported row is written with
from_address: null,domain_id: null,scope: account, so a block that previously applied to one sending domain now blocks that recipient across your whole account. - Group-scoped opt-outs become global.
group_idis not read on import, so a newsletter-only unsubscribe re-imports as a suppression that blocks every send to that recipient, including transactional mail. - Expiry metadata is not preserved. Imported
manual_blockrows get a freshexpires_atderived fromblock_ttl_days; every other reason getsexpires_at: null. - Removed rows come back active.
statusis ignored, so an exported tombstone re-imports as an active suppression.
POST /email_blocks (with domain_id / from) or POST /email_unsubscribe_groups/{id}/suppressions.Send-time behavior
When you send an email, Telnyx checks every recipient against the suppression list before creating the message. Suppressed recipients are reported in the response — they’re never silently dropped.Partial suppression (202)
When some recipients are suppressed but others are not, the send proceeds for the non-suppressed recipients. The202 response includes a top-level suppressed array describing each suppressed recipient:
suppressed array has:
All recipients suppressed (422)
When every recipient is suppressed, the request returns422 with a recipient_suppressed error code and the suppressed array:
Batch sends
In a batch (POST /email_messages/batch), suppression is checked per message. A message whose recipients are all suppressed produces a per-item error with code: "recipient_suppressed" in the batch response’s errors array (indexed by position in the request).
Overriding suppressions
Setignore_suppression: true on a send to bypass overridable suppressions (unsubscribe and manual_block). This requires an API key with the email:override scope — without it, the request returns 403:
ignore_suppression: true is honored:
- Recipients with only overridable suppressions (
override_allowed: true) proceed — the send goes through and anoverride_usedaudit event is recorded on each bypassed suppression. - Recipients with any non-overridable suppression (
override_allowed: false) remain blocked —ignore_suppressioncannot bypasshard_bounce,spam_complaint, orinvalid.
email:override to keys with full_access or email:admin. Keys with only email:send or email:read cannot override.
curl
ignore_suppression can only bypass unsubscribe and manual_block. It will never override a hard_bounce, spam_complaint, or invalid suppression — those recipients stay blocked and appear in the suppressed array even when the flag is set.Unsubscribe groups
Unsubscribe groups let you suppress a recipient for one category of email without blocking all your sends to them. This is the standard pattern for separating newsletter opt-outs from transactional email. For example, a recipient who unsubscribes from your “Weekly Newsletter” group should still receive password-reset emails. Group-scoped suppressions only block sends that specify the samegroup_id.
Create a group
POST /email_unsubscribe_groups creates a group.
curl
List, retrieve, update, and delete groups
GET /email_unsubscribe_groups— list groups (offset pagination,page[size]default 25, max 100).GET /email_unsubscribe_groups/{id}— retrieve a group.PATCH /email_unsubscribe_groups/{id}— updatenameand/ordescription.PUTis not supported.DELETE /email_unsubscribe_groups/{id}— delete a group. If the group has active suppressions, the request returns409unless you pass?force=true, which soft-deletes the suppressions first.
Manage suppressions in a group
GET /email_unsubscribe_groups/{id}/suppressions— list suppressions in a group (offset pagination).POST /email_unsubscribe_groups/{id}/suppressions— add a recipient to the group’s suppression list. The server forcesreason: unsubscribe,source: manual, andgroup_id: <this group>. Onlytois read from the request body.DELETE /email_unsubscribe_groups/{id}/suppressions/{email}— remove a recipient from the group’s suppression list. Returns204.
curl
Send with a group
Passgroup_id on a send to scope the suppression check to that group. The send will check account-, domain-, and address-scoped suppressions as usual. Records with group_id: null apply to every send regardless of group; records with group_id set additionally require the same group_id to match.
curl
The
group_id you set on a send is persisted on the message. When the recipient later unsubscribes via the RFC 8058 one-click flow, the resulting suppression is scoped to that same group — so the opt-out only affects future sends to that group, not your transactional email.RFC 8058 one-click unsubscribe
RFC 8058 defines a one-click unsubscribe mechanism: supporting email clients (Gmail, Yahoo, Apple Mail, and others) send an automaticPOST to a unsubscribe URL when the recipient clicks the unsubscribe button, without the recipient having to visit a web page.
How it works
-
Headers on outgoing mail. When the sending domain has
unsubscribe_trackingenabled (the default), Telnyx adds two headers to every outgoing message:List-Unsubscribe: <https://...tracking/unsubscribe/{token}>— a signed, unguessable URL.List-Unsubscribe-Post: List-Unsubscribe=One-Click— signals RFC 8058 support to the mailbox provider.
-
Recipient unsubscribes. A recipient can unsubscribe two ways:
- One-click (
POST) — the email client sends an automaticPOSTto the unsubscribe URL (RFC 8058). Telnyx records the unsubscribe withmethod: "one_click". - Link (
GET) — the recipient clicks the unsubscribe link and lands on a confirmation page. Telnyx records the unsubscribe withmethod: "link".
- One-click (
-
Suppression created. Telnyx enqueues a worker that creates an
unsubscribesuppression for the recipient. When the original send specified agroup_id, the suppression is scoped to that group. -
Event recorded. Telnyx records an
email.unsubscribedevent on the message (with themethod—linkorone_click). These events also flow to configured webhooks.
POST and GET always return 200, even for invalid tokens, to avoid leaking information about which message IDs exist (per RFC 8058 §3).
Enabled by default
unsubscribe_tracking defaults to true for every email domain. This is intentional: Gmail and Yahoo require RFC 8058 one-click unsubscribe support for bulk senders. You can disable it per domain if you handle unsubscribes yourself, but doing so risks deliverability problems with those providers.
curl
Open and click tracking default to
false; unsubscribe tracking defaults to true. The one-click unsubscribe flow only fires when the sending domain has unsubscribe_tracking enabled.The
List-Unsubscribe URL carries a signed HMAC token, not a bare message ID. If the signing key is missing or misconfigured, Telnyx omits the unsubscribe headers entirely rather than emitting an invalid token — recipients can’t unsubscribe from a message with no headers, but they also won’t see a broken unsubscribe button.