Suppressions
A suppression stops mail to an address — every message, or marketing mail only when the row came from an unsubscribe from marketing mail. Bounces, complaints and hand-added rows stop everything. List with filters, look one up, add one, remove one. Removal is audited. An entry that exists because of an erasure stops every message, has no expiry and is locked — removing it would let us mail an erased person again, and nobody can name the address to add it back.
Authentication
Read paths require suppressions:read. Create and delete require suppressions:write.
Keys rule — prefix, shown once, abilities never widen, and no key carries both messages:send and messages:read-content.
Endpoints
reason on create is one of manual, bounce, complaint, unsubscribe. Email is lowercased. Re-adding an address returns 200 and updates that row, not a second one. already_suppressed is true only when the live row already blocks the requested scope. Reviving an expired row, and widening a marketing row to all, return 200 with already_suppressed: false and the updated row.
scope says what a row stops: all — every message, the default and what bounces and complaints write — or marketing — only messages sent with "marketing": true, which is what an unsubscribe from one of them writes. Adding an all block for an address that only unsubscribed from marketing widens that row; nothing narrows one.
List
Keyset page of every live suppression on this account. Filter with ?email= or ?reason=. Same cursor shape as every other list.
PHP
Node
Python
Ruby
Each row is the object below. The list wraps those rows in data and adds next_cursor.
{
"data": {
"id": 1,
"email": "[email protected]",
"reason": "manual",
"source": "api",
"detail": "support ticket 12",
"scope": "all",
"suppressed_at": "2026-08-06T08:11:01+00:00",
"expires_at": null,
"erasure_locked": false
}
}Lookup
One row by address. ?email= is required. The 200 body is the same object. An address that is not suppressed, or whose row has expired, is 404 with message Not suppressed.
PHP
Node
Python
Ruby
Create
Body: email, reason, and optional detail and scope. Email is lowercased. 201 returns the same object. already_suppressed is true only when the live row already blocks the requested scope. Reviving an expired row, and widening a marketing row to all, return 200 with already_suppressed: false and the updated row.
PHP
Node
Python
Ruby
{
"data": {
"id": 2,
"email": "[email protected]",
"reason": "manual",
"source": "api",
"detail": "Re-added via API.",
"scope": "all",
"suppressed_at": "2026-08-07T09:00:00+00:00",
"expires_at": null,
"erasure_locked": false
},
"already_suppressed": false
}{
"data": {
"id": 3,
"email": "[email protected]",
"reason": "manual",
"source": "api",
"detail": "Added via API.",
"scope": "all",
"suppressed_at": "2026-08-07T09:05:00+00:00",
"expires_at": null,
"erasure_locked": false
},
"already_suppressed": false
}Show
One row by id. The body is the same object. Cross-account is 404.
PHP
Node
Python
Ruby
Delete
Removes the row and writes an audit entry. The response is a message, not the object above.
PHP
Node
Python
Ruby
{
"message": "[email protected] removed — sending to it is allowed again."
}Erasure lock
When a person is erased, their identity survives on the suppression list as an erased:… HMAC so the account can never mail them again. Those rows set erasure_locked: true. DELETE against one returns 409 with type: erasure_locked. Do not retry — the lock is the point.
{
"message": "This entry survives an erasure request. Removing it would defeat the erasure, so it is locked.",
"type": "erasure_locked"
}Responses & failures
{ "message": "Unauthenticated" }{
"message": "This API key does not have the \"suppressions:read\" ability. Abilities are chosen when the key is minted and cannot be widened — mint a new key that includes this ability.",
"type": "forbidden_ability",
"required_ability": "suppressions:read"
}{
"message": "Not suppressed"
}{
"message": "This entry survives an erasure request. Removing it would defeat the erasure, so it is locked.",
"type": "erasure_locked"
}