Sending is half the story. Providers also report back — delivery confirmations, opens,
clicks, bounces, spam complaints — via webhooks. postboi/webhooks receives those the
same way postboi sends: one normalized shape, any provider. Point your provider’s
webhook at an endpoint, hand postboi the request, and get typed events out — signature
verification included.
Like mail(), receive() is zero-config: the provider comes from POSTBOI_PROVIDER / postboi.config.ts / a POSTBOI_TOKEN, and the signing secret from the provider’s <PROVIDER>_WEBHOOK_SECRET env var. Both can be passed explicitly:
SvelteKit
On SvelteKit, use the ready-made handler from postboi/kit — it verifies, normalizes,
calls your handler once per event, and answers the provider correctly (200 on success,
401 on a bad signature, 400 on a bad payload, 500 when your handler throws so the
provider retries):
The event shape
Every provider’s payload normalizes to a WebhookEvent:
Who opened it, and on what
On opens and clicks, most providers report the recipient’s user-agent. postboi parses it locally (a pure function — no lookup service, nothing leaves your server) into:
So event.client answers “opened in Apple Mail on an iPhone” out of the box. Two honest
caveats: proxied opens (Gmail, Yahoo fetch the pixel on the recipient’s behalf) identify
the mailbox provider but hide the device, and Apple Mail Privacy Protection means open
events generally are an approximation, whatever the provider.
Verification
Verification is fail-closed: if no secret is configured, receive() throws rather
than silently accepting unauthenticated requests. Every comparison is timing-safe, and
schemes with timestamps get replay protection.
Providers fall into three camps:
SMTP, Microsoft 365 and Cloudflare don’t emit delivery-event webhooks — receive() throws webhooks_not_supported for them.
To skip verification deliberately (a local experiment, a payload replay), pass { verify: false } — it’s always an explicit opt-out, never a fallback.
Testing without a provider
You don’t need a tunnel or a real provider to test your handler. mock_event builds a
normalized event; mock_request builds a full, correctly signed HTTP request:
Custom providers
receive() accepts a custom adapter for anything postboi doesn’t cover — implement verify and normalize and pass it as provider: