Suppressions

The suppression list is the memory of who you must not contact. Sending to a suppressed address is refused before it leaves the queue.

List and manage

GET /email/suppressions lists suppressions; POST adds a destination by hand, with reason MANUAL or INVALID_ADDRESS; DELETE removes one you are sure about. Hard bounces, complaints and unsubscribes add themselves automatically, and entries born from a recipient's own choice cannot be deleted through the API (removable: false).

cURL
curl -X POST https://api.mailer.quathos.com/api/v1/email/suppressions \
  -H "Authorization: Bearer $QUATHOS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "destination": "user@example.com", "reason": "MANUAL" }'
json
{
  "data": [
    {
      "id": "019f7155-2c40-7b11-a9d3-6e2b8f014c55",
      "destination": "user@example.com",
      "channel": "email",
      "reason": "HARD_BOUNCE",
      "detail": "550 5.1.1 mailbox unavailable",
      "removable": true,
      "created_at": "2026-07-17T18:05:36Z"
    }
  ],
  "next_cursor": null
}
A suppression carries the reason it exists.

Reasons

ReasonOrigin
HARD_BOUNCEThe recipient server rejected the address permanently.
SPAM_COMPLAINTThe recipient marked a message as spam. Not removable.
UNSUBSCRIBEThe recipient opted out of your email. Not removable.
MANUALYou added the destination yourself.
INVALID_ADDRESSYou marked the destination as invalid.
PLATFORM_BLOCKBlocked by the platform. Not removable.
SOFT_BOUNCE_ESCALATEDFive soft bounces (mailbox full, temporary failure) in 14 days with no delivery in between. Temporary: expires after 30 days, and you can remove it earlier.
Removing a suppression does not undo the reason it was added. Delete a hard-bounce entry only if you know the address is now valid: sending to dead addresses is what wrecks a sender reputation, and reputation does not roll back.

Export as CSV

GET /email/suppressions/export.csv streams the whole list as a CSV attachment, with the columns email, reason, created_at, expires_at, source, detail — in that order. Two optional filters narrow it: reason= for a single reason, and since= for everything added at or after an ISO-8601 instant. There is no row limit and no pagination to walk: the file is the list.

The export needs the same read scope as listing suppressions, and it is recorded in your account audit trail as suppressions.exported, with who downloaded it and when.

Your suppression list is never held hostage. Export it before you need it, keep it somewhere you control, and take it with you if you ever leave: the columns above are the same ones our import reads back.

Import from another provider

POST /migration/suppressions/csv uploads a CSV and applies it in the request. If the header row matches an exporter we recognise, the reason comes from the file itself instead of defaulting to MANUAL — the job report names the provider and the list it came from.

CSV headerRead as
email,created,reason,statusSendGrid bounces → HARD_BOUNCE
email,createdSendGrid unsubscribes → UNSUBSCRIBE
address,code,error,created_atMailgun bounces → HARD_BOUNCE
address,tag,created_atMailgun unsubscribes → UNSUBSCRIBE
address,created_atMailgun complaints → SPAM_COMPLAINT
EmailAddress,Reason,LastUpdateTimeSES suppression list → per row, from Reason (BOUNCE, COMPLAINT)

The match is on the exact header, in the order the exporter writes it. Anything else — your own spreadsheet, an export with an extra column — is read the old way: the email column is found by name and every row lands as MANUAL, which you can remove yourself.

An unsubscribe or a spam complaint that arrives by import is as final as one raised here: it came from the recipient, not from your account, so DELETE refuses it. That is the point of detecting the format — importing a complaint list as MANUAL would hand you a button to undo somebody else's decision.