Best practices
Build webhook handlers that are fast, safe and correct under retries.
1 min read
Respond fast, work later#
Return 2xx as soon as you have verified the signature and stored the event. Put the real work on a queue. A handler that calls other APIs before responding will eventually hit the 10-second timeout and trigger retries for events you already processed.
Verify every request#
Check the Listener-Signature on every request, against the raw body, with a timestamp tolerance. Reject anything that fails. See Verify signatures.
Handle duplicates#
Delivery is at least once. Store the id of each event you process (or use it as a unique key) and skip ids you have already handled. The same id is also in the Listener-Event-Id header, so you can deduplicate before parsing.
Do not rely on order#
Retries mean an older event can arrive after a newer one. Compare created timestamps, or treat events as a signal to re-read the current state with a GET. For example, on campaign.updated, fetch GET /v1/campaigns/{id} rather than applying previous_attributes blindly.
Subscribe to what you need#
List specific types in enabled_events instead of *. You get less traffic, and your handler is not surprised by new event types added later. If you do use *, ignore types you do not recognise and return 2xx for them.
Reconcile with the Events API#
Webhooks can be missed: your endpoint was disabled, a deploy dropped requests, a queue lost messages. Run a periodic job that lists recent events with created_gte and processes any you have not seen. That turns webhooks from “usually right” into “eventually always right”.
Keep secrets secret#
Store each endpoint’s signing secret in a secrets manager. Roll it with POST /v1/webhook-endpoints/{id}/roll-secret if it might have leaked; the previous secret stays valid for 24 hours while you deploy.
Use separate endpoints per environment#
Register one endpoint for production and another for staging, each with its own secret, so a staging bug never processes production events twice.