Docs · Suppression

Suppression and unsubscribes

Owlpost keeps a suppression list per account and checks it before every send. Hard bounces and spam complaints go on it by themselves, and so does everyone who unsubscribes from marketing mail. You can read it, add to it and remove from it.

What gets suppressed

CauseReason on the listScope
A permanent (hard) bouncebounceThe whole account
A spam complaintcomplaintThe whole account
An unsubscribe from marketing mailunsubscribeThe message's topic, or the whole account (the recipient chooses)
Added through the APImanualA topic, or the whole account

A temporary (soft) bounce is not suppressed: a full mailbox today may be fine tomorrow.

What a suppression blocks. Suppressed recipients are dropped when the message is sent, not when it is accepted, so POST /v1/emails still answers 200. If every recipient is suppressed, the message ends failed (with an email.failed webhook for live mail).

  • Marketing mail is blocked by any account-wide suppression, and by one for its own topic.
  • Transactional mail is blocked by account-wide bounces, complaints and manual suppressions, but not by an unsubscribe: leaving a newsletter must not cost someone their receipts. Topic suppressions never block it.

One-click unsubscribe (RFC 8058)

Every message on the marketing stream carries two headers that mail clients turn into an "Unsubscribe" button:

headers
List-Unsubscribe: <https://api.owlpost.to/v1/emails/unsubscribe/{token}>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

The token is signed and names the account, the recipient and the topic, so it needs no key and cannot be altered. Owlpost serves both sides of it:

  • POST with the body List-Unsubscribe=One-Click (what a mail client sends) unsubscribes at once, with no page in between, as RFC 8058 requires. For a topic message this is a topic-only unsubscribe.
  • GET (a person clicking the link) shows a small confirmation page with a button. For a topic message there are two: "Unsubscribe from {topic name}" and "Unsubscribe from all mail from {sender}".

Marketing mail goes to one recipient per message so that each link unsubscribes exactly one person. To mail a list, send a batch.

Topics

A topic Owlpost lets one account (say, a platform sending for many publishers) scope an unsubscribe to one list. Send with "stream": "marketing" and "topic": "project:news" (1 to 64 of a-z 0-9 : _ -). Topics are refused on transactional mail.

PUT/v1/emails/topics/{topic}Built

Scope emails:send. Body {"name": "Acme release digest"} (1 to 200 characters). The unsubscribe page shows this name instead of the raw id. Answers {"object": "topic", "id": "project:news", "name": "Acme release digest"}; calling it again renames.

The suppression API

GET/v1/emails/suppressionsBuilt

Scope emails:read. Up to 1,000 entries, newest first. ?topic=project:news lists one topic's entries only.

response · 200
{
  "object": "list",
  "data": [
    { "address": "old@example.org", "topic": null, "reason": "bounce",
      "email_id": "em_01j9…", "created_at": "2026-10-03T09:00:00Z" },
    { "address": "ada@example.org", "topic": "project:news", "reason": "unsubscribe",
      "email_id": null, "created_at": "2026-10-02T17:12:40Z" }
  ]
}

topic is null for an account-wide entry. email_id names the message that bounced or drew the complaint.

POST/v1/emails/suppressionsBuilt

Scope emails:send. Body {"address": "…", "topic": "…"}, topic optional. Answers 201 with {"address", "topic", "reason": "manual"}. Adding an entry that already exists changes nothing.

DELETE/v1/emails/suppressions/{address}Built

Scope emails:send. Removes one entry: the account-wide one, or the topic's with ?topic=. An address that complained can only be removed with a reason query parameter saying why (400 otherwise); it is logged. Answers {"address": "…", "deleted": true}, or 404 when there is no such entry.

curl
curl -X DELETE "https://api.owlpost.to/v1/emails/suppressions/ada%40example.org?topic=project:news" \
  -H "Authorization: Bearer $OWLPOST_API_KEY"

The email.unsubscribed webhook

Fired when a new unsubscribe or manual suppression is added: through the one-click POST, the page, or the API. Adding one that already exists fires nothing. Bounces and complaints fire email.bounced and email.complained instead.

data
{
  "email_id": null,
  "to": "ada@example.org",
  "topic": "project:news",
  "scope": "topic",
  "source": "one-click",
  "occurred_at": "2026-10-03T09:00:00Z"
}

scope is topic or account; source is one-click, page or api.

Erasing one recipient's data across an account, while keeping a hashed suppression so they are not mailed again, is Planned (#9).