Suppression list

The addresses nothing will reach on this workspace, why each one is there, and the two that are not yours to clear.

The suppression list is the set of addresses this workspace will not send to. A send to one is refused with 422 recipient_suppressed rather than attempted, and an import writes such an address as cleaned rather than subscribed. It is the single most load-bearing piece of deliverability machinery here, and it works by saying no.

Addresses arrive on it on their own. A hard bounce, a spam complaint, and an unsubscribe all add one without you doing anything, which is exactly what you want: continuing to mail an address that bounced is the fastest way to lose a sending reputation, and the fastest way to do that by accident is to have to remember to stop.

Read it

GET /v1/suppressions
# Everything, newest first
curl "https://emails.sh/v1/suppressions?limit=200" \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"

# Just the complaints
curl "https://emails.sh/v1/suppressions?reason=complaint" \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"

# Ask about one address
curl "https://emails.sh/v1/suppressions?email=ada@example.com" \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"
200 OK
{
  "suppressions": [
    {
      "id": "a6c40b98-7f21-4d53-9e08-3b15c7a2f640",
      "email": "grace@example.com",
      "reason": "bounce",
      "reason_detail": "smtp; 550 5.1.1 Recipient address rejected: User unknown",
      "is_global": false,
      "created_at": "2026-07-02T14:31:09.220Z"
    },
    {
      "id": "e19d7350-24ba-4c86-b0f7-5a83e6c14297",
      "email": "spamtrap@example.net",
      "reason": "complaint",
      "reason_detail": "feedback loop",
      "is_global": true,
      "created_at": "2026-05-18T07:02:44.006Z"
    }
  ],
  "next_cursor": "2026-05-18T07:02:44.006Z",
  "total": 1180
}

Pagination is a cursor: take next_cursor from a response and pass it back as before. It stops when next_cursor is null. limit defaults to 50 and takes anything from 1 to 200.

reasonHow it got there
bounceThe receiving server permanently refused it. reason_detail carries the remote server's own words.
complaintSomebody pressed the spam button and the receiver told us through a feedback loop.
unsubSomebody unsubscribed at the workspace level rather than from one topic.
manualYou added it, or support did.

The global rows

A row with is_global set is ours rather than yours: known spam traps, addresses that complain across every workspace, and the handful of things that are never a good idea to mail. They appear in your list so nothing is invisible, and DELETE on one answers 404. You cannot clear them and neither can support.

Add one yourself

Somebody who asks you to stop by replying to the email, rather than by clicking the link, has unsubscribed just as much as anybody else. Record it.

POST /v1/suppressions
curl -X POST https://emails.sh/v1/suppressions \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "ada@example.com", "reason": "manual"}'

Clear one

Deliberately, one at a time, and only when you know something changed. A typo somebody has now fixed is a good reason. "The campaign is going out tomorrow" is not.

DELETE /v1/suppressions/:id
# By id, taking the id off a row in GET /v1/suppressions
curl -X DELETE https://emails.sh/v1/suppressions/a6c40b98-7f21-4d53-9e08-3b15c7a2f640 \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"

# Or by address, when the address is what you have
curl -X DELETE "https://emails.sh/v1/suppressions?email=ada@example.com" \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"

Both forms lift exactly one row and answer { "deleted": ... }. DELETE /v1/suppressions?id= is the older spelling of the first one and still works, so nothing already written has to change; the id in the path is the URL to write now. A global row, a row on another workspace, and an id that never existed all answer 404 with the same sentence, because none of them is something this workspace can lift and none of them is state you should be able to discover.

Import one before you send

If you are arriving from another provider, import their suppression list before your first real send. Those addresses already bounced or complained somewhere else, and mailing them from a new setup is the most reliable way to get a domain filtered in its first week. The migration commands in /docs/migrate-resend, /docs/migrate-postmark, /docs/migrate-sendgrid, and /docs/migrate-mailgun each do it first, before they touch anything else.

GET /v1/suppressions

?reason=&email=&limit=&before= over blocked addresses.

POST /v1/suppressions

{ email, reason? } blocks one yourself.

DELETE /v1/suppressions/:id

Lift one by id. A global row answers 404, and so does another workspace's.

DELETE /v1/suppressions

?email= clears one by address. ?id= is the older spelling of the route above and still works.

suppressions.list

Addresses nothing will reach on this workspace, and why each one is there. Rows with is_global set are ours rather than yours.

Arguments

reason "bounce" | "complaint" | "unsub" | "manual"
email string
Ask about one address.
limit number
Defaults to 50, from 1 to 200.
before string
The next_cursor from the previous page.

Returns{ suppressions[], next_cursor, total }

suppressions.create

Block an address yourself, for somebody who asked you to stop by replying rather than by clicking.

Arguments

email string required
reason string
Defaults to manual.

ReturnsSuppression

suppressions.delete

Clear one, when you know the address is good again. A global row answers 404 and cannot be cleared.

Arguments

id string
Give id or email.
email string

Returns{ deleted }