Glossary

Webhooks

Webhooks are the standard way email providers report message lifecycle events (delivered, bounced, opened, unsubscribed) back to your application in real time. Without them you would have to poll an API to discover state changes. With them your system reacts in seconds.

One-line definition

Webhooks are server-to-server HTTP callbacks: when an event happens on a provider, the provider POSTs to a URL you registered, with a signed JSON payload describing the event.

Why webhooks beat polling

Polling means hitting an API on a schedule to ask "anything new?". For event-rich systems that scales badly (most calls return no new data) and the latency between event and reaction is whatever your poll interval is.

Webhooks invert the relationship: the provider knows when something happened and tells you immediately. Lower latency, lower request volume, simpler client code. The trade-off is you need a public HTTPS endpoint and a strategy for handling retries.

Signing and verification

Webhook receivers must verify that a request actually came from the provider — without verification, anyone who knows your endpoint URL can fire forged events at it. The standard mechanism is HMAC: the provider signs each payload with a shared secret, you verify the signature before processing.

SNDR signs every webhook with HMAC-SHA256 over <timestamp>.<rawBody>, delivered as X-Sndr-Signature: t=<ts>,v1=<hex>. The timestamp prefix defends against replay — your verifier rejects bodies whose timestamp is older than ~5 minutes.

POST /webhooks/sndr HTTP/1.1
Host: yourdomain.com
Content-Type: application/json
X-Sndr-Signature: t=1747556000,v1=4f8a3b6...

{"id":"evt_abc","type":"email.delivered","data":{"email_id":"em_xyz","to":["user@example.com"]},"created_at":"2026-05-18T12:00:00Z"}

Retries and idempotency on the receiver

Networks fail. Your receiver might be down for 30 seconds. Providers retry. SNDR retries up to 12 times with exponential backoff and full jitter, so an event your endpoint missed lands minutes (or hours) later.

That means your receiver MUST be idempotent — receiving the same event twice should produce the same end state as receiving it once. The cheapest way: persist event.id with a unique constraint and short TTL, and no-op the second receipt.

Raw body and the body-parsing trap

HMAC signatures are computed over the raw bytes of the request body. If your web framework parses JSON before you verify, you cannot reconstruct the original bytes — re-serialising the parsed object loses key ordering and whitespace, which breaks the signature.

Frameworks differ in how they expose raw bodies: Express needs express.raw({type:"application/json"}) instead of express.json(), Next.js Route Handlers expose request.arrayBuffer(), Django gives request.body, Rails gives request.raw_post. Verify first, then parse.

Replay and the freshness check

Even with a valid signature, an attacker who captured a webhook off the wire could replay it later. The fix is the timestamp prefix in the signed payload: the verifier rejects bodies whose timestamp is more than ~5 minutes old.

The 5-minute window is tunable. Some receivers — those running heavily delayed queue processing — relax it; rare cases that need replay (testing, recovery from a long outage) pass toleranceSeconds: 0 to disable the check entirely.

Related