Skip to main content
Every delivery is signed with Standard Webhooks using your endpoint’s whsec_… secret. Verify the signature before you trust the payload.
Verify the raw request body, before any JSON parsing. A parsed and re-serialized body won’t match the signature.

With the SDK

The SDK checks the signature and the timestamp, then returns the parsed event. It throws WebhookVerificationError if either check fails.

Without the SDK

  1. Build the signed content: {webhook-id}.{webhook-timestamp}.{raw body}.
  2. Strip whsec_ from the secret and base64-decode the rest to get the key.
  3. Compute HMAC-SHA256 of the signed content and base64-encode it.
  4. Compare v1,<result> with each space-separated value in webhook-signature, in constant time.
  5. Reject the request if webhook-timestamp is more than 5 minutes from your clock.
Off-the-shelf Standard Webhooks libraries work too.

Test locally

Send a correctly signed sample event to your handler with the CLI. Any whsec_ plus base64 value works as a test secret.

Respond and retry

  • Return any 2xx within 10 seconds. Do slow work after you respond.
  • Anything else is a failure: 3xx (redirects aren’t followed), 4xx, 5xx, a timeout, or a URL that now resolves to a private address.
  • Deliveries can repeat. Deduplicate on the webhook-id header, which stays the same across retries.
  • Order isn’t guaranteed. Use the event’s created_at, or fetch the invoice.
A failed delivery is retried at most 6 times. Each retry waits at least this long after the failed attempt: After the sixth retry fails, the delivery is dropped.
Deliveries currently go out once a day at 03:00 UTC, so in practice each retry happens on the next daily run, about a day apart.
After 20 dropped deliveries in a row, the endpoint is disabled: it shows "active": false and a disabled_at time, and stops receiving events. Any successful delivery resets the count. To start again, delete the endpoint and register it again.