Mail guides
05 · Deliverability

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

GET /workspaces/:ws/suppression
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.

POST /workspaces/:ws/suppression
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.

DELETE /workspaces/:ws/suppression/:email
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 includes bounce_type.
  • message.complained — recipient hit “spam”.
  • message.deferred — temporary failure, will retry.
  • message.opened — tracking pixel loaded (optional, requires track_opens: true on send).
  • message.link_clicked — link click recorded (optional, track_clicks: true).
PUT /workspaces/:ws/webhooks/default
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.

Bounce event
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-Unsubscribe header 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.