Suppressions
Managing the bounce, spam-report, unsubscribe, and suppression-group lists through the API. For the conceptual overview — what each list means and when addresses are added automatically — read Suppressions first.
Base paths:
/v3/suppression/bounces
/v3/suppression/spam_reports
/v3/asm/suppressions/global (global unsubscribes)
/v3/asm/groups (suppression groups)
Auth: API key or JWT; suppressions.read for reads, suppressions.write for modifications.
Bounces
List bounces
| curl "https://scaisend.scailabs.ai/v3/suppression/bounces?limit=100&start_time=1704067200" \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
| import os, httpx
resp = httpx.get(
"https://scaisend.scailabs.ai/v3/suppression/bounces",
headers={"Authorization": f"Bearer {os.environ['SCAISEND_API_KEY']}"},
params={"limit": 100, "start_time": 1704067200},
)
bounces = resp.json()["bounces"]
|
| const resp = await fetch(
"https://scaisend.scailabs.ai/v3/suppression/bounces?limit=100&start_time=1704067200",
{ headers: { "Authorization": `Bearer ${process.env.SCAISEND_API_KEY}` } },
);
const { bounces } = await resp.json();
|
Query parameters:
| Parameter |
Notes |
limit |
Results per page (default 50, max 500) |
offset |
Pagination offset |
start_time / end_time |
Unix timestamp range |
email |
Filter by a specific recipient |
Response:
1
2
3
4
5
6
7
8
9
10
11
12 | {
"bounces": [
{
"email": "invalid@example.com",
"created": 1714060800,
"reason": "550 5.1.1 User unknown",
"bounce_type": "hard",
"status": "5.1.1"
}
],
"total": 152
}
|
Add a bounce manually
Useful during migration — import bounce lists from your previous provider so you don't re-send to addresses that have already bounced.
| curl -X POST https://scaisend.scailabs.ai/v3/suppression/bounces \
-H "Authorization: Bearer $SCAISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "invalid@example.com",
"bounce_type": "hard",
"reason": "Imported from SendGrid, 2026-03-01"
}'
|
bounce_type values: hard, soft, block. The effect is the same — ScaiSend won't send to the address — but bounce_type is stored for your reporting.
Bulk import
| # CSV body with columns: email, reason (optional)
curl -X POST https://scaisend.scailabs.ai/v3/suppression/bounces/import \
-H "Authorization: Bearer $SCAISEND_API_KEY" \
-H "Content-Type: text/csv" \
--data-binary @bounces.csv
|
Response includes counts and per-row errors:
| {
"imported": 1482,
"skipped": 12,
"errors": [
{"row": 44, "email": "not-an-email", "error": "Invalid email format"}
]
}
|
Remove a bounce
Re-enable sending to an address:
| curl -X DELETE https://scaisend.scailabs.ai/v3/suppression/bounces/invalid@example.com \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
204 on success. No body.
Bulk delete all bounces:
| curl -X DELETE "https://scaisend.scailabs.ai/v3/suppression/bounces?delete_all=true" \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
The delete_all=true query parameter is required — without it, you get 400. Use cautiously.
Spam reports
Same endpoints, different list:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | # List
curl https://scaisend.scailabs.ai/v3/suppression/spam_reports \
-H "Authorization: Bearer $SCAISEND_API_KEY"
# Get a specific recipient's report(s)
curl https://scaisend.scailabs.ai/v3/suppression/spam_reports/abuser@example.com \
-H "Authorization: Bearer $SCAISEND_API_KEY"
# Add manually (rare; usually populated by FBL)
curl -X POST https://scaisend.scailabs.ai/v3/suppression/spam_reports \
-H "Authorization: Bearer $SCAISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "abuser@example.com", "source": "support_ticket_#4821"}'
# Remove (rare; removing a spam report is a policy decision, not cleanup)
curl -X DELETE https://scaisend.scailabs.ai/v3/suppression/spam_reports/abuser@example.com \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
Global unsubscribes
These are the most powerful suppression — a globally unsubscribed address receives nothing from this tenant.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | # List everyone who's globally unsubscribed
curl https://scaisend.scailabs.ai/v3/asm/suppressions/global \
-H "Authorization: Bearer $SCAISEND_API_KEY"
# Check a specific address
curl https://scaisend.scailabs.ai/v3/asm/suppressions/global/user@example.com \
-H "Authorization: Bearer $SCAISEND_API_KEY"
# -> 200 with {"recipient_email": "user@example.com"} if suppressed, 404 if not
# Add (e.g., responding to a "remove me from everything" ticket)
curl -X POST https://scaisend.scailabs.ai/v3/asm/suppressions/global \
-H "Authorization: Bearer $SCAISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipient_emails": ["user@example.com", "another@example.com"]}'
# Remove (they asked back in)
curl -X DELETE https://scaisend.scailabs.ai/v3/asm/suppressions/global/user@example.com \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
Suppression groups
Groups let recipients opt out of a type of email rather than all email.
Create a group
| curl -X POST https://scaisend.scailabs.ai/v3/asm/groups \
-H "Authorization: Bearer $SCAISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Newsletter",
"description": "Weekly roundup and product updates",
"is_default": false
}'
|
Response:
| {
"id": 123,
"name": "Newsletter",
"description": "Weekly roundup and product updates",
"is_default": false,
"unsubscribes": 0,
"created_at": "2026-04-23T10:00:00Z"
}
|
The numeric id is what you pass in asm.group_id on a send.
List groups
| curl https://scaisend.scailabs.ai/v3/asm/groups \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
Update a group
| curl -X PATCH https://scaisend.scailabs.ai/v3/asm/groups/123 \
-H "Authorization: Bearer $SCAISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "Updated description"}'
|
Delete a group
| curl -X DELETE https://scaisend.scailabs.ai/v3/asm/groups/123 \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
Deletes the group and all per-group suppressions. Messages already sent with this group_id still exist; the preference page they link to will show the group as "deleted" to any late clicker.
List addresses suppressed in a group
| curl "https://scaisend.scailabs.ai/v3/asm/groups/123/suppressions?page=1&page_size=100" \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
Response:
| {
"recipient_emails": ["opted-out@example.com", "another@example.com"],
"total": 2
}
|
Add to a group
| curl -X POST https://scaisend.scailabs.ai/v3/asm/groups/123/suppressions \
-H "Authorization: Bearer $SCAISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipient_emails": ["opt-out@example.com", "also-opt-out@example.com"]}'
|
Remove from a group
| curl -X DELETE https://scaisend.scailabs.ai/v3/asm/groups/123/suppressions/opt-out@example.com \
-H "Authorization: Bearer $SCAISEND_API_KEY"
|
The recipient is re-subscribed to this group. A group_resubscribe event is emitted.
Sending with a group
| {
"personalizations": [{"to": [{"email": "user@example.com"}]}],
"from": {"email": "hello@mail.example.com"},
"subject": "This week in Acme",
"content": [{"type": "text/html", "value": "<html>..."}],
"asm": {
"group_id": 123,
"groups_to_display": [123, 124, 125]
}
}
|
group_id is the primary group this message belongs to. If the recipient is in this group's suppression list, the message is dropped with reason suppressed_group_unsubscribe.
groups_to_display lists the groups to show on the unsubscribe preference page. Use it to let a recipient manage their subscriptions in one click.
Recipes
Clean import from another provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | import csv
import os
import httpx
api_key = os.environ["SCAISEND_API_KEY"]
base = "https://scaisend.scailabs.ai"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
with open("sendgrid-export.csv") as f:
reader = csv.DictReader(f)
for row in reader:
suppression_type = row["type"]
email = row["email"]
if suppression_type == "bounce":
httpx.post(
f"{base}/v3/suppression/bounces",
headers=headers,
json={"email": email, "bounce_type": "hard", "reason": "Imported"},
)
elif suppression_type == "spam":
httpx.post(
f"{base}/v3/suppression/spam_reports",
headers=headers,
json={"email": email, "source": "migration"},
)
elif suppression_type == "unsubscribe":
httpx.post(
f"{base}/v3/asm/suppressions/global",
headers=headers,
json={"recipient_emails": [email]},
)
|
Or use the bulk import endpoints — faster on large lists:
| curl -X POST https://scaisend.scailabs.ai/v3/suppression/bounces/import \
-H "Authorization: Bearer $SCAISEND_API_KEY" \
-H "Content-Type: text/csv" \
--data-binary @sendgrid-bounces.csv
|
Weekly suppression-list audit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | from datetime import datetime, timedelta
import os, httpx
api_key = os.environ["SCAISEND_API_KEY"]
base = "https://scaisend.scailabs.ai"
week_ago = int((datetime.now() - timedelta(days=7)).timestamp())
resp = httpx.get(
f"{base}/v3/suppression/bounces",
headers={"Authorization": f"Bearer {api_key}"},
params={"start_time": week_ago, "limit": 500},
)
new_bounces = resp.json()["bounces"]
print(f"{len(new_bounces)} new bounces this week")
for b in new_bounces:
if b["bounce_type"] == "soft":
print(f" soft: {b['email']} — {b['reason']}")
|
Soft bounces are candidates for eventual removal (mailbox was temporarily full; the recipient is probably fine now). Hard bounces should stay.
What's next