Webhook & CRM integration

Every delivered lead is POSTed to your endpoint as a signed JSON payload — in parallel with portal and email delivery. This is exactly what our platform sends.

Last updated August 2, 2026

Overview

When a lead is delivered to your account, we immediately send an HTTP POST to the webhook URL configured in Settings. The request body is JSON, and it is signed so you can verify it came from us and was not altered. Add your webhook URL in the portal and verify it with the test button before going live.

Request headers

HeaderDescription
Content-Typeapplication/json
X-BBL-TimestampUnix time in seconds when the request was signed. Used in the signature and for replay protection.
X-BBL-SignatureLowercase hex HMAC-SHA256 of `${timestamp}.${rawBody}`, keyed with your webhook secret.
X-BBL-Delivery-IdThe unique delivery ID. Use it to make processing idempotent.
X-BBL-ReplayPresent and set to true only when the request is a manual replay of a previous delivery.

The lead.delivered payload

The body has this shape:

{
  "event": "lead.delivered",
  "delivery_id": "e3b0c442-98fc-1c14-9afb-4c8996fb9242",
  "lead_id": "9f1a2b3c-4d5e-6f70-8a9b-0c1d2e3f4a5b",
  "vertical": "dscr",
  "delivered_at": "2026-08-02T18:24:07.512Z",
  "lead": {
    "...": "the full borrower field payload for this vertical"
  },
  "trustedform_cert_url": "https://cert.trustedform.com/...",
  "consent_version": "v1.0"
}

lead is the full field payload — the same record you see in the portal and email. The fields inside it vary by vertical. Manual replays include an additional "replay": true field.

Verifying the signature

Recompute the HMAC over `${X-BBL-Timestamp}.${rawRequestBody}` using your webhook secret and compare it to X-BBL-Signature with a constant-time comparison. You must hash the raw request body bytes — do not re-serialize the parsed JSON, or whitespace differences will break the match. Reject requests whose timestamp is too old (for example, more than five minutes) to prevent replay of captured payloads.

import crypto from "node:crypto";

function verify(rawBody, headers, secret) {
  const ts = headers["x-bbl-timestamp"];
  const sig = headers["x-bbl-signature"];

  // Reject stale timestamps (replay protection).
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");

  // Constant-time compare.
  const a = Buffer.from(sig, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Responding

Return a 2xx status once you have accepted the payload. Any non-2xx response, a timeout (we abort after 10 seconds), or a connection error is treated as a failed attempt. Process asynchronously if your handling is slow — acknowledge first, work after.

Retries

The first delivery attempt is made inline the moment the lead is distributed. If it fails, the delivery is queued and retried automatically by a job that runs every minute:

  • Attempt 1 — inline, at delivery time.
  • Attempt 2 — about 1 minute after attempt 1 fails.
  • Attempt 3 — about 5 minutes after attempt 2 fails.

After the third attempt fails, the delivery is marked failed and we email the account's contact once (at most one such notice per account per 24 hours). Email and portal delivery are unaffected by webhook failures — the lead is always available there.

Manual replay

You can replay failed webhook deliveries from the portal — a single delivery, or in bulk (up to the 100 oldest failed deliveries from the last 7 days). A replay is a fresh signed POST with the same payload plus "replay": true and an X-BBL-Replay: true header; a 2xx marks the delivery delivered. Replays are rate-limited to one per delivery per minute and do not start a new automatic retry ladder.

Idempotency

Because of retries and replays, your endpoint may receive the same X-BBL-Delivery-Id more than once. Treat the delivery ID as an idempotency key and ignore duplicates you have already processed.