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. The signing secret comes back once; store it like an API key:Response (200)
PUT /webhook again replaces the URL and issues a fresh secret; that’s also the rotation mechanism.
Then prove the loop works end to end:
ping event to your endpoint. If your handler verifies it and returns 2xx, you’re done with setup.
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
| Event | Fires when | data |
|---|---|---|
ping | You call POST /webhook/test. | {} |
quote.finalized | An in_review quote (above the rate card’s in-review threshold, default 200 employees) is finalized by underwriting and becomes ready. | quoteId, groupId, status |
enrollment.created | An enrollment is created for one of your groups. | enrollmentId, groupId, quoteId, startDate, companyDomain, onboardingMode |
group.state_changed | The provisioned company changes state, most importantly sandbox → prod (the group is live). | groupId, companyDomain, previousState, state |
census.processed | A census write finishes processing. | groupId, received, accepted, memberCount, errorCount |
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
Delivery is best-effort with 3 retries on exponential backoff (1s, 5s, 25s) after the initial attempt. A delivery counts as successful on any 2xx response. Exhausted deliveries are recorded on our side and surfaced to your partner engineer, but they are not queued durably in v1. Build your handler accordingly:- Return 2xx fast (under 10 seconds). Enqueue work; don’t do it inline.
- Dedupe on
event.id. Retries reuse the sameid, and you should treat deliveries as at-least-once. - 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.