prod, an enrollment landing.
Signatures follow the standard-webhooks scheme (the same one used by Svix and a growing list of providers), so you can verify with an off-the-shelf library or ~10 lines of code.
Setup
Register your HTTPS endpoint. It must be a publichttps:// URL whose hostname resolves only to public addresses: loopback, private, link-local and internal hostnames or addresses (such as localhost, *.internal or 10.0.0.0/8) are rejected. Prescience checks the address again before each delivery. The signing secret comes back once; store it like an API key:
Response (200)
PUT /webhook again with the same mode replaces the URL and issues a fresh secret; that’s also the rotation mechanism. POST /webhook/test pings the endpoint for your key’s mode.
Then prove the loop works end to end:
ping event to your endpoint and waits for the result:
Response (200)
statusCode is your endpoint’s response, or null on a network error or timeout. The ping is listed by GET /events but is not retried automatically.
Delivery format
Every event is aPOST with a JSON body and three signature headers:
whsec_4f8a1c2b9d3e6f7a8b9c0d1e2f3a4b5c. Wire up your verifier against it as a unit test before touching production traffic.
Event catalog
Full payload schemas are in the API reference under Webhooks.
Verifying signatures
The signature is an HMAC-SHA256 over{webhook-id}.{webhook-timestamp}.{raw body}:
- Key: strip the
whsec_prefix from your secret and base64-decode the remainder; the resulting bytes are the HMAC key. - Signed content: concatenate the
webhook-idheader, thewebhook-timestampheader, and the raw request body, joined by.. Verify against the exact bytes you received, never a re-serialized parse. - Compare: base64-encode the HMAC and compare (constant-time) against each space-separated
v1,<base64>candidate inwebhook-signature. Any match passes.
Retries and reliability
Every event is stored before it is sent, so an event is never lost because of a failure on either side. The first attempt runs right after the change that caused it. A delivery counts as successful on any 2xx response within 10 seconds; redirects are not followed. When an attempt fails, the event is retried with the sameid on this schedule after each failure: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. That is 8 attempts over about two days. After the last one the event is marked dead; it stays listed and you can send it again.
Build your handler accordingly:
- Return 2xx fast (under 10 seconds). Enqueue work; don’t do it inline.
- Dedupe on
event.id. Retries and redeliveries reuse the sameid, so treat deliveries as at-least-once. - Don’t rely on ordering. A retried event can arrive after a newer one. Read the current state from the API before acting on it.
- Don’t treat webhooks as the source of truth. They’re a prompt, not a ledger: on anything critical (or after downtime), reconcile by polling
GET /groups,GET /quotes/{id}, andGET /enrollments/{id}. Every webhook-announced fact is readable from the API.
Reconciling missed events
GET /events lists the events for your key’s mode, newest first, with each one’s delivery state. Filter with ?type= and page with ?limit= and ?cursor=:
Response (200)
GET /events/{eventId} returns one event. POST /events/{eventId}/redeliver sends it again now with the same id and returns the updated delivery state; a failed or dead event also restarts its retry schedule. Events created before you registered a webhook are kept, so after setup you can list them and redeliver the ones you need.