Webhooks & API
When something happens in SmartPropLeads — a skip trace completes, a lead enters Hot, a deal moves — we POST a signed JSON event to the URLs you configure under Account → Integrations. Zapier, Make, n8n or your own server catch it.
How it works
Every app action that matters writes an event to an outbox in the same database transaction as the action itself. A delivery worker reads that outbox and sends each event to every endpoint you subscribed to it — so nothing is lost if a server restarts mid-send, and a purchased lead exists in your CRM exactly when it exists in ours.
Three doors, one pipeline: a webhook URL (your server, Make, n8n, or a Zapier catch hook), the Zapier app (built on these same events — in review), and a read-only API key for on-demand reads and Zapier’s connection test.
Events
| Event | Fires when | Notes |
|---|---|---|
lead.skiptrace.completedNew skip-traced lead | A skip-trace order finishes (single or batch: one event per lead). | The flagship — carries the purchased phones and emails, DNC-screened where the scrub ran. |
lead.entered_hotLead entered Hot | A lead in your saved lists, farms or watched counties crosses into Hot at the weekly rescore. | One event per lead, de-duplicated across overlapping lists; capped per run with a rescore.summary digest for the overflow. |
lead.score_updatedScore updated | The weekly rescore changes the score or tier of a lead you saved or purchased. | Only held leads — never a whole county. Same per-run cap and digest overflow. |
deal.status_changedDeal status changed | A deal moves stage in your pipeline (including creation at New). | Carries from/to, dead reason and sub-state; full current state, so a late event never corrupts. |
deal.closedDeal closed | A deal reaches Closed with its actuals. | For revenue dashboards in your tools. Fires alongside deal.status_changed. |
postcard.status_changedPostcard status changed | A campaign moves Draft → In production → Mailed → Est. delivered. | Enables follow-up timing ("call 5 days after delivery"). |
rescore.summaryRescore summary (digest) | Once per rescore run when more leads changed than the per-run cap, or while the delivery queue is in backlog mode. | Counts per source plus a link to the filtered view, so a Zap can still react without flooding your task quota. |
Envelope
Every payload has the same shape, so you parse one thing. id is unique and stable across retries — de-duplicate on it. data is flat and CRM-friendly (no nesting to dig through), and always carries the full current state, never a delta: “score is 88 now” beats “score changed by +3”, so a late event can never corrupt your record.
{
"id": "evt_sample_lead_skiptrace_completed",
"type": "lead.skiptrace.completed",
"created_at": "2026-09-23T14:07:11Z",
"user_id": "usr_sample",
"data": {
"parcel_id": "collin:R-1234-00",
"county": "collin",
"account_num": "R-1234-00",
"address": "1108 Heather Ln, McKinney, TX 75069",
"city": "McKinney",
"zip": "75069",
"score": 88,
"tier": "hot",
"lead_types": [
"probate",
"long-term-owner",
"free-clear"
],
"why": "Probate filed 9/11 (Collin Co.) · owned 23 yrs",
"owner_name": "MARTINEZ ROSA ESTATE OF",
"mailing_address": "PO Box 2210, Allen, TX 75013",
"deal_url": "https://smartpropleads.com/leads/property/collin/R-1234-00",
"crm_note": "SmartPropLeads score 88 · Hot · Lead types: Probate, Long-Term Owners (15+ yrs), Free & Clear · Why: Probate filed 9/11 (Collin Co.) · owned 23 yrs · Owner: MARTINEZ ROSA ESTATE OF · Mailing: PO Box 2210, Allen, TX 75013 · Open: https://smartpropleads.com/leads/property/collin/R-1234-00",
"order_id": "ord_sample_01",
"batch_id": "batch_sample_01",
"list_id": "list_sample_01",
"list_name": "Collin probate — Sept",
"phones": [
"+12145550143",
"+14695550198"
],
"phone_details": [
{
"number": "+12145550143",
"type": "mobile",
"dnc": false,
"dnc_status": "clear",
"carrier": "T-Mobile"
},
{
"number": "+14695550198",
"type": "landline",
"dnc": true,
"dnc_status": "listed",
"carrier": null
}
],
"emails": [
"rosa.martinez@example.com"
],
"phone_primary": "+12145550143",
"phone_secondary": "+14695550198",
"email_primary": "rosa.martinez@example.com",
"callable_phones": 1,
"dnc_screened": true,
"litigator": false,
"traced_at": "2026-09-23T14:07:05Z",
"fixture": true
}
}Versioning: additive only. Fields are added, never renamed, removed or re-typed. A breaking change is a new event type. Contact data appears only in events your plan entitles you to, and no event ever carries another user’s data.
Verifying signatures
Each request carries four headers. Verify before you trust the body:
X-SPL-Signature—v1=<hex>, an HMAC-SHA256 over<timestamp>.<raw body>with your endpoint’s secret.X-SPL-Timestamp— Unix seconds when we signed. Reject anything older than 5 minutes to defeat replays.X-SPL-Event-IdandX-SPL-Event-Type— the envelope’s id and type, for routing before you parse.
Compute the HMAC over the raw request body (not a re-serialised object), compare in constant time, then check the timestamp. Answer 2xx quickly and do your work afterwards — anything else, including a redirect, counts as a failure and is retried.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.SPL_WEBHOOK_SECRET; // from Account → Integrations
const app = express();
// Verify against the RAW body — never a re-serialised object.
app.post("/spl", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("X-SPL-Timestamp") ?? "";
const sig = (req.header("X-SPL-Signature") ?? "").replace(/^v1=/, "");
const expected = crypto.createHmac("sha256", SECRET).update(`${ts}.${req.body}`).digest("hex");
const valid = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
if (!valid) return res.status(401).send("bad signature");
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(401).send("stale");
const event = JSON.parse(req.body); // { id, type, created_at, user_id, data }
// De-duplicate on event.id (retries reuse it), then do your work AFTER the 200.
res.sendStatus(200);
handle(event).catch(console.error);
});Delivery & retries
Delivery is at-least-once. A 2xx within 10 seconds is delivered; otherwise we retry on a backoff of 1 min → 5 min → 30 min → 2 h → 6 h, then mark the delivery failed. An endpoint that fails every delivery for 3 days is paused automatically and you get an email. The Integrations page shows the last 100 deliveries per endpoint with response codes and a one-click redeliver; the log keeps 30 days.
Ordering is not guaranteed. Payloads carry timestamps and full state, so apply the newest created_at and ignore the rest.
Caps & digests
Bulk operations are where naive event systems break, so the caps are enforced server-side and designed for:
lead.entered_hotandlead.score_updated: at most 500 per event type per rescore run, highest scores first, de-duplicated across overlapping lists and farms. Anything beyond arrives as onerescore.summarywith counts per source and a link to the filtered view.lead.score_updatedfires only for leads you saved or purchased — never a whole county.- Bulk data loads (a new county, a scoring-model change, a backfill) never emit per-lead events. Every rescore event carries
rescore_run_id. - Per endpoint, deliveries are paced at 10/second (queued, not dropped); a per-account daily ceiling is the final circuit breaker.
Design your Zap for the digest: a rescore.summary is the signal that a big week happened, and the filtered view is one click away.
API keys
Create a key under Integrations; it is shown once and stored hashed. Send it as Authorization: Bearer spl_live_…. Keys are read-only, scoped to your own data, limited to 120 requests a minute, and stop working within a minute of being revoked. There is no admin or global token anywhere — a stolen key exposes one account’s leads, never the platform.
curl -H "Authorization: Bearer spl_live_…" https://api.smartpropleads.com/v1/ping
# → {"ok":true,"user_id":"…","plan":"pro","scopes":["read"]}
curl -H "Authorization: Bearer spl_live_…" https://api.smartpropleads.com/v1/events/samples
# → {"events":[ …one sample per event type… ]}Included with Pro and Elite at no extra charge. The same key reads your leads, deals and orders on demand — see the Read API reference.
Sample payloads
One per event, with realistic DFW data. They are fixtures — every one carries "fixture": true and an id beginning evt_sample_ — and they are exactly what the Send test button delivers, so you can map fields in your CRM before the first real event.
lead.skiptrace.completed
{
"id": "evt_sample_lead_skiptrace_completed",
"type": "lead.skiptrace.completed",
"created_at": "2026-09-23T14:07:11Z",
"user_id": "usr_sample",
"data": {
"parcel_id": "collin:R-1234-00",
"county": "collin",
"account_num": "R-1234-00",
"address": "1108 Heather Ln, McKinney, TX 75069",
"city": "McKinney",
"zip": "75069",
"score": 88,
"tier": "hot",
"lead_types": [
"probate",
"long-term-owner",
"free-clear"
],
"why": "Probate filed 9/11 (Collin Co.) · owned 23 yrs",
"owner_name": "MARTINEZ ROSA ESTATE OF",
"mailing_address": "PO Box 2210, Allen, TX 75013",
"deal_url": "https://smartpropleads.com/leads/property/collin/R-1234-00",
"crm_note": "SmartPropLeads score 88 · Hot · Lead types: Probate, Long-Term Owners (15+ yrs), Free & Clear · Why: Probate filed 9/11 (Collin Co.) · owned 23 yrs · Owner: MARTINEZ ROSA ESTATE OF · Mailing: PO Box 2210, Allen, TX 75013 · Open: https://smartpropleads.com/leads/property/collin/R-1234-00",
"order_id": "ord_sample_01",
"batch_id": "batch_sample_01",
"list_id": "list_sample_01",
"list_name": "Collin probate — Sept",
"phones": [
"+12145550143",
"+14695550198"
],
"phone_details": [
{
"number": "+12145550143",
"type": "mobile",
"dnc": false,
"dnc_status": "clear",
"carrier": "T-Mobile"
},
{
"number": "+14695550198",
"type": "landline",
"dnc": true,
"dnc_status": "listed",
"carrier": null
}
],
"emails": [
"rosa.martinez@example.com"
],
"phone_primary": "+12145550143",
"phone_secondary": "+14695550198",
"email_primary": "rosa.martinez@example.com",
"callable_phones": 1,
"dnc_screened": true,
"litigator": false,
"traced_at": "2026-09-23T14:07:05Z",
"fixture": true
}
}lead.entered_hot
{
"id": "evt_sample_lead_entered_hot",
"type": "lead.entered_hot",
"created_at": "2026-09-23T14:07:11Z",
"user_id": "usr_sample",
"data": {
"parcel_id": "tarrant:04512890",
"county": "tarrant",
"account_num": "04512890",
"address": "2417 Ridgmar Blvd, Fort Worth, TX 76116",
"city": "Fort Worth",
"zip": "76116",
"score": 84,
"tier": "hot",
"lead_types": [
"tax-delinquent",
"absentee-owner"
],
"why": "Tax suit filed 9/15 (Tarrant Co.) · out-of-state owner",
"owner_name": "NGUYEN THANH & LINH",
"mailing_address": "880 Piedmont Ave NE, Atlanta, GA 30309",
"deal_url": "https://smartpropleads.com/leads/property/tarrant/04512890",
"crm_note": "SmartPropLeads score 84 · Hot · Lead types: Tax Delinquent, Absentee Owners · Why: Tax suit filed 9/15 (Tarrant Co.) · out-of-state owner · Owner: NGUYEN THANH & LINH · Mailing: 880 Piedmont Ave NE, Atlanta, GA 30309 · Open: https://smartpropleads.com/leads/property/tarrant/04512890",
"previous_score": 71,
"previous_tier": "warm",
"rescore_run_id": "2026-09-21",
"sources": [
{
"kind": "farm",
"id": "farm_sample_01",
"name": "West Fort Worth"
},
{
"kind": "list",
"id": "list_sample_02",
"name": "Absentee follow-ups"
}
],
"fixture": true
}
}lead.score_updated
{
"id": "evt_sample_lead_score_updated",
"type": "lead.score_updated",
"created_at": "2026-09-23T14:07:11Z",
"user_id": "usr_sample",
"data": {
"parcel_id": "dallas:00000456789000000",
"county": "dallas",
"account_num": "00000456789000000",
"address": "6115 Bryan Pkwy, Dallas, TX 75214",
"city": "Dallas",
"zip": "75214",
"score": 66,
"tier": "warm",
"lead_types": [
"senior-ov65",
"long-term-owner"
],
"why": "Owned 31 yrs · senior exemption",
"owner_name": "OKAFOR PATRICIA",
"mailing_address": "6115 Bryan Pkwy, Dallas, TX 75214",
"deal_url": "https://smartpropleads.com/leads/property/dallas/00000456789000000",
"crm_note": "SmartPropLeads score 66 · Warm · Lead types: Senior (65+), Long-Term Owners (15+ yrs) · Why: Owned 31 yrs · senior exemption · Owner: OKAFOR PATRICIA · Mailing: 6115 Bryan Pkwy, Dallas, TX 75214 · Open: https://smartpropleads.com/leads/property/dallas/00000456789000000",
"previous_score": 81,
"previous_tier": "hot",
"rescore_run_id": "2026-09-21",
"held_via": [
"purchased",
"saved"
],
"fixture": true
}
}deal.status_changed
{
"id": "evt_sample_deal_status_changed",
"type": "deal.status_changed",
"created_at": "2026-09-23T14:07:11Z",
"user_id": "usr_sample",
"data": {
"deal_id": "deal_sample_01",
"parcel_id": "denton:R123456",
"county": "denton",
"account_num": "R123456",
"address": "3305 Long Prairie Rd, Flower Mound, TX 75022",
"city": "Flower Mound",
"zip": "75022",
"from_status": "OFFER_SENT",
"to_status": "UNDER_CONTRACT",
"sub_state": "option_period",
"exit_strategy": "assigned",
"dead_reason": null,
"next_action": "Send assignment to title",
"next_action_due": "2026-09-26",
"offer_date": "2026-09-15",
"effective_date": "2026-09-22",
"option_end_date": "2026-09-29",
"closing_date": "2026-10-17",
"buyer_name": "Trinity Ridge Homes LLC",
"title_company": "Capital Title — Frisco",
"source": "pipeline",
"money": {
"contract_price": 31500000,
"earnest": 250000,
"est_fee": 1500000
},
"stage_entered_at": "2026-09-23T14:07:11Z",
"deal_url": "https://smartpropleads.com/pipeline?deal=deal_sample_01",
"fixture": true
}
}deal.closed
{
"id": "evt_sample_deal_closed",
"type": "deal.closed",
"created_at": "2026-09-23T14:07:11Z",
"user_id": "usr_sample",
"data": {
"deal_id": "deal_sample_02",
"parcel_id": "collin:R-5567-01",
"county": "collin",
"account_num": "R-5567-01",
"address": "412 Rockhill Rd, Frisco, TX 75034",
"city": "Frisco",
"zip": "75034",
"from_status": "UNDER_CONTRACT",
"to_status": "CLOSED",
"sub_state": null,
"exit_strategy": "assigned",
"dead_reason": null,
"next_action": null,
"next_action_due": null,
"offer_date": "2026-08-20",
"effective_date": "2026-08-28",
"option_end_date": "2026-09-04",
"closing_date": "2026-09-22",
"buyer_name": "Lone Star Capital Partners",
"title_company": "Lawyers Title — Plano",
"source": "pipeline",
"money": {
"contract_price": 28900000,
"earnest": 200000,
"actual_fee": 1750000
},
"stage_entered_at": "2026-09-23T14:07:11Z",
"deal_url": "https://smartpropleads.com/pipeline?deal=deal_sample_02",
"closed_at": "2026-09-23T14:07:11Z",
"actual_fee_cents": 1750000,
"actual_sale_price_cents": null,
"actual_profit_cents": null,
"contract_price_cents": 28900000,
"fixture": true
}
}postcard.status_changed
{
"id": "evt_sample_postcard_status_changed",
"type": "postcard.status_changed",
"created_at": "2026-09-23T14:07:11Z",
"user_id": "usr_sample",
"data": {
"campaign_id": "batch_sample_07",
"campaign_name": "Tarrant tax-delinquent — drop 2",
"list_id": "list_sample_03",
"state": "mailed",
"state_label": "Mailed",
"cards": 180,
"by_state": {
"mailed": 178,
"production": 2
},
"mailed_at": "2026-09-22T16:40:00Z",
"est_delivery_date": "2026-09-29",
"campaign_url": "https://smartpropleads.com/my-leads",
"fixture": true
}
}rescore.summary
{
"id": "evt_sample_rescore_summary",
"type": "rescore.summary",
"created_at": "2026-09-23T14:07:11Z",
"user_id": "usr_sample",
"data": {
"rescore_run_id": "2026-09-21",
"reason": "cap_exceeded",
"entered_hot_total": 1240,
"entered_hot_sent": 500,
"score_updated_total": 3912,
"score_updated_sent": 500,
"per_source": [
{
"kind": "farm",
"id": "farm_sample_01",
"name": "West Fort Worth",
"entered_hot": 812
},
{
"kind": "county",
"id": "tarrant",
"name": "Tarrant County",
"entered_hot": 428
}
],
"view_url": "https://smartpropleads.com/dashboard?since=2026-09-21",
"fixture": true
}
}Compliance
Contact data pushed to your CRM is for your own permitted use. Phone numbers carry their DNC screening result where the scrub ran (dnc_status: clear, listed or unknown); callable_phones counts only cleared numbers on a record that is not a known litigator. Texting cold leads in Texas has registration and liability consequences under SB 140 — see our SMS terms before you automate outreach.
Endpoints must be public https:// URLs; private and internal addresses are refused, redirects are not followed, and we never store your CRM’s credentials — Zapier holds those on its side.