SonarSend API Contacts & audience Suppression

Suppression

The suppression list is a hard block on sending. An address on it receives nothing from your workspace, regardless of lists, segments, or sequences.

It is deliberately separate from a contact’s subscription state: unsubscribing is a preference attached to a contact record, while suppression is a bare address that we refuse to send to even if no contact exists for it. Use suppression for compliance obligations and hard bounces; use subscriptions for “this person doesn’t want the newsletter”.

Required scopes#

EndpointScope
GET /api/suppressionssuppression:read
POST /api/suppressionssuppression:write
DELETE /api/suppressionssuppression:write
POST /api/suppressions/bulksuppression:write

suppression:write implies suppression:read.

List suppressed addresses#

GET /t/{tenant_slug}/api/suppressions
X-API-Key: sonar_xxxxx
[
  {
    "tenant_id": "…",
    "email": "blocked@example.com",
    "reason": "hard_bounce",
    "created_at": "2026-07-01T12:00:00.000Z"
  }
]

Returns the whole list — there is no pagination on this endpoint today. If yours is large enough for that to matter, tell us.

Suppress an address#

curl -X POST "https://app.example.com/t/acme/api/suppressions" \
  -H "X-API-Key: $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"email":"blocked@example.com","reason":"complaint"}'

Returns 201 with the created row. reason is free text and optional; it is for your own auditing, and nothing branches on its value.

The list is keyed on (tenant, email), so re-suppressing an address already on the list is harmless — it upserts rather than erroring. Note that it replaces the stored reason, so re-suppressing without one clears the previous value.

Addresses are lowercased and trimmed on the way in, and on lookup. You do not need to normalize case yourself, and Blocked@Example.com will match a stored blocked@example.com.

Un-suppress an address#

curl -X DELETE "https://app.example.com/t/acme/api/suppressions?email=blocked@example.com" \
  -H "X-API-Key: $API_KEY"

Returns 204. The address goes in the query string, not a path segment, because it contains @ and dots. Remember to URL-encode it.

Idempotent: removing an address that is not on the list is also a 204. Case is normalized here too.

Removing a suppression does not resubscribe anyone. It only lifts the block. If the contact is also unsubscribed, they stay unsubscribed — that is a separate piece of state, and lifting a compliance block should never silently opt someone back in.

Suppress many contacts at once#

curl -X POST "https://app.example.com/t/acme/api/suppressions/bulk" \
  -H "X-API-Key: $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["…","…"],"reason":"sunset"}'
{ "suppressed": 2 }

Takes contact IDs, not addresses — it is the “act on a selection of contacts” path, matching the sunset-candidates flow in the app. For each contact it adds the address to the suppression list and flips the contact to unsubscribed.

Limits: 1–500 contact IDs per call. Over 500 returns 400 TOO_MANY; batch larger jobs client-side.

reason defaults to "sunset" here, unlike the single-address endpoint where it is simply null.

Choosing the right endpoint#

You haveUse
An email address, no contact recordPOST /api/suppressions
A contact who asked to be removed entirelyPOST /api/suppressions/bulk
A contact who wants fewer emails, not noneSubscriptions on the contact — see contacts.md

Errors#

CodeStatusMeaning
TOO_MANY400More than 500 contact_ids in one bulk call.
INSUFFICIENT_SCOPE403The key is missing suppression:write.

See errors.md for the full envelope.