Help

Connect your CRM

Give WonFlo one webhook URL and every lead lands in the system you already run your jobs in — no export, no copy-paste, no second place to check.

Last updated September 10, 2026

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 SettingsCRM integration.

  1. Paste your webhook URL. Either a Zapier Catch Hook URL or your CRM's own inbound-webhook address.
  2. Hit Save.
  3. 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.
Shown once — and re-saving rotates it
If you close the modal without copying the secret, the way back is to save the URL again, which issues a new secret and retires the old one. That is harmless if nothing was using the old one yet, and it is a small outage if something was. So re-save deliberately, not by accident.

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:

Top-level key
What is in it
event
Which of the two events this is
delivered_at
When WonFlo sent it
lead
The lead itself — see below
contractor
The WonFlo account it belongs to

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.

  1. In Zapier, start a Zap with the Webhooks by Zapier trigger, event Catch Hook. Copy the URL it hands you.
  2. Paste that URL into WonFlo's CRM integration setting and save. Copy the signing secret while the modal is open.
  3. Run one test lead through your funnel, so Zapier has a real sample to map against.
  4. Map the fields you care about onto your CRM's "create lead" or "create contact" action.
  5. 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:

Request 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:

Node — no dependencies
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.

The webhook is filing, not alerting
Getting the lead into your CRM is where it goes to live. It is not how you find out it arrived. Lead alerts hit your phone and inbox in the first sixty seconds, and that is the one that decides whether you win the job.

Related

Still stuck?

A real person reads every one of these, and it is usually faster than you would expect. Tell us your business name and what you were trying to do.

Email [email protected] ← Back to the help center