Create a subscription
In the app (Settings → Developers → Webhooks → Add endpoint), or with the API using a credential that haswebhooks:manage:
Response 201
url must be an absolute HTTPS endpoint (http://localhost and http://127.0.0.1 are accepted for local development; hosts that resolve to private or loopback addresses are rejected). An empty events array subscribes to every event, including types added later. GET /v1/webhooks lists subscriptions with their health (status, failing_since, last_success_at, last_failure_at) and without secrets, DELETE /v1/webhooks/{id} removes one and returns { deleted: true, id }, and POST /v1/webhooks/{id}/test sends a webhook.test event so you can check your endpoint end to end.
Event types
webhook.test is sent only by the test endpoint and the test button in the app.
Payload
id (evt_…, stable across retries), type, created_at, location_id and data. data carries the ids you need for routing plus a compact snapshot of the object concerned:
The snapshots (
deal, submission, offer, document, advance, business, person) are a fixed subset of the REST fields: ids, names, amounts, statuses and timestamps. They never include form_data, SSNs, dates of birth, tokens or CRM ids, and the offer snapshot carries status_id rather than the status slug. Treat the payload as a notification, not the source of truth: fetch the resource (GET /v1/offers/{id} and so on) when you need the full, current record.
Headers on every delivery:
Verify the signature
The signature is HMAC-SHA256 with your subscription secret over`${t}.${rawBody}`: the timestamp from the header, a dot, and the exact request body bytes. Verify before you parse, compare in constant time, and reject anything whose timestamp is more than five minutes off your clock. During a secret rotation the header can carry more than one v1= value; accept the delivery when any of them matches.
Delivery and retries
- Deliveries are
POSTrequests with a 10 second timeout. Any2xxstatus is a success; anything else is a failure: a non-2xxstatus, a redirect (redirects are not followed), a connection error or a timeout. - Failed deliveries are retried 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours after the previous attempt (six attempts in all), then marked dead.
- If a subscription keeps failing for 3 days, it is set to
disabled(withdisabled_reason) and the location’s admins receive an email. Fix the endpoint, then re-enable it from Settings → Developers → Webhooks. - Deliveries are at-least-once: they are not guaranteed to arrive in order, and an event can be delivered more than once.
- Test events (
POST /v1/webhooks/{id}/test) are delivered synchronously, signed like real events and never retried. The response reports the single attempt:delivery_id,event_type,status(succeededordead),response_status, the first 2 KB ofresponse_body,errorandduration_ms.
Replay
Settings → Developers → Webhooks shows every delivery per endpoint with the event type, the attempts and your endpoint’s response. Any delivery, including dead ones, can be replayed from there, which is how you catch up after an outage or test a fix against real payloads.Consume idempotently
- Dedupe on the event
id. Keep the ids you have handled for a day and ignore repeats. - Return
2xxas soon as the signature checks out and the event is persisted; do the real work asynchronously. Slow handlers hit the 10 second timeout and cause retries. - Do not rely on order. Use
created_atand, where it matters, refetch the resource before acting on it. - Return a non-
2xxstatus only when you actually want a retry.