WonFlo is not a CRM and is not trying to become one. If you close jobs in JobNimbus, AccuLynx, Roofr, Jobber, QuoteIQ or anything else, the lead should show up there on its own. That is what this is.
The mechanism is a webhook: when something happens to a lead, WonFlo sends a POST to a URL you choose, with the lead as JSON. Plenty of CRMs accept one directly. The ones that do not, Zapier does — and Zapier talks to everything.
This is the page to hand your Zapier person, or whoever wired up your last integration. If that person is you and you have never done this before, the Zapier path below is genuinely the whole job.
Set it up
In app.wonflo.com, go to Settings → CRM integration.
- Paste your webhook URL. Either a Zapier Catch Hook URL or your CRM's own inbound-webhook address.
- Hit Save.
- Copy the signing secret out of the modal that appears. It is shown once. Put it wherever you keep credentials before you close that window.
What WonFlo sends
A signed POST with a JSON body, on two events: lead.captured and lead.released. Branch on the event field — if all you want is new leads arriving in your CRM, act on lead.captured and ignore the rest.
Every payload carries schema_version "1.0", so a receiver can check what it is looking at before it starts reading fields. The top level is small:
Inside lead, the dossier is grouped roughly the way the dashboard shows it: contact, property, estimate, tailored_answers, storm, attribution and device.
We are deliberately not printing a field-by-field dictionary here, because the honest way to see the payload is to look at a real one. Save the webhook, then run one test lead through your own funnel — Zapier's Catch Hook will show you every field it actually received, with real values in it, which beats any table we could write.
The Zapier path
This is the route for almost everyone.
- In Zapier, start a Zap with the Webhooks by Zapier trigger, event Catch Hook. Copy the URL it hands you.
- Paste that URL into WonFlo's CRM integration setting and save. Copy the signing secret while the modal is open.
- Run one test lead through your funnel, so Zapier has a real sample to map against.
- Map the fields you care about onto your CRM's "create lead" or "create contact" action.
- Turn the Zap on.
Worth saying plainly: Zapier's catch hooks do not verify the signature by default, and for most roofing shops that is a perfectly reasonable place to land. The URL is long and unguessable, and the worst case is a junk row in your CRM. Verification is what the next section is for, and it earns its keep when you are writing the receiver yourself.
Verifying the signature, for direct receivers
If you are posting to your own endpoint rather than to Zapier, verify before you trust. Every request carries this header:
X-WonFlo-Signature: t=<unix>,v1=<hex>
t is the Unix timestamp of the send. v1 is an HMAC-SHA256, in hex, computed over the string "<t>.<raw request body>" with your signing secret as the key.
Two rules decide whether this works at all:
- Verify against the raw body bytes. Not a parsed object you re-serialized — a re-serialized body will not match, because a JSON round trip changes whitespace and key order. Most frameworks hand you a parsed body by default, so you usually have to ask for the raw one on purpose.
- Reject anything older than about five minutes. Compare
tagainst your own clock and drop the request if the gap is bigger than that. That is the replay window — without it, a signature that was valid once stays valid forever.
const crypto = require('crypto');
function verify(rawBody, header, secret) {
// header looks like: t=1757500000,v1=9f8e7d...
const p = Object.fromEntries(header.split(',').map(s => s.split('=')));
const age = Math.abs(Date.now() / 1000 - Number(p.t));
if (!(age < 300)) return false; // 5-minute replay window
const expected = crypto
.createHmac('sha256', secret)
.update(p.t + '.' + rawBody) // rawBody = the exact bytes
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(p.v1 || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
That is the whole recipe, and it is the same recipe in any language: split the header, check the age, HMAC-SHA256 the timestamp and the raw body together, compare in constant time. PHP, Python, Ruby, Go and C# all ship this in the standard library.
Delivery, retries, and how to tell it is working
Sends ride a durable queue rather than going out inline with the homeowner's submit, so a slow or briefly unreachable receiver never affects the person filling out your funnel. In normal conditions a delivery lands within about a minute.
Failures are retried. And the CRM card on your dashboard shows the last delivery status, which is the first place to look when somebody says leads stopped arriving. If that card looks healthy and your CRM is empty, the problem is downstream of us — check the Zap is switched on, and check its own history.
Related
- Lead alerts: where they go and what's in them — the email and text that reach you immediately.
- Understanding the lead score — what the score attached to each lead means.
- How the closed loop works — why the outcome you record still belongs in your WonFlo dashboard, CRM or no CRM.