Skip to content

Webhooks

Measured Bid can post each new lead to a URL you choose, and send stage changes back to the system that sent a lead in. Both send JSON.

View as Markdown

New lead webhook

Set it in the widget's settings, under New-lead alerts, in Webhook. It works with Zapier, Make, n8n, a HighLevel inbound webhook or your own endpoint. Each widget has its own, so one account can feed several systems.

  • Measured Bid sends each new lead once, as a POST with a JSON body, right after the homeowner sends the form. Leads your team saves from New estimate are sent the same way.
  • The webhook address must be a public https:// address. Redirects are not followed.
  • It waits up to 8 seconds for a reply and does not retry.
  • If your endpoint is down, the lead is still saved in your dashboard and the homeowner still sees their confirmation.
  • Demo widgets never send webhooks.
  • It isn't signed. Use an address that's hard to guess, as Zapier and Make URLs are.
  • Leads posted to your form leads URL are not sent to this webhook.
Sample new lead (fictional)JSON
{
  "leadId": "5b0e2c1a-8f3d-4c7e-9a61-2d4f7b8c9e10",
  "brand": "sierra-gutter-co",
  "brandName": "Sierra Gutter Co.",
  "service": "gutters",
  "guardProduct": {
    "id": "sample-mesh",
    "name": "Micro-mesh aluminum"
  },
  "quoteId": "9c4d7e2f-1a3b-4c5d-8e6f-7a8b9c0d1e2f",
  "formattedAddress": "412 Sample Ridge Ct, Roseville, CA 95661, USA",
  "name": "Jordan Example",
  "phone": "(916) 555-0114",
  "email": "jordan@example.com",
  "preferredTime": "Weekday mornings",
  "estimateLow": 1500,
  "estimateHigh": 2025,
  "smsConsent": false,
  "smsConsentText": null,
  "utm": {
    "utm_source": "google",
    "utm_medium": "cpc",
    "utm_campaign": "sample-guards",
    "gclid": "sample-click",
    "referrer": "https://example.com/gutter-guards"
  },
  "property": {
    "source": "rentcast",
    "stories": 1,
    "yearBuilt": 1998,
    "propertyType": "single_family"
  },
  "receivedAt": "2026-09-25T16:42:07.000Z",
  "firstName": "Jordan",
  "lastName": "Example",
  "street": "412 Sample Ridge Ct",
  "city": "Roseville",
  "state": "CA",
  "zip": "95661"
}

Fields

FieldTypeWhat it holds
leadIdstring or nullThe lead's ID in Measured Bid. Null only if the lead could not be saved; the rest still arrives.
brandstringYour widget's ID. Each widget has its own webhook, so one account can feed several CRMs.
brandNamestringThe business name shown on the widget.
service"gutters" | "painting" | "siding" | "roofing" | "lighting" | "holiday" | "washing"The trade the widget prices.
guardProductobjectThe guard product id and name saved with the estimate, when a named guard was quoted.
quoteIdstringThe estimate record behind the lead, when there is one.
formattedAddressstringThe address as Google formats it: street, city, state and ZIP, then country.
namestringThe homeowner's name as typed.
phonestringTheir phone number as typed. Every lead has one.
emailstringOptional. Missing when they left it blank.
preferredTimestringOptional. The best time for a visit, in their words.
estimateLownumberThe low end of the price range they saw, in dollars. Missing when no price was shown.
estimateHighnumberThe high end of the range, in dollars. Missing when no price was shown.
smsConsentbooleanTrue only when they checked the box to get texts.
smsConsentTextstring or nullThe exact consent wording they agreed to, or null.
utmobjectWhere the lead came from. Any of utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, gbraid, wbraid, fbclid, msclkid, plus referrer, the page the widget was on. Missing when there's nothing to report.
propertyobjectCounty records for the home, such as stories, living area and year built. Only on plans with county records, and only when the county has a record.
receivedAtstringWhen Measured Bid received the lead, as an ISO 8601 time in UTC.
firstNamestringThe first word of name, for CRMs that want first and last names.
lastNamestringEvery word of name after the first. Missing when they typed one word.
streetstringThe street line of formattedAddress, with any unit. The whole address when it isn't a US street address.
citystringThe city from formattedAddress. Missing when the address isn't a US street address.
statestringThe two-letter state. Missing when the address isn't a US street address.
zipstringThe ZIP code. Missing when the address isn't a US street address.

The webhook carries the contact details, the range, the trade and the source. The measured quantities are in the new-lead email. The lead in your dashboard has those, plus the homeowner's answers, the line-item dollars and the map of what was measured.

Step-by-step setup: Zapier, HighLevel, Jobber.

Allowed webhook addresses

  • It must start with https://.
  • It must be a public web address. IP addresses, localhost and internal names like .local or .internal are refused.
  • Redirects aren't followed. Use the final address.

Lead updates webhook

For leads that arrived through your form leads URL with an external_id. When such a lead's stage or value changes in Measured Bid, it posts the new state to the address in Send lead updates to, under Leads from your other website forms in the widget's settings.

Sample lead updateJSON
{
  "event": "lead.updated",
  "widget": "your-widget-id",
  "lead": {
    "id": "measured-bid-lead-id",
    "external_ids": [
      "your-lead-id"
    ],
    "status": "won",
    "value": 1775,
    "updated_at": "2026-09-26T12:00:00.000Z"
  }
}
FieldWhat it holds
eventAlways lead.updated.
widgetThe widget ID.
lead.idThe lead's ID in Measured Bid.
lead.external_idsThe IDs your system sent with the lead.
lead.statusOne of new, contacted, quoted, won, lost.
lead.valueThe estimate total in whole dollars, or null.
lead.updated_atWhen it changed, ISO 8601 in UTC.

It is sent once per change, after the change is saved. It waits up to 5 seconds and does not retry. The same address rules apply.

Check the signature

Each update carries X-MeasuredBid-Signature: sha256=<hex>, an HMAC-SHA256 of the raw body. The key is your form leads URL's token, the last part of that URL. Rotating the URL changes the key.

verify.js (Node)JS
import { createHmac, timingSafeEqual } from "node:crypto";

// token: the last part of your form leads URL, https://measuredbid.com/api/intake/<token>
// rawBody: the request body exactly as received, before JSON.parse
export function isFromMeasuredBid(rawBody, signatureHeader, token) {
  const expected = "sha256=" + createHmac("sha256", token).update(rawBody).digest("hex");
  const given = String(signatureHeader ?? "");
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}