What you get told about
Each notification carries the ids you need and a compact snapshot of the record: names, amounts, statuses and dates. It never carries application data, social security numbers, dates of birth or CRM ids. Treat it as the nudge, not the source of truth: look the record up when you need the full, current picture.
Subscribing
In the app, go to Settings → Developers → Webhooks → Add endpoint, paste your HTTPS address and tick the events you want (or none, for all of them, including any added later). You get a signing secret once; keep it. The same page shows every delivery with your endpoint’s response, lets you send a test event, rotate the secret, and replay any delivery, which is how you catch up after an outage. A developer can do the same through the API with a credential that haswebhooks:manage.
When your endpoint is down
- We keep trying: 1 minute, 5 minutes, 30 minutes, 2 hours and then 6 hours after each failed attempt, before giving up on that delivery.
- If every delivery to an endpoint has failed for three days, the subscription is switched off and the workspace admins get an email. Fix the endpoint, then switch it back on from Settings.
- Nothing is lost. Deliveries that gave up can be replayed from Settings at any time.
What to build with it
- Rep alerts.
offer.receivedandsubmission.respondedinto Slack, Teams or a text, with the lender name and terms. - Stuck-deal follow-up.
deal.stage_changedstarts or stops a follow-up cadence in your CRM or dialer. - Underwriting automation.
statement.analyzedre-runs lender matching and submits to the lenders that fit. - Renewal pipeline.
advance.renewal_readyopens a task or a conversation with the merchant. - Reporting. Mirror every event into your warehouse for dashboards that update as the day goes on.
For developers
Envelope. Every delivery is aPOST with the same JSON body: id (evt_…, stable across retries), type, created_at, location_id and data. data carries routing ids plus a snapshot of the object concerned:
Snapshots are a fixed subset of the REST fields; the
offer snapshot carries status_id rather than the status slug. Fetch the resource (GET /v1/offers/{id} and so on) for the full record.
Subscribe by API. POST /v1/webhooks with url, events (empty array for all) and an optional description; the response includes secret once. url must be absolute HTTPS (http://localhost and http://127.0.0.1 are accepted for local development; private and loopback hosts are otherwise rejected). GET /v1/webhooks lists subscriptions with their health (status, failing_since, last_success_at, last_failure_at) without secrets; DELETE /v1/webhooks/{id} removes one; POST /v1/webhooks/{id}/test delivers a webhook.test event synchronously (even to a disabled subscription), signed like a real event and never retried, and returns the single attempt: delivery_id, event_type, status (succeeded or dead), response_status, the first 2 KB of response_body, error and duration_ms.
Headers on every delivery. Content-Type: application/json, User-Agent: NextLevelMCA-Webhooks/1.0, X-NLMCA-Event (the type), X-NLMCA-Delivery (the delivery id; retries carry the same one) and X-NLMCA-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>.
Verify the signature. HMAC-SHA256 with your secret over `${t}.${rawBody}`, the timestamp, a dot and the exact request bytes. Hash the raw body before parsing (re-serialised JSON breaks the HMAC), 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=; accept the delivery when any matches.
verify.js
express.raw({ type: 'application/json' }) so the bytes reach the verifier untouched, answer 200 as soon as the signature checks out and the event is stored, and do the real work afterwards.
Delivery rules. Deliveries time out after 10 seconds; any 2xx is a success, and a non-2xx status, a redirect (not followed), a connection error or a timeout is a failure that follows the retry schedule above, then dead. Deliveries are at-least-once and not guaranteed in order, so dedupe on the event id (keep handled ids for a day), use created_at rather than arrival order, refetch the resource before acting where it matters, and return a non-2xx only when you actually want a retry.