Manage email suppressions (blocks), import and export suppression lists, and manage unsubscribe groups. Use for deliverability compliance and bounce handling.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add team-telnyx/telnyx-ext-agent-skills --skill telnyx-email-suppressions-curl --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Telnyx Email Suppressions Curl?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/team-telnyx-telnyx-email-suppressions-curl)More formats (shields.io, HTML) on the badges page.
---
name: telnyx-email-suppressions-curl
description: >-
Manage email suppressions (blocks), import and export suppression lists,
and manage unsubscribe groups. Use for deliverability compliance and
bounce handling.
metadata:
author: telnyx
product: email
language: curl
---
<!-- Auto-generated from Telnyx OpenAPI specs. Do not edit. -->
# Telnyx Email Suppressions - curl
## Installation
```text
# curl is pre-installed on macOS, Linux, and Windows 10+
# jq is required for the import polling examples below:
# macOS: brew install jq
# Debian/Ubuntu: sudo apt-get install jq
```
## Setup
```bash
export TELNYX_API_KEY="YOUR_API_KEY_HERE"
```
All examples below use `$TELNYX_API_KEY` for authentication and the API base URL
`https://api.telnyx.com/v2`.
## Error Handling
All API calls can fail with network errors, authentication errors (401), or
framework errors (406). List-query validation failures return 400, resource
lookups can return 404, and JSON body validation failures normally return 422.
Inspect the HTTP status and the top-level `.errors` array before continuing:
```bash
response_file=$(mktemp)
status=$(curl --silent --show-error \
--output "$response_file" \
--write-out '%{http_code}' \
-H "Authorization: Bearer $TELNYX_API_KEY" \
"https://api.telnyx.com/v2/email_blocks")
if [ "$status" -lt 200 ] || [ "$status" -ge 300 ]; then
printf 'Telnyx API error (HTTP %s):\n' "$status" >&2
jq . "$response_file" >&2
rm -f "$response_file"
exit 1
fi
jq . "$response_file"
rm -f "$response_file"
```
Common statuses are `400` malformed query or import, `401` invalid API key,
`404` resource not found, `409` group still has active suppressions, `413`
import too large, and `422` invalid request attributes. A successful delete may
return `204 No Content`; do not attempt to parse that response as JSON.
## Important Notes
- **Account isolation:** Every lookup is scoped to the authenticated account. A malformed UUID or a UUID owned by another account is reported as 404.
- **Normalized addresses:** Recipient addresses are trimmed and lower-cased. The `from` address on a manual block is also normalized.
- **Pagination:** Most list operations use `page[number]` and `page[size]` (maximum 100). The main block list also supports opaque cursors. Do not combine offset and cursor modes.
- **URL encoding:** Percent-encode an email address placed in a URL path. For example, use `alice%40example.com`, not an untrusted raw string.
- **Idempotency:** Creating a block or adding a group suppression returns 200 when the matching suppression already exists and 201 when a row is created.
## Operational Caveats
- `POST /v2/email_blocks` always creates `reason: manual_block` with `source: manual`. Customers cannot use this endpoint to create `hard_bounce`, `spam_complaint`, or `invalid` suppressions; caller-supplied `reason` and `source` are ignored.
- Scope is server-derived as `account`, `domain`, or `address`, never customer-set: no `domain_id` and no `from` gives `account`; `domain_id` without `from` gives `domain`; a `from` address gives `address` scope.
- `unsubscribe` and `manual_block` are overridable at send time with `ignore_suppression: true`. `hard_bounce`, `spam_complaint`, and `invalid` are not overridable. Bypassing an overridable suppression should be deliberate and auditable.
- Import is asynchronous. `POST /v2/email_blocks/import` returns 202 and a job ID; poll `GET /v2/email_blocks/import/{id}` until `completed` or `failed`. Import behavior for scoped suppressions may vary. Check the import result for the actual scope assigned.
- Export is synchronous and streams CSV directly. It does not create a job.
- Deleting a block is a soft delete: the row remains as a tombstone with `status: removed`. Recreating the same removed suppression reactivates it.
- The `expires_at` field is available for setting an expiration timestamp on suppressions.
- Check both `error_count` and `skipped_count` in the import response for rejected entries.
- A group suppression prevents sending to that address for every campaign that uses the unsubscribe group. It is not an account-wide unsubscribe for campaigns that do not use that group.
## Reference Use Rules
Do not invent Telnyx parameters, enums, response fields, import counters, or CSV
columns.
- Read [references/api-details.md](references/api-details.md) before building pagination, bulk migration, or delete automation.
- Before deciding whether a send may bypass a block, read [suppression semantics](references/api-details.md#suppression-semantics).
- Before importing an exported list, read [CSV export and import](references/api-details.md#csv-export-and-import) and verify the scope assigned by the import result.
- For exhaustive request fields, response fields, status codes, and operation IDs, use [the operation catalog](references/api-details.md#operation-catalog) and [response schemas](references/api-details.md#response-schemas).
## Core Tasks
### List suppressions
Use offset pagination for page-oriented tools or cursor pagination for
sequential traversal without page-number offsets.
`GET /v2/email_blocks`
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `page[number]` | integer | No | Offset page, default 1. Do not combine with a cursor. |
| `page[size]` | integer | No | 1-100, default 25. |
| `page[after]` | string | No | Opaque next-page cursor. Exclusive with `page[number]` and `page[before]`. |
| `page[before]` | string | No | Opaque previous-page cursor. Exclusive with `page[number]` and `page[after]`. |
| `sort` | enum | No | `created_at` or `-created_at` (default). |
| `filter[reason]` | enum | No | Exact reason match. |
| `filter[domain_id]` | UUID | No | Exact domain ID match. |
| `filter[created_after]` | date-time | No | Match `created_at > value`. |
| `filter[created_before]` | date-time | No | Match `created_at < value`. |
```bash
curl --get --silent --show-error \
-H "Authorization: Bearer $TELNYX_API_KEY" \
--data-urlencode 'page[size]=100' \
--data-urlencode 'filter[reason]=hard_bounce' \
--data-urlencode 'sort=-created_at' \
"https://api.telnyx.com/v2/email_blocks"
```
Offset responses expose `.meta.total_pages`; cursor responses expose
`.meta.has_next` and, when another page exists, `.meta.next_cursor`. Pass the
returned cursor unchanged.
### Create a manual suppression
`POST /v2/email_blocks`
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `to` | string | Yes | Recipient address; trimmed and lower-cased by the server. |
| `domain_id` | UUID or null | No | Domain context; omit/null for account scope. |
| `from` | string or null | No | Sender context; a value produces address scope. |
| `expires_at` | date-time or null | No | Expiration timestamp for the suppression. |
```bash
curl --silent --show-error \
-X POST \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "blocked@example.com"
}' \
"https://api.telnyx.com/v2/email_blocks"
```
The response is `.data` with forced `reason: manual_block`, `source: manual`,
a server-derived `scope`, and `status: active`. Do not send `scope`, `group_id`,
`bounce_category`, `dsn_code`, or `meta` to this public operation.
### Export suppressions as CSV
`GET /v2/email_blocks/export`
```bash
curl --get --silent --show-error \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Accept: text/csv" \
--data-urlencode 'filter[created_after]=2026-01-01T00:00:00Z' \
--output email_blocks_export.csv \
"https://api.telnyx.com/v2/email_blocks/export"
```
The 200 response is the CSV stream itself. Filters supported by the list
endpoint affect export. Although `sort` and `page[*]` are parsed and invalid
values can return 400, valid values are ignored; export always streams every
matching row ordered by `created_at ASC, id ASC`.
### Start an asynchronous CSV import
`POST /v2/email_blocks/import`
```bash
curl --silent --show-error \
-X POST \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Accept: application/json" \
-F 'file=@email_blocks.csv;type=text/csv' \
-F 'block_ttl_days=30' \
"https://api.telnyx.com/v2/email_blocks/import"
```
A valid request returns 202 with `.data.id` and `.data.status` equal to
`pending`. CSV content may not exceed 25 MiB or 250,000 rows. Provider format is
auto-detected as `sendgrid`, `mailgun`, `ses`, or `generic`.
`block_ttl_days` applies only to imported `manual_block` rows.
### Poll an import job
`GET /v2/email_blocks/import/{id}`
```bash
IMPORT_ID="00000000-0000-0000-0000-000000000000"
while :; do
body=$(curl --silent --show-error \
-H "Authorization: Bearer $TELNYX_API_KEY" \
"https://api.telnyx.com/v2/email_blocks/import/$IMPORT_ID") || exit 1
state=$(printf '%s' "$body" | jq -r '.data.status')
printf 'import status: %s\n' "$state"
case "$state" in
completed)
printf '%s' "$body" | jq '.data | {
processed_rows, created_count, existing_count,
skipped_count, error_count, errors
}'
break
;;
failed)
printf '%s' "$body" | jq '.data | {status, failure_reason}' >&2
exit 1
;;
pending|processing) sleep 2 ;;
*) printf 'unexpected import status: %s\n' "$state" >&2; exit 1 ;;
esac
done
```
Completion counters are omitted until status is `completed`; `failure_reason`
is only present on failure. Check both `error_count` and `skipped_count` in the
import response for rejected entries, and inspect `errors` when present.
### Retrieve a suppression
`GET /v2/email_blocks/{id}`
```bash
BLOCK_ID="00000000-0000-0000-0000-000000000000"
curl --silent --show-error \
-H "Authorization: Bearer $TELNYX_API_KEY" \
"https://api.telnyx.com/v2/email_blocks/$BLOCK_ID"
```
Primary response fields are `.data.id`, `.data.to`, `.data.from`,
`.data.domain_id`, `.data.group_id`, `.data.reason`, `.data.source`,
`.data.scope`, `.data.status`, `.data.expires_at`, `.data.created_at`, and
`.data.updated_at`.
### Soft-delete a suppression
`DELETE /v2/email_blocks/{id}`
```bash
BLOCK_ID="00000000-0000-0000-0000-000000000000"
curl --silent --show-error \
-X DELETE \
-H "Authorization: Bearer $TELNYX_API_KEY" \
"https://api.telnyx.com/v2/email_blocks/$BLOCK_ID"
```
This returns 200 with the tombstone in `.data`; verify
`.data.status == "removed"`. Repeating the delete is idempotent and does not
append another audit event.
### List a suppression's audit events
`GET /v2/email_blocks/{id}/events`
```bash
BLOCK_ID="00000000-0000-0000-0000-000000000000"
curl --get --silent --show-error \
-H "Authorization: Bearer $TELNYX_API_KEY" \
--data-urlencode 'page[number]=1' \
--data-urlencode 'page[size]=50' \
"https://api.telnyx.com/v2/email_blocks/$BLOCK_ID/events"
```
Events are newest first and can be `created`, `removed`, `expired`, or
`override_used`. This endpoint has offset pagination only and a default page
size of 50; it has no filters, sort, or cursor parameters.
### List unsubscribe groups
`GET /v2/email_unsubscribe_groups`
```bash
curl --get --silent --show-error \
-H "Authorization: Bearer $TELNYX_API_KEY" \
--data-urlencode 'page[number]=1' \
--data-urlencode 'page[size]=25' \
"https://api.telnyx.com/v2/email_unsubscribe_groups"
```
Groups use offset pagination only and fixed newest-first ordering.
### Create an unsubscribe group
`POST /v2/email_unsubscribe_groups`
```bash
curl --silent --show-error \
-X POST \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Product announcements",
"description": "Optional opt-out category for product email"
}' \
"https://api.telnyx.com/v2/email_unsubscribe_groups"
```
`name` is required, non-empty, and at most 255 characters. A successful create
returns 201 and the group in `.data`.
### Retrieve an unsubscribe group
`GET /v2/email_unsubscribe_groups/{id}`
```bash
GROUP_ID="00000000-0000-0000-0000-000000000000"
curl --silent --show-error \
-H "Authorization: Bearer $TELNYX_API_KEY" \
"https://api.telnyx.com/v2/email_unsubscribe_groups/$GROUP_ID"
```
### Update an unsubscribe group
Only `name` and `description` are mutable. This is a partial update; `PUT` is
not routed.
`PATCH /v2/email_unsubscribe_groups/{id}`
```bash
GROUP_ID="00000000-0000-0000-0000-000000000000"
curl --silent --show-error \
-X PATCH \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Announcements and release notes"
}' \
"https://api.telnyx.com/v2/email_unsubscribe_groups/$GROUP_ID"
```
### Delete an unsubscribe group
`DELETE /v2/email_unsubscribe_groups/{id}`
```bash
GROUP_ID="00000000-0000-0000-0000-000000000000"
curl --silent --show-error \
-X DELETE \
-H "Authorization: Bearer $TELNYX_API_KEY" \
"https://api.telnyx.com/v2/email_unsubscribe_groups/$GROUP_ID"
```
A successful delete returns 204. If active group suppressions remain, the
request returns 409. Either remove them first or deliberately force the delete:
```bash
curl --get --silent --show-error \
-X DELETE \
-H "Authorization: Bearer $TELNYX_API_KEY" \
--data-urlencode 'force=true' \
"https://api.telnyx.com/v2/email_unsubscribe_groups/$GROUP_ID"
```
`force=true` soft-deletes active group suppressions, clears their group links,
appends removal events, and then hard-deletes the group in one transaction.
### List suppressions in a group
`GET /v2/email_unsubscribe_groups/{id}/suppressions`
```bash
GROUP_ID="00000000-0000-0000-0000-000000000000"
curl --get --silent --show-error \
-H "Authorization: Bearer $TELNYX_API_KEY" \
--data-urlencode 'page[number]=1' \
--data-urlencode 'page[size]=100' \
"https://api.telnyx.com/v2/email_unsubscribe_groups/$GROUP_ID/suppressions"
```
Rows use the standard email-block shape with `.group_id` set to this group.
This list supports offset pagination only; no filters, sort, or cursor.
### Add a group suppression
`POST /v2/email_unsubscribe_groups/{id}/suppressions`
```bash
GROUP_ID="00000000-0000-0000-0000-000000000000"
curl --silent --show-error \
-X POST \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "subscriber@example.com"
}' \
"https://api.telnyx.com/v2/email_unsubscribe_groups/$GROUP_ID/suppressions"
```
Only `to` is read. The server forces `reason: unsubscribe`, `source: manual`,
and this group's `group_id`. A duplicate is idempotent and returns 200.
### Remove a group suppression
`DELETE /v2/email_unsubscribe_groups/{id}/suppressions/{email}`
```bash
GROUP_ID="00000000-0000-0000-0000-000000000000"
# The email path segment is percent-encoded.
EMAIL_PATH="subscriber%40example.com"
curl --silent --show-error \
-X DELETE \
-H "Authorization: Bearer $TELNYX_API_KEY" \
"https://api.telnyx.com/v2/email_unsubscribe_groups/$GROUP_ID/suppressions/$EMAIL_PATH"
```
A successful removal returns 204 and soft-deletes every active matching row for
the normalized address in that group. A repeat returns 404 because no active
matching group suppression remains.
## Webhooks
Email webhooks are configured at the **domain level**, not on an individual
suppression. Create a subscription with `POST /email_domains/{id}/webhooks`
using the `https://api.telnyx.com/v2` base URL. The suppression-related event
names in the current OpenAPI `EmailWebhookEvent` enum are:
| Event | Use in a suppression workflow |
|-------|-------------------------------|
| `email.bounced` | Process a bounced-delivery outcome. |
| `email.failed` | Process a terminal delivery failure. |
| `email.complained` | Process a recipient complaint. |
| `email.unsubscribed` | Process a recipient unsubscribe action. |
```bash
DOMAIN_ID="00000000-0000-0000-0000-000000000000"
curl --silent --show-error \
-X POST \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/email",
"events": ["email.bounced", "email.failed", "email.complained", "email.unsubscribed"]
}' \
"https://api.telnyx.com/v2/email_domains/$DOMAIN_ID/webhooks"
```
### Verify signatures before parsing
Telnyx signs webhook requests with Ed25519. Read the
`telnyx-signature-ed25519` and `telnyx-timestamp` headers, and retain the exact
raw request body bytes. Do **not** parse or re-serialize the JSON before
verification.
1. Reject a missing or malformed timestamp, signature, or body.
2. Reject a timestamp more than 5 minutes outside the server clock.
3. Verify the Ed25519 signature over `telnyx-timestamp + "|" + raw_body` with
the Telnyx public key. Ed25519 verification is not HMAC validation.
4. Parse the JSON only after the signature and timestamp are valid.
### Deduplicate and acknowledge quickly
- Deduplicate by event ID with an atomic uniqueness check before applying side
effects. Treat an already-seen event as successfully handled.
- Enqueue slow work and return a `2xx` response within 10 seconds.
- Telnyx retries on timeout or non-2xx. Keep your endpoint idempotent.
## Additional Operations
Use the core tasks above first. All 16 reachable suppression and unsubscribe
group operations are indexed here with their exact HTTP endpoints. Use
[references/api-details.md](references/api-details.md) for full optional
parameters, response schemas, status codes, and operation IDs.
| Operation | SDK method | Endpoint | Use when | Required params |
|-----------|------------|----------|----------|-----------------|
| List suppressions | HTTP only | `GET /email_blocks` | Inspect suppressions or select a block before another action. | None |
| Create a manual suppression | HTTP only | `POST /email_blocks` | Add a manual recipient suppression. | `to` |
| Export suppressions as CSV | HTTP only | `GET /email_blocks/export` | Download matching suppressions as CSV. | None |
| Start a CSV import | HTTP only | `POST /email_blocks/import` | Upload suppressions for asynchronous import. | `file` |
| Poll an import job | HTTP only | `GET /email_blocks/import/{id}` | Check whether an asynchronous import completed or failed. | `id` |
| Retrieve a suppression | HTTP only | `GET /email_blocks/{id}` | Fetch one suppression by ID. | `id` |
| Soft-delete a suppression | HTTP only | `DELETE /email_blocks/{id}` | Remove an existing suppression. | `id` |
| List suppression audit events | HTTP only | `GET /email_blocks/{id}/events` | Inspect the history of one suppression. | `id` |
| List unsubscribe groups | HTTP only | `GET /email_unsubscribe_groups` | Inspect available unsubscribe groups. | None |
| Create an unsubscribe group | HTTP only | `POST /email_unsubscribe_groups` | Create a campaign-level unsubscribe category. | `name` |
| Retrieve an unsubscribe group | HTTP only | `GET /email_unsubscribe_groups/{id}` | Fetch one unsubscribe group by ID. | `id` |
| Update an unsubscribe group | HTTP only | `PATCH /email_unsubscribe_groups/{id}` | Change a group's name or description. | `id` |
| Delete an unsubscribe group | HTTP only | `DELETE /email_unsubscribe_groups/{id}` | Remove an unsubscribe group. | `id` |
| List group suppressions | HTTP only | `GET /email_unsubscribe_groups/{id}/suppressions` | Inspect recipients suppressed in one group. | `id` |
| Add a group suppression | HTTP only | `POST /email_unsubscribe_groups/{id}/suppressions` | Suppress a recipient in one unsubscribe group. | `id`, `to` |
| Remove a group suppression | HTTP only | `DELETE /email_unsubscribe_groups/{id}/suppressions/{email}` | Remove a recipient suppression from one group. | `id`, `email` |
---
For exhaustive parameter behavior, CSV columns and limitations, response
schemas, status codes, and operation IDs, see
[references/api-details.md](references/api-details.md).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!