Integrations
Lead delivery & webhooks
When a visitor finishes an estimate, ClearCost creates an exclusive lead and POSTs it to your endpoint as a signed JSON webhook — so your crew can follow up in minutes.
Available on Studio and Enterprise
CRM webhook delivery is included on the Studio ($249/mo) and Enterprise plans. On Solo, leads still arrive by email and in your dashboard. See pricing.Enable webhooks
In Pro dashboard → Website Embed → Webhooks, set your endpoint URL and copy the signing secret. Don't want to write code? Skip straight to the CRM setup guide and point the endpoint at Zapier.
The lead.created event
Each delivery is an HTTP POST with a JSON body and these headers:
| Field | Type | Description |
|---|---|---|
X-ClearCost-Signature | string | Signature in the form "t=<unix>,v1=<hmac>". See below. |
X-ClearCost-Event | string | Event type — currently always "lead.created". |
Content-Type | string | application/json |
User-Agent | string | ClearCost-Embed-Webhook/1 |
Payload
{
"event": "lead.created",
"embed_id": "emb_123",
"lead": {
"id": "lead_abc123",
"category": "deck",
"location_zip": "30080",
"estimated_price_min": 14200,
"estimated_price_max": 19600,
"customer_name": "Jane Doe",
"customer_email": "jane@example.com",
"customer_phone": "555-123-4567",
"notes": "Replacing an old 12x16 deck",
"image_url": "https://...",
"created_at": "2026-06-21T15:04:05Z"
}
}| Field | Type | Description |
|---|---|---|
event | string | "lead.created". |
embed_id | string | null | The embed that produced the lead. |
lead.id | string | Unique lead ID — use it to de-duplicate (see retries). |
lead.category | string | Project category, e.g. "deck". |
lead.location_zip | string | Customer ZIP code. |
lead.estimated_price_min | number | Low end of the Fair Market Rate (USD). |
lead.estimated_price_max | number | High end of the Fair Market Rate (USD). |
lead.customer_name | string | Customer's full name. |
lead.customer_email | string | Customer's email address. |
lead.customer_phone | string | null | Phone, if the customer provided one. |
lead.notes | string | null | Free-text project notes. |
lead.image_url | string | null | Uploaded photo or AI render, if any. |
lead.created_at | string | ISO 8601 timestamp. |
Verify the signature
The signature is HMAC-SHA256 over "{timestamp}.{rawBody}" using your endpoint secret, formatted as t=<unix>,v1=<hex> (the same scheme Stripe uses). Always verify against the raw request body, before parsing JSON:
import crypto from "node:crypto";
// Your endpoint's signing secret (Pro dashboard → Website Embed → Webhooks)
const SECRET = process.env.CLEARCOST_WEBHOOK_SECRET;
// rawBody must be the exact bytes ClearCost sent — verify BEFORE JSON.parse,
// and don't let a body parser re-serialize it.
export function verifySignature(header, rawBody) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("="))
); // { t, v1 }
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const signatureOk = crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected)
);
// Reject replays: signature older than 5 minutes.
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return signatureOk && fresh;
}Use the raw body
If your framework parses and re-serializes JSON, the bytes change and the signature won't match. Capture the raw body (e.g. Expressexpress.raw(), Next.js route handlers via await req.text()) and hash that.Retries & idempotency
Respond with a 2xx within 10 seconds to acknowledge. Any non-2xx response, a timeout, or a network error is retried up to 5 times with increasing backoff (about 1, 5, 30, 120, then 360 minutes). Because a lead can be delivered more than once, treat lead.id as an idempotency key and ignore IDs you've already processed.