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
| Cause | Reason on the list | Scope |
|---|---|---|
| A permanent (hard) bounce | bounce | The whole account |
| A spam complaint | complaint | The whole account |
| An unsubscribe from marketing mail | unsubscribe | The message's topic, or the whole account (the recipient chooses) |
| Added through the API | manual | A 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:
List-Unsubscribe: <https://api.owlpost.to/v1/emails/unsubscribe/{token}>
List-Unsubscribe-Post: List-Unsubscribe=One-ClickThe 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:
POSTwith the bodyList-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.
{
"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 -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.
{
"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).