Docs
FBL Ingest
PostMTA Hosted accepts FBL (complaint) feedback via a single shared-secret
endpoint and (Release B) drives suppressions from it.
Endpoint
POST /v1/fbl/ingest requires X-FBL-Secret header matching
FBL_INGEST_SECRET.
{
"provider": "postmark",
"feedback_type": "abuse",
"recipient_hash": "sha256:redacted@example.com",
"raw": { /* free-form provider payload */ }
}
Returns 200 { ok: true, id } or 401 { error: 'unauthorized' }.
Storage
Rows land in fbl_events with the panel Realtime channel (when enabled) so
the FBL panel section refreshes without polling.
Release B — FBL → Suppressions (M55)
scripts/fbl-poll.ts (or the in-process poller if RUN_FBL_POLLER=1)
walks fbl_events created in the last FBL_POLL_WINDOW_HOURS (default
24) and upserts suppressions rows. Idempotent on
(workspace_id, address, reason). The dry-run path prints what it would
upsert; --apply hits Supabase.
# dry-run, prints candidate suppressions
node --experimental-strip-types scripts/fbl-poll.ts
# apply (requires SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY)
node --experimental-strip-types scripts/fbl-poll.ts --apply
Release B — Admission reject (M56)
runSend and the SMTP path call checkSuppressed at the top of every
send. If any recipient address is on the suppression list, the call
short-circuits:
| Path | Behaviour on suppressed address |
|---|---|
POST /v1/send / POST /v1/panel/send |
422 SUPPRESSED with { suppressed: [...], reasons: [...] } |
| SMTP DATA | 550 5.7.1 Recipient suppressed: <addr> (fbl,bounce) |
Mock provider honours MOCK_SUPPRESS_ADDRESS to keep the smoke suite
deterministic. Live provider uses the partial index
btree(workspace_id, address) for sub-ms admission.
Operational notes
- Never store plaintext recipient addresses off-box; ship a hash.
- HMAC-sign the secret in your provider integration; rotate via
FBL_INGEST_SECRETenv flip + smoke-prod.
Related
Last verified: Sat Jul 11 2026 17:00:00 GMT-0700 (Pacific Daylight Time). Cross-link maintained byscripts/check-docs-drift.sh (M54).