Skip to main content
Webhooks push events from your location to an HTTPS endpoint you control: a deal was created, a lender replied, a statement finished analysing. They are the trigger layer for anything automated. Poll the API only for what webhooks do not tell you.

Create a subscription

In the app (Settings → Developers → Webhooks → Add endpoint), or with the API using a credential that has webhooks:manage:
Response 201
secret is returned once, on creation. Store it now; you need it to verify signatures. You can rotate it later from Settings → Developers, which invalidates the old one.
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

Every event has the same envelope: 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.
The usual mistake is parsing the JSON and re-serialising it before hashing: any change in whitespace or key order breaks the HMAC. Always hash the bytes as received.

Delivery and retries

  • Deliveries are POST requests with a 10 second timeout. Any 2xx status is a success; anything else is a failure: a non-2xx status, 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 (with disabled_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 (succeeded or dead), response_status, the first 2 KB of response_body, error and duration_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 2xx as 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_at and, where it matters, refetch the resource before acting on it.
  • Return a non-2xx status only when you actually want a retry.