Theme

Webhooks

How a sending provider tells your Mimeo what happened to a message it took — delivered, bounced, complained, suppressed.

POST /webhooks/email/:provider

One route per configured adapter. Verification belongs to the adapter, because how a provider proves a request is its own differs per provider.

Verification

Provider How
Resend Svix signature headers — svix-id, svix-timestamp, svix-signature — HMAC-SHA256 over id.timestamp.body, with a five-minute freshness window. Set the signing secret under Settings → Provider → Credentials.
Postmark Postmark can't sign, so the claim is its webhook secret in the URL: ?secret=…, compared in constant time against provider.postmark.webhook_secret — set under Settings → Provider → Credentials.
Providers that can't sign A shared secret in the URL: ?secret=…, compared in constant time against provider.webhook_secret.

A provider that signs does not fall back to the shared secret. A signature is a much stronger claim, and accepting the weaker one from a provider that offers the stronger would undo the point of checking.

Responses

Status When
200 Understood — including an event type we don't act on. Retrying a no-op helps nobody.
401 Verification failed.
404 No adapter by that name.
400 The body isn't parseable. A retry of the same bytes won't fix it.
500 Genuinely failed to process. Please retry.

The internal vocabulary

Adapters normalize their provider's own event names into five, and nothing above the adapter ever sees a provider's vocabulary:

Type Effect
delivered The send is marked delivered.
bounced Recorded with a bounce_kind. A hard bounce suppresses the person; a soft one (a full mailbox) does not — that's a try-again, not a dead address.
complained Suppresses the person. Reason complaint.
unsubscribed Suppresses the person.
suppressed The provider suppressed them on its side. Suppresses locally too.

A suppression from a webhook is recorded with source provider_webhook, which stops it being pushed straight back to the provider that just reported it.

At-least-once and unordered

Assumed of every provider, whatever they promise. Handling is built for it:

If a provider lacks them

A provider without webhooks would be polled instead. Either way the local database is the authority and the standing guard checks it before every send — a provider without webhooks is slower to reconcile, not less safe.

See also: Write your own adapter · Postmark API notes · Resend API notes