events table, and a worker fans those rows out to subscribed endpoints with signing and retries.
Endpoints are subscribed in the dashboard (Developers → Webhooks), not through the public API — so a leaked
sk_ key can’t quietly redirect a merchant’s event stream. The signing secret is shown once on creation.Fan-out from the events table
Decoupling delivery from the event source means a merchant’s slow or down endpoint never blocks the operation that produced the event. The producer just appends a row; the worker does the rest, exactly once per (event, endpoint) thanks to adispatchedAt marker and jobId dedup.
Signing — HMAC over a timestamped payload
Every delivery carries aDuro-Signature header the merchant verifies:
WebhookSigner in @duro/crypto) is the same primitive used for the docs’ verification guide.
Retries with exponential backoff
A non-2xx (or a network error) schedules a retry:pending with a future nextRetryAt until it succeeds (→ delivered) or exhausts the offsets (→ failed). Every attempt records the response status and a truncated body, visible in the dashboard’s delivery inspector, which also offers a one-click replay.
SSRF guard — the security teeth
A webhook URL is attacker-controlled (the merchant types it), and the response body is stored and shown back in the inspector. That’s a textbook SSRF-to-exfiltration vector: point the URL athttp://169.254.169.254/… and read cloud-metadata credentials out of the delivery log. Duro blocks it at two layers:
- When saved in the dashboard: require
https, reject loopback, RFC-1918 private ranges, link-local169.254/16, CGNAT100.64/10, ULA IPv6, and hostnames likelocalhost,*.internal,metadata.google.internal— for IPv4 and (bracketed) IPv6. - At delivery: re-resolve the host’s DNS and reject if it now points at a private IP — defeating DNS-rebinding, where a public hostname flips to an internal address after it passed the creation check.
The event catalog
Event names are canonical and consistent — one underscore convention, defined once inDuroEvent and emitted by the state machine and services, so a subscription to subscription_payment_success actually matches what’s sent. The full catalog with payloads is in the API reference.
Next: the queue & worker topology that runs all of this.