Outbound Webhooks
When we notify other systems of events by calling their URL (outbound webhooks), we play two roles. We are a sender that must be trustworthy, and a client calling an untrusted destination. Sign what we send so receivers can verify it is really us. Deliver reliably with retries. And treat the target URL itself as a possible attack vector.
Outbound webhooks are the opposite of the inbound ones we receive (see Third-Party Integrations). As the sender, we owe receivers authenticity (a signature they can verify), reliability (retries, ordering hints, deduplication), and care about what we put in the payload. There is also a less obvious risk: the destination URL is data an attacker can influence. Calling it without checks can enable SSRF (server-side request forgery) into our own network.
Design webhooks as a small, durable delivery system, not as a send-and-forget HTTP call inside a request.
Send trustworthy, useful events
- AlwaysSign outbound webhook payloads (for example, HMAC with a per-subscriber secret, plus a timestamp), so receivers can verify they are genuine and reject forgeries and replays.
- DoInclude a stable event id and type so receivers can deduplicate and handle events idempotently. Send minimal data (ids or references), not full sensitive payloads.
- DoVersion the event schema and evolve it additively, so subscribers do not break (see Backward Compatibility).
- NeverPut secrets, full PII, or special-category data in a webhook body. Send a reference and let the receiver fetch it over an authorised API (see Data Classification).
Deliver reliably and safely
- DoDeliver out of band (queue or background job) with bounded retries and backoff, and a dead-letter or visibility path for endpoints that stay down (see Background Jobs, Asynchronous Messaging).
- DoSet timeouts on the outbound call and isolate it (circuit breaker), so a slow or hostile receiver cannot tie up our resources (see Third-Party Integrations).
- DoValidate and restrict destination URLs to prevent SSRF. Block internal and link-local addresses and require HTTPS, so a configured webhook URL cannot reach our internal network.
- ConsiderLetting subscribers see delivery status and retry, and giving them a way to rotate their signing secret.
- AvoidSending webhooks synchronously inside a user request. A slow receiver then slows or fails the user's action.
// inside the request, no signature, URL straight from config
await http.PostAsync(subscriber.Url, fullCustomerJson);
Receivers cannot verify it is us, and we sent full PII. A slow receiver stalls the user's request. And if the URL points at an internal address, we have made a request into our own network for an attacker (SSRF).
EnqueueDelivery(new Event { Id, Type, CustomerRef }); // background, retried
// on send: assert HTTPS + non-internal host; add HMAC signature + timestamp
The event is signed and carries only a reference. Delivery is durable and retried off the request path. The destination is validated, so it cannot be abused for SSRF.
Self-review checklist
- AskCan the receiver verify this webhook genuinely came from us (signature + timestamp)?
- AskIs there any sensitive data in the body that should be a reference instead?
- AskIs delivery durable (queued, retried, dead-lettered) and off the user's request path?
- AskCould the destination URL be pointed at our internal network (SSRF)?