WonFlo is not a CRM and is not trying to become one. If you close jobs in JobNimbus, AccuLynx, Roofr, Jobber or anything else, the lead should show up there on its own — no export, no copy-paste, no second place to check.
Connecting goes both ways. Leads go into your CRM the moment they come in. Results come back — when a job is booked or sold, that is recorded on the lead in WonFlo, so you are not tapping the same thing in two places.
Everything lives in one place: in app.wonflo.com, go to Settings → Connect your CRM. It asks one question.
Which CRM do you use?
Pick your answer and jump to it. You only need to read your own section.
- JobNimbus — paste one key. Leads go in and results come back on their own.
- Another CRM — two short steps in Zapier: one sends new leads to your CRM, the other reports job results back.
- I don't use a CRM — nothing to set up. You tap Booked or Sold on each lead.
JobNimbus
WonFlo connects to JobNimbus directly. No Zapier, no webhook, no developer — just a key.
- Make a key in JobNimbus. In JobNimbus, go to Settings → API and create a new API key. Copy it. It is long — that is normal.
- Paste it into WonFlo. In WonFlo, go to Settings → Connect your CRM, choose JobNimbus, paste the key and save.
- That is it. Run one test lead through your funnel if you want to watch it land.
From then on:
- Every new lead shows up in JobNimbus as a contact with a job on it. The job description carries the estimate, the roof summary, the storm history and a line that starts
wonflo_lead_id=— leave that line alone, it is how WonFlo matches the job back to the lead. - When you move the job along in JobNimbus, WonFlo records it. Booked and sold come back on their own, so you do not have to tap them again in WonFlo. WonFlo checks JobNimbus for changes on a schedule, so a status change shows up on the lead a little later, not the same second.
- A sold job brings its number with it. WonFlo uses the job's approved estimate total from JobNimbus. If there is none, it records your default job value instead.
- Results only move forward. An update from JobNimbus never un-sells a sold lead and never overwrites a job value you already entered in WonFlo.
Another CRM
For AccuLynx, Roofr, Jobber, QuoteIQ, ServiceTitan, HubSpot, GoHighLevel or anything else, the connection runs through Zapier, which talks to almost every CRM out there. There are two steps, and each one is a single Zap.
- Step 1 — send new leads to your CRM. WonFlo hands each new lead to Zapier, and Zapier puts it in your CRM.
- Step 2 — report job results back. When a job changes status in your CRM, Zapier tells WonFlo, and the result is recorded on the lead.
Step 1 works on its own if that is all you want. Step 2 needs Step 1 in place first, because it uses the WonFlo lead ID that Step 1 drops into your CRM.
Step 1 — Send new leads to your CRM
- Start a Zap. In Zapier, make a new Zap. For the trigger, pick Webhooks by Zapier and the event Catch Hook. Zapier hands you a web address — copy it.
- Paste it into WonFlo. In WonFlo, go to Settings → Connect your CRM → Another CRM → Step 1. Paste the address and save.
- Copy the signing secret. A box pops up with it. It is shown once. Zapier does not need it, but keep it with your passwords in case a developer ever does.
- Send a test lead. Run one test lead through your own funnel, so Zapier has a real lead to look at.
- Point it at your CRM. Add an action for your CRM — usually "create lead" or "create contact" — and match up the fields: name, phone, address and so on.
- Save the WonFlo lead ID in your CRM. Put the field Zapier shows as Lead Id into a custom field or the notes on the new record. You will need it for Step 2. Skip this and Step 2 has nothing to match on.
- Turn the Zap on.
How to tell it is working. New leads appear in your CRM, usually within about a minute. The CRM card on your WonFlo dashboard shows the last delivery, and it is the first place to look if someone says leads stopped arriving. If that card looks healthy and your CRM is empty, the problem is on the Zapier side — check the Zap is switched on, and look at its history.
Step 2 — Report job results back
Step 2 is a second Zap. When a job changes status in your CRM, it tells WonFlo what happened — booked, sold, lost and so on — and the result is recorded on that lead in WonFlo. Same result as tapping the button in your dashboard, without the tapping.
Before you start: Step 1 is working, and your CRM has the WonFlo lead ID saved on each new lead (step 6 above). If your CRM is JobNimbus, you do not need any of this — the JobNimbus section does it with one key.
- Make a WonFlo key. In WonFlo, go to Settings → Connect your CRM → Another CRM → Step 2 and tap Create key. WonFlo shows you two things, once: a key that starts with
wfo_in_, and an address —https://api.wonflo.com/api/leads/{lead_id}/outcome. Copy both somewhere safe before you close the box. Treat the key like a password — anyone who has it can mark your leads. - Start a new Zap with your CRM as the trigger. Pick your CRM's app in Zapier and the event that fires when a job changes status. Each CRM names it a little differently — look for something like "job status changed", "job updated" or "deal stage changed".
- Optional: add a Filter. Add Filter by Zapier so the Zap only carries on when the status is one you care about — for example, only when the status is your "Appointment Scheduled" status. The easiest setup is one Zap per result: build it once for booked, then copy the Zap and change the filter and the outcome for sold, lost and the rest.
- Add the action that talks to WonFlo. Pick Webhooks by Zapier, event POST, and fill in four boxes:
Zapier boxWhat goes in itURLThe address from step 1, with
{lead_id}— curly brackets and all — replaced by the WonFlo lead ID field from your CRM trigger. (Leads that came in through the JobNimbus connection carry it in the job description, afterwonflo_lead_id=.)Payload TypejsonDataA row namedoutcomewith the result, likebooked. For a sale, a second row namedvaluewith the job's dollar amount. The examples below show every result.HeadersA row namedAuthorizationwith the valueBearer, one space, then your key. - Test it — see below — and then turn the Zap on.
What to send for each result
Five results, one word each. Only sold takes a dollar amount — a plain number like 12400 or $12,400 both work. Leave the amount out and WonFlo records your default job value instead. On any other result an amount is simply ignored.
{"outcome":"contacted"}{"outcome":"booked"}{"outcome":"sold","value":12400}{"outcome":"lost"}{"outcome":"junk"}The $12,400 is only there to show the shape — send the real number from the job. Put together, a sale looks like this to WonFlo:
POST https://api.wonflo.com/api/leads/<WonFlo lead ID>/outcome
Authorization: Bearer <your wfo_in_ key>
Content-Type: application/json
{"outcome":"sold","value":12400}
Test it
- Use a test lead. Pick the test lead you ran through your funnel in Step 1 — it is in your CRM and in WonFlo.
- Send
bookedfor it. Either move that lead's job to your booked status in your CRM, or use Test step on the webhook action in Zapier with that lead as the sample. - Watch the lead in WonFlo. Open it in your dashboard. It should now show as booked.
- Check Settings. Under Connect your CRM → Another CRM → Step 2 you will see Last update received N min ago. That line is how you know the connection is working.
If the test lead did not change, Zapier's history shows the reply WonFlo sent back. Here is what each one means.
If something goes wrong
Bearer, one space, then the whole key. Check the address has the WonFlo lead ID in it — not your CRM's own job number — and that {lead_id} is gone.Authorization header. Revoke stops the key immediately, with no replacement. Use Revoke if the key ever ends up somewhere it should not be.
I don't use a CRM
Then there is nothing to set up. Choose I don't use a CRM in Settings and you are done.
WonFlo is where you keep track. Every lead in your dashboard has Booked and Sold buttons — tap Booked once the inspection is on the calendar, and Sold with the job value once the contract is signed. It takes seconds, and it is recorded in WonFlo against that lead. How the closed loop works explains why that tap is worth making.
For your developer
Everything above is all a roofer needs. This part is for whoever builds a receiver by hand instead of using Zapier, or wants to know exactly what goes over the wire.
What WonFlo sends in Step 1
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, lead.id is the WonFlo lead ID — the value Step 2 puts in its address — and 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.
One field is worth knowing ahead of time: lead.estimate.is_area is true when the estimate range is an area estimate, because WonFlo had no roof measurement for that address, and false when the roof was measured or there is no range.
Verifying the signature
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. 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 and retries
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, and failures are retried.
The Step 2 endpoint
Any system that can make an HTTP request can report results — Zapier is just the easy way.
- Request.
POST /api/leads/{lead_id}/outcomeon the address shown in Settings, where{lead_id}is thelead.idfrom the Step 1 payload. SendAuthorization: Bearer <key>and a JSON body. - Body.
outcomeis one ofcontacted,booked,sold,lostorjunk, matched without regard to case; the labelsAppointment bookedandJob soldare accepted too.valueis read only onsold, as a number or a string such as"12,400"; absent, blank or0records the account's default job value. - Answers.
404 {"error":"not_found"}for a missing or wrong key and for a lead that does not exist or is not this account's — deliberately the same reply.400carriesfield,errorand a readablemessage.429past 120 requests a minute per key.503when the feature is not switched on for the account. - The key. It is shown once and WonFlo keeps only a fingerprint of it, so a lost key cannot be looked up — replace it instead.
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 recorded on each lead matters, CRM or no CRM.