Skip to main content
Webhooks tell your systems the moment something happens in your workspace: a deal was created, a lender replied, an offer came in, a bank statement finished analysing, an advance is ready to renew. Each notification is delivered to a web address you choose, signed so you can be sure it came from us. They are the trigger for anything automated, from a Slack ping to a full underwriting agent, and they replace asking the API “has anything changed?” over and over.

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 has webhooks: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.received and submission.responded into Slack, Teams or a text, with the lender name and terms.
  • Stuck-deal follow-up. deal.stage_changed starts or stops a follow-up cadence in your CRM or dialer.
  • Underwriting automation. statement.analyzed re-runs lender matching and submits to the lenders that fit.
  • Renewal pipeline. advance.renewal_ready opens 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 a POST 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
In Express, read the body with 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.