Getting events sent to your own system
What we post, how to check it came from us, what happens when your endpoint is down, and the three rules for writing the receiving end.
What arrives
A POST with a JSON body: a `type`, a `timestamp`, and a `data` object holding what happened — which code, its name, where in the world, what kind of device. Small on purpose.
What is never in it: no address, no user agent, no visitor identifier. Those are the things we promise the person who scanned that we do not keep, and that promise does not weaken because the recipient is a system you chose.
Checking it came from us
Three headers travel with every delivery: `webhook-id`, `webhook-timestamp` and `webhook-signature`. The signature is HMAC-SHA256 over `id.timestamp.body`, base64, prefixed `v1,`.
This is the Standard Webhooks format, which means you do not have to write any of that yourself — there are verification libraries for about ten languages, and the same ones that verify OpenAI, Clerk and Resend will verify us. Give the library your signing secret and the raw request body.
Use the **raw** body, not a re-serialised copy. A signature is over exact bytes, so reformatting the JSON before checking it will fail every time.
Three rules for the receiving end
**Answer within ten seconds.** We give up after that and count it a failure. Return a 2xx immediately and do your work afterwards.
**Expect the same event twice.** If your endpoint handles a delivery but we never hear the response, we cannot tell that apart from it never arriving — so we try again. Keep the `webhook-id` values you have already handled and ignore repeats.
**Do not rely on the order.** Events can arrive out of the order they happened in, particularly after a retry. Nobody in this category guarantees ordering, ourselves included.
When your endpoint is down
We try again: immediately, after 15 seconds, 2 minutes, 15 minutes, an hour, and then every six hours for up to three days. Each attempt is signed afresh, so the timestamp is always current.
Anything that is not a 2xx is a failure, **including a redirect**. Give us the final address rather than one that forwards, because we deliberately do not follow them.
We email you after 5, 10 and 15 failures in a row, and switch the endpoint off at 20. You will always have been told three times first — an endpoint that stops working silently is worse than one that stops working.
Switching it back on resets the count to zero.
Changing the secret
Press New secret and the old one keeps working for 24 hours. During that window we send both signatures, space-separated in the same header, so you can change your configuration without dropping anything. Any library that follows the specification already tries each one.
If something looks wrong
The delivery log keeps 30 days: what was sent, what came back, and the response code. Send again resends the exact bytes of the original rather than a fresh copy, so it is genuinely the same message.
Send a test posts a real, signed delivery — it proves your endpoint is reachable, verifies signatures, and answers in time. A simulated one would only prove the button worked.