Suppressions
Docs / Control plane

Suppressions

A suppression is an address you have asked us never to mail again — bounced, complained, or added by hand. List with filters, look one up, add one, remove one. Removal is audited. An entry that exists because of an erasure is locked — removing it would let us mail an erased person again.

Authentication

Authorization: Bearer spw_

Read paths require suppressions:read. Create and delete require suppressions:write.

Endpoints

GET/v1/suppressions list · ?email · ?reason · ?limit · ?cursor
GET/v1/suppressions/lookup by address · ?email= required
POST/v1/suppressions add · body: email, reason, detail?
GET/v1/suppressions/{id} show one row
DELETE/v1/suppressions/{id} remove · audited · may 409

reason on create is one of manual, bounce, complaint, unsubscribe. Email is lowercased. Re-adding an address already on the list returns 200 with already_suppressed: true, not a second row.

List

Keyset page of every live suppression on this account. Filter with ?email= or ?reason=. Same cursor shape as every other list.

curl "https://spoolway.com/api/v1/suppressions?limit=25" \
  -H "Authorization: Bearer spw_…"
200 listcaptured LOCAL
{
  "data": [
    {
      "id": 1,
      "email": "[email protected]",
      "reason": "manual",
      "source": "api",
      "detail": "support ticket 12",
      "suppressed_at": "2026-08-06T08:11:01+00:00",
      "expires_at": null,
      "erasure_locked": false
    }
  ],
  "next_cursor": null
}

Show

One row by id. Same body as lookup, but the key is the id, not the address. Cross-account is 404.

curl https://spoolway.com/api/v1/suppressions/1 \
  -H "Authorization: Bearer spw_…"
200 showcaptured LOCAL
{
  "data": {
    "id": 1,
    "email": "[email protected]",
    "reason": "manual",
    "source": "api",
    "detail": "support ticket 12",
    "suppressed_at": "2026-08-06T08:11:01+00:00",
    "expires_at": null,
    "erasure_locked": false
  }
}

Worked example

curl -X POST https://spoolway.com/api/v1/suppressions \
  -H "Authorization: Bearer spw_…" \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]", "reason": "manual", "detail": "support ticket 12" }'
201 Createdcaptured LOCAL
{
  "data": {
    "id": 1,
    "email": "[email protected]",
    "reason": "manual",
    "source": "api",
    "detail": "support ticket 12",
    "suppressed_at": "2026-08-06T08:11:01+00:00",
    "expires_at": null,
    "erasure_locked": false
  }
}
curl "https://spoolway.com/api/v1/suppressions/[email protected]" \
  -H "Authorization: Bearer spw_…"
200 lookup
{
  "data": {
    "id": 1,
    "email": "[email protected]",
    "reason": "manual",
    "source": "api",
    "detail": "support ticket 12",
    "suppressed_at": "2026-08-06T08:11:01+00:00",
    "expires_at": null,
    "erasure_locked": false
  }
}
200 already suppressed
{
  "data": {
    "id": 1,
    "email": "[email protected]",
    "reason": "manual",
    "source": "api",
    "detail": "support ticket 12",
    "suppressed_at": "2026-08-06T08:11:01+00:00",
    "expires_at": null,
    "erasure_locked": false
  },
  "already_suppressed": true
}
curl -X DELETE https://spoolway.com/api/v1/suppressions/1 \
  -H "Authorization: Bearer spw_…"
200 deleted
{
  "message": "[email protected] removed — sending to it is allowed again."
}

Erasure lock

When a person is erased, their identity survives on the suppression list as an erased:… HMAC so the account can never mail them again. Those rows set erasure_locked: true. DELETE against one returns 409 with type: erasure_locked. Do not retry — the lock is the point.

409erasure locked
response body
{
  "message": "This entry survives an erasure request. Removing it would defeat the erasure, so it is locked.",
  "type": "erasure_locked"
}
What to do: stop. Removing the row would defeat the erasure. There is no override over the API.

Responses & failures

401bad or missing key
response body
{ "message": "Unauthenticated" }
What to do: mint or restore Authorization: Bearer with the key prefix from brand config.
403key lacks suppressions:read or write
response body
{ "message": "Forbidden" }
What to do: mint a key with the ability you need — abilities never widen after mint.
404lookup: address not suppressed (or expired)
response body
{
  "message": "Not suppressed"
}
What to do: treat as clear to send. Lookup deliberately 404s rather than returning an empty 200.
409erasure_locked
response body
{
  "message": "This entry survives an erasure request. Removing it would defeat the erasure, so it is locked.",
  "type": "erasure_locked"
}
What to do: do not retry. The identity must stay suppressed.