Land in the inbox. Stay there.
Bounce handling, complaint handling, the suppression list, webhook config, and the hygiene practices that keep your sender reputation healthy.
1
The suppression list
Workspace-scoped. Hard wall — once an address is on it, Mail refuses to send regardless of sender or template. Goal: never send to an address that has bounced or complained, both for the recipient's benefit and for your sender reputation.
Entries land on the list:
- Automatically on hard bounce (mailbox doesn't exist, domain rejects).
- Automatically on complaint (recipient hits “Mark as spam”).
- Automatically after 5 soft bounces in 7 days (transient → treated as persistent).
- Manually via
POST /workspaces/:ws/suppression(your unsubscribe form posts here).
2
Listing + filtering
curl 'https://api.mail.productcraft.co/v1/workspaces/<ws>/suppression?reason=bounce&limit=50' \
-H 'authorization: Bearer pcft_live_...'{
"data": [
{
"email": "bounced@example.com",
"reason": "bounce_hard",
"added_at": "2026-04-12T...",
"metadata": { "bounce_code": "550", "diagnostic": "user unknown" }
}
],
"pagination": { "next_cursor": "...", "has_more": true }
}3
Manual add (unsubscribe)
When a recipient hits unsubscribe on your form, POST to the suppression list. Idempotent — already-suppressed addresses get a 200, not a duplicate row.
curl https://api.mail.productcraft.co/v1/workspaces/<ws>/suppression \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-d '{
"email": "userwhowantsout@example.com",
"reason": "manual_unsubscribe"
}'4
Remove from suppression
Rarely needed. A customer changes their mind, an address is wrongly suppressed (false-positive bounce), an internal test address.
curl -X DELETE https://api.mail.productcraft.co/v1/workspaces/<ws>/suppression/userwhowantsback@example.com \
-H 'authorization: Bearer pcft_live_...'5
Webhooks — learn what happened
Mail's send is async. The webhook is how your system learns about delivery, bounces, complaints, opens. Subscribe to the events you care about; ignore the rest.
Events:
message.delivered— receiver accepted the message.message.bounced— soft or hard bounce. Payload includesbounce_type.message.complained— recipient hit “spam”.message.deferred— temporary failure, will retry.message.opened— tracking pixel loaded (optional, requirestrack_opens: trueon send).message.link_clicked— link click recorded (optional,track_clicks: true).
curl -X PUT https://api.mail.productcraft.co/v1/workspaces/<ws>/webhooks/default \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-d '{
"url": "https://acme.example.com/webhooks/mail",
"events": ["message.bounced", "message.complained"]
}'Per-domain webhooks override the workspace default for that domain. Useful if you want bounce events for staging to land somewhere different than prod.
6
Webhook payload + signature
Same shape as Auth webhooks (HMAC-SHA256 of body keyed by webhook secret, timestamp header, retry on non-2xx). See Auth guide 08 for the signature verification snippet — it's drop-in compatible.
POST https://acme.example.com/webhooks/mail
x-mail-event: message.bounced
x-mail-delivery: whd_a1b2c3
x-mail-timestamp: 1778499378
x-mail-signature: t=1778499378,v1=...
{
"event": "message.bounced",
"occurred_at": "...",
"data": {
"message_id": "msg_...",
"to": "bounced@example.com",
"from": "noreply@acme.com",
"bounce_type": "hard",
"bounce_code": "550",
"diagnostic": "user unknown",
"template": "welcome",
"metadata": { "user_id": "...", "trigger": "signup" }
}
}7
Reputation hygiene
- Don't send to addresses you haven't verified. Customer signs up → ask them to verify before sending anything beyond the verification email itself. Bounces on unverified addresses dent your domain's reputation across receivers.
- Watch your bounce rate. Above 2% is the widely-cited threshold for AWS / SendGrid. Receivers start filtering. Mail shows the rolling 7-day rate per domain — set up an alert at 1.5%.
- Don't reuse a long-suspended domain without warming it up. If you stop sending for months then resume at full volume, receivers treat the spike as suspicious. Ramp over 7-14 days.
- One sender intent per mailbox.
noreply@for operational mail,support@for inbound,billing@for receipts. Mixing intents on one mailbox (transactional + marketing + replies) confuses receivers' per-mailbox reputation tracking. - Respect the unsubscribe header. Mail auto-includes an
List-Unsubscribeheader with a one-click suppression URL. Don't override it. Customers without one get filtered to spam by Gmail and outright rejected by some corporate receivers.
8
When things go wrong
Symptoms + first-look:
- “My emails are landing in spam” — check the DMARC report (sent to your
rua=address). If SPF or DKIM are failing for any of your sends, that's the cause. Most often: missing or wrong DKIM record. - “Send is returning 422 RECIPIENT_SUPPRESSED” — the address is on the list. Hit
GET /workspaces/:ws/suppression?email=...to see why and when. - “Bounces aren't firing webhooks” — check webhook config + attempt history. If webhooks are auto-disabled (5 consecutive failures), Mail stopped firing.
- “A specific recipient never gets mail” — they might be on the suppression list, OR their corporate filter is rejecting silently. Use the message log (Chapter 6) to confirm delivery status before suspecting the customer.