Skip to content

Suppressions & unsubscribe

The platform enforces two opt-out mechanisms on every send: a workspace suppression list (a hard block on a recipient) and subscription topics (per-topic opt-out, a.k.a. unsubscribe groups). Both are checked before a message is queued, and both feed the RFC 8058 one-click unsubscribe link stamped on every email.

All endpoints use the base URL https://app.pufferpost.com/v1 and bearer API-key auth. See /reference/api/ for the full reference.

The suppression list

A suppression entry blocks a single recipient address for the whole workspace. Entries are created two ways:

  • Automatically — a permanent (hard) bounce or a complaint on a delivered message adds the recipient with reason hard_bounce or complaint. This is reputation protection and happens with no action from you.
  • Manually — you add an address yourself (reason manual), or a recipient opts out via the hosted unsubscribe page (reason unsubscribe).

When you call POST /v1/messages, the recipient is checked against this list first. A suppressed recipient is rejected at accept time — the send never enters the queue and no email goes out. The API returns an error envelope with code recipient_suppressed:

{
"error": {
"code": "recipient_suppressed",
"message": "\"jane@example.com\" is on the suppression list.",
"request_id": "req_01J9Z…"
}
}

Add a suppression

POST /v1/suppressions adds a recipient manually. Scope required: suppressions:manage.

Terminal window
curl -X POST https://app.pufferpost.com/v1/suppressions \
-H "Authorization: Bearer key_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"recipient": "jane@example.com"}'

The only field is recipient, which must be a valid email address. The call is idempotent — re-suppressing an address that is already on the list keeps the original entry rather than creating a duplicate.

Response — 201 Created:

{
"id": "sup_01J9ZQ8F3K2R7M5N6P4T0V8W1X",
"recipient": "jane@example.com",
"reason": "manual",
"source": "api",
"created_at": "2026-06-09T10:21:44+00:00"
}

The reason reflects how the entry was created (hard_bounce, complaint, manual, or unsubscribe); source is a short provenance string (api, one-click, or the originating message id for event-driven entries).

List suppressions

GET /v1/suppressions returns the workspace’s suppression list, newest first. Scope required: suppressions:read.

Terminal window
curl https://app.pufferpost.com/v1/suppressions \
-H "Authorization: Bearer key_live_xxxxxxxx"

Response — 200 OK:

{
"data": [
{
"id": "sup_01J9ZQ8F3K2R7M5N6P4T0V8W1X",
"recipient": "jane@example.com",
"reason": "manual",
"source": "api",
"created_at": "2026-06-09T10:21:44+00:00"
},
{
"id": "sup_01J9ZP1A2B3C4D5E6F7G8H9J0K",
"recipient": "bounced@example.com",
"reason": "hard_bounce",
"source": "msg_01J9ZNX…",
"created_at": "2026-06-08T14:03:11+00:00"
}
]
}

Remove a suppression

DELETE /v1/suppressions/{id} removes a recipient once they are safe to send to again. Scope required: suppressions:manage.

Terminal window
curl -X DELETE https://app.pufferpost.com/v1/suppressions/sup_01J9ZQ8F3K2R7M5N6P4T0V8W1X \
-H "Authorization: Bearer key_live_xxxxxxxx"

Response — 204 No Content. The id is workspace-scoped: an id that belongs to another workspace returns 404 Not Found, never a cross-tenant delete.

Deleting a suppression that was created from a hard bounce or complaint is allowed, but do it only when you know the underlying problem is fixed — sending to an address that keeps bouncing damages your sender reputation.

One-click unsubscribe (RFC 8058)

Every email the platform sends carries the headers required for one-click unsubscribe, so Gmail, Apple Mail, Outlook and friends render a native “Unsubscribe” affordance:

List-Unsubscribe: <https://app.pufferpost.com/unsubscribe?token=…>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

The token is signed and carries the workspace, recipient, and (if set) the subscription topic. You do not build these headers — they are stamped automatically at send time.

The hosted unsubscribe page

The URL in the header is a hosted, public surface:

  • GET /unsubscribe?token=… renders a branded confirmation page — what a human clicking the link sees.
  • POST /unsubscribe is the RFC 8058 one-click action. The recipient’s email client posts it automatically; the confirmation page’s button posts here too.

A valid token opts the recipient out — of one subscription topic if the message was tagged with one, otherwise globally (a unsubscribe-reason suppression). If the token’s topic no longer exists, it falls back to a global suppression so the recipient is always honoured. Forged or expired tokens render a branded error page. The action is idempotent.

You don’t call these endpoints yourself; they exist so recipients always have a working opt-out without you hosting anything.

Subscription topics (unsubscribe groups)

A subscription topic lets a recipient opt out of one category of mail (for example “Receipts” or “Shipping updates”) without being globally suppressed. Topics are per-workspace.

Create a topic

POST /v1/unsubscribe-groups. Scope required: unsubscribe_groups:manage.

Terminal window
curl -X POST https://app.pufferpost.com/v1/unsubscribe-groups \
-H "Authorization: Bearer key_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"code": "receipts", "name": "Receipts & invoices"}'

Fields:

  • code — a stable machine identifier you reference when sending. Max 64 chars; letters, numbers, hyphens and underscores only.
  • name — a human-readable label shown on the hosted unsubscribe page.

Response — 201 Created:

{
"id": "usg_01J9ZR4M5N6P7Q8R9S0T1U2V3W",
"code": "receipts",
"name": "Receipts & invoices",
"created_at": "2026-06-09T10:25:02+00:00"
}

A duplicate code in the same workspace returns 409 Conflict.

List topics

GET /v1/unsubscribe-groups. Scope required: unsubscribe_groups:read.

Terminal window
curl https://app.pufferpost.com/v1/unsubscribe-groups \
-H "Authorization: Bearer key_live_xxxxxxxx"

Response — 200 OK:

{
"data": [
{
"id": "usg_01J9ZR4M5N6P7Q8R9S0T1U2V3W",
"code": "receipts",
"name": "Receipts & invoices",
"created_at": "2026-06-09T10:25:02+00:00"
}
]
}

Delete a topic

DELETE /v1/unsubscribe-groups/{id}. Scope required: unsubscribe_groups:manage.

Terminal window
curl -X DELETE https://app.pufferpost.com/v1/unsubscribe-groups/usg_01J9ZR4M5N6P7Q8R9S0T1U2V3W \
-H "Authorization: Bearer key_live_xxxxxxxx"

Response — 204 No Content. Like suppressions, the id is workspace-scoped; an id outside your workspace returns 404.

Sending to a topic

Tag a send with a topic by passing its code as unsubscribeGroup in the POST /v1/messages body:

Terminal window
curl -X POST https://app.pufferpost.com/v1/messages \
-H "Authorization: Bearer key_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"from": "no-reply@acme.com",
"to": "jane@example.com",
"template": "receipt",
"data": { "order": "1234" },
"unsubscribeGroup": "receipts"
}'

When unsubscribeGroup is set, accept-time validation does two extra checks before queuing:

  • If no topic with that code exists in the workspace, the send is rejected with code unknown_unsubscribe_group.
  • If the recipient has opted out of that topic, the send is rejected with code recipient_unsubscribed.
{
"error": {
"code": "recipient_unsubscribed",
"message": "\"jane@example.com\" has unsubscribed from \"receipts\".",
"request_id": "req_01J9Z…"
}
}

A per-topic opt-out is scoped: the recipient stays eligible for your other topics and for untagged mail. By contrast, the global suppression list (above) blocks the recipient for everything. See Sending email for the full send payload.

Gotchas

  • Suppression is global within a workspace; topic opt-out is not. A recipient on the suppression list is blocked for every send. A recipient who opted out of one topic only loses mail tagged with that topic — untagged sends and other topics still go through.
  • Suppressed and unsubscribed sends never reach the queue. They’re rejected synchronously at POST /v1/messages with recipient_suppressed, recipient_unsubscribed, or unknown_unsubscribe_group — no msg_… id is created and nothing is enqueued.
  • The code, not the id, is what you send. unsubscribeGroup takes the topic’s code (e.g. receipts), never its usg_… id.
  • Auto-suppressions are reputation protection. Hard-bounce and complaint entries are created from real delivery events. Only delete one once you’re confident the address is deliverable again.
  • Manual adds are idempotent; topic codes must be unique. Re-suppressing an address is a no-op that returns the existing entry, but creating a topic with a code that already exists returns 409.
  • You never build the List-Unsubscribe header or host the unsubscribe page. Both are produced for you; tagging a send with a topic simply scopes what the recipient’s one-click opt-out removes.