Endpoints other systems call. None of them is reachable through the SPA; all are secret-guarded and answer in the standard envelope.
| Method | Path | Caller | Purpose |
|---|---|---|---|
POST |
/api/_webhooks/postmark/bounce |
Postmark | Record hard bounces and spam complaints on the member's private profile. |
Postmark's Bounce webhook.
Configured on the Postmark server that sends our notifications, with either
HTTP basic auth (any username, password = the secret) or an Authorization: Bearer <secret> header. The secret is POSTMARK_WEBHOOK_SECRET.
-
If
POSTMARK_WEBHOOK_SECRETis unset →503 not_configured(mirrors the reload webhook: the route exists, the feature is off). -
Authenticate: bearer token or basic-auth password must equal the secret (constant-time compare). Otherwise
401 unauthenticated. -
Accept only
RecordType = "Bounce"(and Postmark'sSpamComplaintvariant, which arrives on the same webhook withType = "SpamComplaint"). Anything else →200with{ "ignored": true }, so Postmark does not retry. -
Resolve
Email(case-insensitive) to a person through the private store's email index. Unknown address →200 { "matched": false }. -
Store on the private profile:
"emailBounce": { "type": "HardBounce", "bouncedAt": "…", "description": "…", "inactive": true }
Only bounce types that mean "this mailbox is not going to work" are kept:
HardBounce,SpamComplaint,SpamNotification,Blocked,DnsError,BadEmailAddress,ManuallyDeactivated,Unsubscribe. Transient types (Transient,SoftBounce,DMARCPolicy, …) are acknowledged and dropped. A later successful delivery does not clear the record; staff can see the date and judge. -
Respond
200 { "matched": true, "recorded": <bool> }. Never 4xx/5xx on a well-formed, authenticated payload — Postmark retries on failure and the data is advisory.
A hard bounce is the one reliable "this mailbox is disabled" signal we get, and we get it for free from mail we already send. It replaces any temptation to probe mailboxes over SMTP, which is unreliable and gets the sending IP listed.
- behaviors/private-storage.md — the
emailBouncefield. - api/moderation.md — surfaced on the roster as a signal.