Suppressions API
The Suppressions API manages the per-workspace lists of email addresses and phone numbers that Joryio will not send to. A suppression is a hard gate: while an identifier is suppressed on a channel, every message to it on that channel is skipped, regardless of which campaign or journey tries to reach it.
This API mirrors the Audience → Suppression Lists dashboard surface. Use it to audit who is suppressed, migrate an existing dead-address list from another platform, honor a consent request from your own systems, or lift a suppression after a hard-bounce is resolved.
All endpoints on this page are relative to the base URL: https://api-eu1.joryio.com - see API Overview.
Authentication
Every request is authenticated with either an API key carrying the relevant deliverability scope, or a dashboard session (JWT). Suppression data is workspace-scoped - a key only ever sees and edits its own workspace's lists.
Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json
Scopes per endpoint
The channel decides which scope is required. Email endpoints need the email_suppression:* scope; SMS and WhatsApp endpoints need the sms_suppression:* scope.
| Endpoint | Email channel | SMS / WhatsApp channel |
|---|---|---|
GET /suppressions | email_suppression:read | sms_suppression:read |
GET /suppressions/{identifier} | email_suppression:read | sms_suppression:read |
POST /suppressions | email_suppression:write | sms_suppression:write |
POST /suppressions/hard-bounce | email_suppression:write | - (email only) |
POST /suppressions/import | email_suppression:write | sms_suppression:write |
DELETE /suppressions/{identifier} | email_suppression:write | sms_suppression:write |
Core concepts
Read this section before you call the write endpoints - the rest of the API only makes sense once the reason vs source split is clear.
reason (why) vs source (where it came from)
Every suppression row carries two independent fields:
reason- why the identifier is suppressed. One ofunsubscribe,hard_bounce,complaint,manual.source- where the suppression came from (its provenance). One ofdelivery,api,import, or a consent source such asunsubscribe_link.
These are orthogonal. The same reason can arrive from different sources - a hard_bounce observed by our own send pipeline has source: "delivery", while a hard_bounce you assert through this API has source: "api". The reason tells you the deliverability/consent meaning; the source tells you how much to trust it and whether it counts toward your reputation metrics.
Consent reasons vs deliverability reasons
The four reasons split into two families that behave differently:
| Family | Reasons | Meaning | Survives a re-subscribe? |
|---|---|---|---|
| Consent | unsubscribe, manual | The recipient (or you, on their behalf) asked not to be contacted. | No - a fresh opt-in clears it. |
| Deliverability | hard_bounce, complaint | The address/number is dead or flagged us as spam. | Yes - it persists even if the recipient re-subscribes. |
A deliverability suppression is a technical fact about the address, not a preference, so a recipient re-subscribing does not lift it. It is lifted only by an explicit operator action: DELETE /suppressions/{identifier}, a dashboard "clear bounce", or the contact changing to a new email address.
You cannot fabricate a bounce
The ordinary write paths - POST /suppressions and POST /suppressions/import - clamp reason to manual or unsubscribe. They physically cannot create a hard_bounce or complaint row. A bounce is something the platform observes, not something a client asserts casually.
The one path that may assert a bounce is POST /suppressions/hard-bounce, and even then the row is stamped source: "api" so it is never confused with a bounce we saw ourselves.
Only real bounces touch your reputation
Your account's reported bounce rate and deliverability/reputation metrics count only source: "delivery" bounces - the ones our own send pipeline observed at the SMTP layer. An API-asserted (source: "api") or imported (source: "import") suppression blocks sending to that identifier but does not inflate your reported bounce rate. This lets you protect your sender reputation by pre-loading known-dead addresses without polluting the very metric you are trying to protect.
Suppression follows the address / number
A suppression is keyed on the normalized email address or phone number, per workspace - not on a contact record. One suppressed address therefore covers every duplicate contact that shares it. Suppress jane@example.com once and all contacts with that address are gated on email, workspace-wide.
Reason / source reference
reason values
reason | Family | Created by |
|---|---|---|
unsubscribe | Consent | Recipient opt-out link, POST /suppressions, POST /suppressions/import |
manual | Consent | Operator action, POST /suppressions, POST /suppressions/import |
hard_bounce | Deliverability | Our send pipeline, POST /suppressions/hard-bounce |
complaint | Deliverability | Our send pipeline (feedback loops) |
source values
source | Meaning | Counts toward reputation? |
|---|---|---|
delivery | Observed by our own send/receive pipeline (real bounce or complaint). | Yes |
api | Asserted through this REST API. | No |
import | Loaded via POST /suppressions/import (bulk migration). | No |
unsubscribe_link (and other consent sources) | Recipient-driven consent change. | No |
List suppressions
Return a paginated list of suppressed identifiers on a channel.
Endpoint
GET /suppressions
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
channel | string | - | email, sms, or whatsapp. Required. |
reason | string | - | Optional filter: unsubscribe, hard_bounce, complaint, or manual. |
limit | number | 100 | Rows per page (max 1000). |
offset | number | 0 | Pagination offset. |
Example request
curl -X GET "https://api-eu1.joryio.com/suppressions?channel=email&reason=hard_bounce&limit=50" \
-H "Authorization: Bearer jry_live_your_api_key"
Response
{
"items": [
{
"identifier": "dead-address@example.com",
"channel": "email",
"identifierType": "email",
"reason": "hard_bounce",
"source": "delivery",
"scope": "global",
"listId": null,
"createdAt": "2026-06-30T12:04:11.000Z"
},
{
"identifier": "jane@example.com",
"channel": "email",
"identifierType": "email",
"reason": "unsubscribe",
"source": "unsubscribe_link",
"scope": "group",
"listId": "grp_newsletter",
"createdAt": "2026-07-02T09:20:00.000Z"
}
],
"total": 214,
"limit": 50,
"offset": 0
}
Check one identifier
Check whether a single identifier is suppressed on a channel.
Endpoint
GET /suppressions/{identifier}
The path parameter {identifier} is the URL-encoded email address or phone number.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
channel | string | - | email, sms, or whatsapp. Required. |
Example request
curl -X GET "https://api-eu1.joryio.com/suppressions/dead-address@example.com?channel=email" \
-H "Authorization: Bearer jry_live_your_api_key"
Response - suppressed
{
"suppressed": true,
"identifier": "dead-address@example.com",
"reason": "hard_bounce",
"source": "delivery",
"scope": "global",
"listId": null,
"createdAt": "2026-06-30T12:04:11.000Z"
}
Response - not suppressed
{
"suppressed": false
}
Add a suppression
Add a consent or manual suppression. Use this to honor an opt-out that reached you through your own systems (a support ticket, a CRM flag, a preference-center change on your side).
Endpoint
POST /suppressions
Request body
| Field | Type | Required | Description |
|---|---|---|---|
channel | string | Yes | email, sms, or whatsapp. |
identifier | string | Yes | Email address or phone number to suppress. |
reason | string | No | manual (default) or unsubscribe. Clamped - any other value is rejected. |
scope | string | No | global (default) or group. |
listId | string | No | Required when scope is group: the group/list this suppression applies to. |
The source of a row created here is always recorded as api. This endpoint cannot create a hard_bounce or complaint.
Example request
curl -X POST https://api-eu1.joryio.com/suppressions \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"identifier": "jane@example.com",
"reason": "unsubscribe",
"scope": "group",
"listId": "grp_newsletter"
}'
Response
{
"identifier": "jane@example.com",
"channel": "email",
"identifierType": "email",
"reason": "unsubscribe",
"source": "api",
"scope": "group",
"listId": "grp_newsletter",
"createdAt": "2026-07-11T08:15:00.000Z"
}
Assert a hard bounce
Record a hard bounce for an email address. This is the only API path that may assert a deliverability suppression, and it is deliberately separate and constrained.
Why this endpoint is separate
- A bounce is normally something Joryio observes at send time, not something a caller declares. Keeping bounce assertion off the ordinary add/import paths prevents accidental or careless fabrication.
- Because you are asserting rather than us observing, the row is stamped
source: "api". It gates sending exactly like a real bounce but is never mistaken for one we saw ourselves, and it does not count toward your reported bounce rate or sender-reputation metrics. - It is email only. There is no phone equivalent - SMS/WhatsApp non-delivery is modeled differently.
Endpoint
POST /suppressions/hard-bounce
Request body
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | Yes | The email address that hard-bounced. |
Channel is implicitly email. The row is recorded with reason: "hard_bounce" and source: "api".
Example request
curl -X POST https://api-eu1.joryio.com/suppressions/hard-bounce \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"identifier": "no-such-mailbox@example.com"
}'
Response
{
"identifier": "no-such-mailbox@example.com",
"channel": "email",
"identifierType": "email",
"reason": "hard_bounce",
"source": "api",
"scope": "global",
"listId": null,
"createdAt": "2026-07-11T08:20:00.000Z"
}
Bulk import
Load an existing suppression list - for example, when migrating off another platform.
Endpoint
POST /suppressions/import
Request body
| Field | Type | Required | Description |
|---|---|---|---|
channel | string | Yes | email, sms, or whatsapp. |
entries | array | Yes | Up to 5000 objects, each { identifier, reason? }. |
Each entry's reason is clamped to manual (default) or unsubscribe; any other value is coerced to manual. Every imported row is recorded with source: "import". Like the add endpoint, import cannot create a bounce or complaint.
Example request
curl -X POST https://api-eu1.joryio.com/suppressions/import \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"entries": [
{ "identifier": "old-dead-1@example.com" },
{ "identifier": "opted-out@example.com", "reason": "unsubscribe" },
{ "identifier": "old-dead-2@example.com" }
]
}'
Response
{
"channel": "email",
"received": 3,
"imported": 3,
"skipped": 0,
"source": "import"
}
Remove a suppression (unsuppress)
Remove a suppression so Joryio may send to the identifier again.
Endpoint
DELETE /suppressions/{identifier}
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
channel | string | - | email, sms, or whatsapp. Required. |
This removes all suppression rows for the identifier on the channel in this workspace - including a hard_bounce or complaint deliverability row. That is a deliberate operator/API lift: unsuppressing an address is exactly how you clear a resolved hard bounce. Only lift a deliverability suppression when you know the underlying problem is fixed, or you risk sending to a dead address and hurting your sender reputation.
Example request
curl -X DELETE "https://api-eu1.joryio.com/suppressions/no-such-mailbox@example.com?channel=email" \
-H "Authorization: Bearer jry_live_your_api_key"
Response
{
"identifier": "no-such-mailbox@example.com",
"removed": 2
}
removed is the number of suppression rows deleted (a single identifier can carry a group-scoped consent row and a global deliverability row at once).
Migrating an existing suppression list
When you move to Joryio from another email or SMS platform, bring your suppression list with you on day one so your first send does not re-mail addresses you already know are dead or opted-out.
- Import the whole list via
POST /suppressions/import. Entries arrive asmanual(orunsubscribeif you tag them), withsource: "import". They block sending and protect your sender reputation on the first send - without inflating your reported bounce rate, because imported rows never count toward reputation. - Only if you specifically need those addresses to report as bounces - for example to keep your bounce analytics continuous across the migration - assert them individually with
POST /suppressions/hard-bounce. They will still be stampedsource: "api", so they gate sending and show as bounces in your suppression list without being counted as bounces we observed.
For most migrations, step 1 alone is the right call: it stops the sends and keeps your reputation metrics clean.
Error responses
All errors share the standard shape - there is no separate machine-readable error-code vocabulary; use the HTTP status plus the message field. See Error Response in the API Overview. Validation failures (400) add an errors array with one message per failed field:
{
"statusCode": 400,
"message": "Bad Request Exception",
"timestamp": "2026-07-12T09:00:00.000Z",
"path": "/suppressions",
"errors": [
"reason must be one of the following values: manual, unsubscribe"
]
}
Notable: this API enforces channel-precise scopes. An API key that holds only the SMS scope gets a 403 when it touches email suppressions (and vice versa):
{
"statusCode": 403,
"message": "API key missing required scope 'email_suppression:write' for channel 'email'",
"timestamp": "2026-07-12T09:00:00.000Z",
"path": "/suppressions"
}