Skip to main content
Manage the Telnyx Email suppression list (blocklist) to protect your sender reputation. Suppressions stop Telnyx from sending to addresses that have bounced, complained, unsubscribed, or that you’ve manually blocked. This guide covers the suppression model, scope, CRUD operations, CSV import/export, send-time override behavior, unsubscribe groups, and the RFC 8058 one-click unsubscribe flow. All requests use the production base URL 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 Feedback record creates a spam_complaint suppression.
  • Hard bounce — a Bounce or OOB record with a 5.1.x or 5.2.x enhanced status code, or a 5xx SMTP code with no enhanced code, creates a hard_bounce suppression.
  • Soft-bounce escalation — repeated TransientFailure (deferred) records for the same recipient increment a counter; when the threshold is breached, Telnyx escalates to a hard_bounce suppression.
  • Expiration — a KumoMTA Expiration record (system gave up retrying) creates a hard_bounce suppression.
  • Unsubscribe — a recipient unsubscribes via the RFC 8058 one-click flow or the unsubscribe link, creating an unsubscribe suppression (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’s domain_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):
Cursor-mode response (when you use page[after] or page[before]):
Cursor 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
Returns 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.
Creating a suppression that already exists and is still active (same account, scope, recipient, reason, domain, from, and group) is idempotent — the API returns 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
Returns 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.
Also note 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
The CSV columns, in order: 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_id is 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_block rows get a fresh expires_at derived from block_ttl_days; every other reason gets expires_at: null.
  • Removed rows come back active. status is ignored, so an exported tombstone re-imports as an active suppression.
Use the export for backup, reporting, or migration to another system — not as a way to restore Telnyx state. To recreate scoped or group-scoped suppressions faithfully, replay them through 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. The 202 response includes a top-level suppressed array describing each suppressed recipient:
Each entry in the suppressed array has:

All recipients suppressed (422)

When every recipient is suppressed, the request returns 422 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

Set ignore_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:
When ignore_suppression: true is honored:
  • Recipients with only overridable suppressions (override_allowed: true) proceed — the send goes through and an override_used audit event is recorded on each bypassed suppression.
  • Recipients with any non-overridable suppression (override_allowed: false) remain blocked — ignore_suppression cannot bypass hard_bounce, spam_complaint, or invalid.
The scope hierarchy grants 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 same group_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} — update name and/or description. PUT is not supported.
  • DELETE /email_unsubscribe_groups/{id} — delete a group. If the group has active suppressions, the request returns 409 unless 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 forces reason: unsubscribe, source: manual, and group_id: <this group>. Only to is read from the request body.
  • DELETE /email_unsubscribe_groups/{id}/suppressions/{email} — remove a recipient from the group’s suppression list. Returns 204.
curl

Send with a group

Pass group_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 automatic POST to a unsubscribe URL when the recipient clicks the unsubscribe button, without the recipient having to visit a web page.

How it works

  1. Headers on outgoing mail. When the sending domain has unsubscribe_tracking enabled (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.
  2. Recipient unsubscribes. A recipient can unsubscribe two ways:
    • One-click (POST) — the email client sends an automatic POST to the unsubscribe URL (RFC 8058). Telnyx records the unsubscribe with method: "one_click".
    • Link (GET) — the recipient clicks the unsubscribe link and lands on a confirmation page. Telnyx records the unsubscribe with method: "link".
  3. Suppression created. Telnyx enqueues a worker that creates an unsubscribe suppression for the recipient. When the original send specified a group_id, the suppression is scoped to that group.
  4. Event recorded. Telnyx records an email.unsubscribed event on the message (with the methodlink or one_click). These events also flow to configured webhooks.
Both 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.