Webhooks, Zapier and your CRM
A webhook sends RoofGo’s news to another app the moment it happens: a rep requests a mailer, the office mails it, a house is priced, a proposal is sent or opened, a homeowner leaves a lead. Use it with Zapier, Make, n8n or any CRM that can receive a webhook, so every mailer and lead lands where your office already works. No CRM? A Zap can add each mailed house to a Google Sheet, a CRM or a follow-up list. Only the Owner and Admin can set one up. Webhooks are included on every plan.
Connect Zapier (Catch Hook)
- In Zapier, make a new Zap. For the trigger pick Webhooks by Zapier, then Catch Hook. Copy the webhook URL it shows you.
- In RoofGo go to Brand Studio, Webhooks (at the bottom of the page). Paste the URL, tick the events you want (start with Mailer requested and Mailer mailed) and tap Add.
- RoofGo shows the webhook’s signing secret once. Zapier does not need it; a developer checking signatures does, so copy it now if you have one.
- Pick a sample in the test list, for example Sample: Mailer mailed, and tap Send test. RoofGo shows the answer from Zapier (200 means it arrived).
- In Zapier tap Test trigger, pick the sample, then add the next step, for example “Create contact” in your CRM or “Create spreadsheet row” in Google Sheets. Map
data.property.address,data.estimate.price,data.homeowner.nameanddata.rep.email.
Connect Make
- In Make, make a new scenario. Add the Webhooks module, Custom webhook, tap Add, name it and copy the address.
- Paste it into RoofGo the same way and tick your events. In Make tap Redetermine data structure, then in RoofGo tap Send test with a sample. Make learns the fields from it.
- Add the next module (your CRM, Google Sheets, an email) and map the fields. Turn the scenario on.
n8n (“Webhook” node) and most CRMs work the same way. The URL must start with https://. A company can have up to 5 webhooks, each with its own events.
The events
- Mailer requested (
mailer.requested): A rep asked for a mailer for a house. It is waiting in your Mail room for the office to print and post. - Mailer mailed (
mailer.mailed): The office marked a mailer as mailed in the Mail room. - Property priced (
property.priced): A rep saved an estimate for a house, from a tap or a bulk job. - Proposal sent (
proposal.sent): A proposal was emailed, or its homeowner link was made to text or share. - Proposal viewed (
proposal.viewed): The homeowner opened their proposal. At most once a day per proposal. - Status changed (
property.status_changed): A proposal's sales status or a door status changed, for example quoted, appointment, sold or lost. - New homeowner lead (
lead.created): A homeowner sent their details through your Instant Estimate page, with a price or as a quote request. - Quote request received (
quote_request.received): A homeowner left a quote request on your Instant Estimate page: they asked for one, or no price could be shown. Call them back. - Instant Estimate priced (
instant_estimate.completed): A homeowner got a price from your Instant Estimate page and it was saved in RoofGo. - Instant Estimate without a price (
instant_estimate.failed): A homeowner's request was saved but no price could be shown (roof not measurable, outside your area, or estimates paused). Follow up by hand. - Test (
test.ping): only when you tap Send test. The property in it is made up.
What each event contains
Every event has the same shape, so a Zap built on one works on the others. A JSON body, sent as a POST:
{
"event": "mailer.mailed",
"version": 1,
"id": "6f1c2a8e-5d0b-4d8e-9a51-1f0c7b2e9d34",
"occurred_at": "2026-10-13T15:04:05.000Z",
"company": {
"id": "2b7e...",
"name": "Your Roofing Co"
},
"data": {
"property": {
"id": "9a3f...",
"address": "123 Example St, Springfield, IL 62701",
"city": "Springfield",
"state": "IL",
"postal": "62701",
"status": "quoted",
"door_status": "knocked"
},
"estimate": {
"id": "c41d...",
"squares": 24.5,
"price": 14700,
"price_low": 13900,
"price_high": 15500,
"material": "architectural",
"created_at": "2026-10-13T15:04:04.000Z"
},
"homeowner": {
"name": "Pat Lee",
"email": "pat@example.com",
"phone": "(555) 010-0199"
},
"rep": {
"name": "Sam Rep",
"email": "sam@yourroofing.com"
},
"proposal_url": "https://roofb.id/K7QM2ABCDE",
"app_url": "https://app.roofgo.com/go/proposal/c41d...",
"mailer": {
"status": "mailed",
"requested_at": "2026-10-13T15:04:04.000Z",
"requested_by": {
"name": "Sam Rep",
"email": "sam@yourroofing.com"
},
"mailed_at": "2026-10-14T16:20:00.000Z",
"mailed_by": {
"name": "Office",
"email": "office@yourroofing.com"
},
"mail_room_url": "https://app.roofgo.com/go/mail"
}
}
}- version: the shape of the body. It changes only if a change could break your Zap; new fields can be added at any time.
- property: the address, the sales status (new, quoted, contacted, appointment, sold, lost) and the door status from canvassing.
- estimate: your latest estimate for the house: the price shown on the proposal (in dollars), your low and high, and the roof system. Squares are included only when a rep typed them in; measured squares stay in RoofGo. Empty for a door marked before any estimate.
- homeowner: the homeowner’s name, email and phone, when your team has them. Empty when nobody has typed them in.
- rep: the rep the property belongs to, with name and email.
- proposal_url: the homeowner’s proposal link, once one has been made and while it is turned on.
- app_url: the property in RoofGo, for your team (it needs a RoofGo login).
- Mailer requested and Mailer mailed add
mailer: its status, who requested it and when, who marked it mailed and when, and a link to the Mail room. RoofGo never prints or posts: your office does, then marks it mailed. - Proposal sent adds
channel: email or link. Proposal viewed addsvia: link, or mail when the QR code on a mailer brought them. Status changed addschange, with each status before and after and who changed it.
Only your own estimate numbers go out. Aerial photos and the raw measurement data behind an estimate are never sent.
Instant Estimate leads
New homeowner lead, Quote request received, Instant Estimate priced and Instant Estimate without a price carry the lead instead of a property: lead (name, email, phone, address, timeline, notes, marketing opt-in), estimate (status priced or quote_request, the reason when there was no price, your price), campaign and first_campaign, entry (link, qr, embed or direct), rep (who has the lead), utm, proposal_url and app_url. Full examples: Instant Estimate.
If something fails
- Your app has 10 seconds to answer with a 2xx status. Redirects are not followed, so paste the final URL.
- A failed delivery is tried again up to 5 more times over the next few days, then marked Failed.
- After 20 failed tries in a row the webhook turns itself off and Settings says so. Fix the URL in your app, tap Turn back on, then Send test.
- Recent deliveries shows the last 50, with the time, the status and the HTTP code your app answered. Deliveries are kept for 30 days.
- An event can arrive more than once. Each has its own
id, the same on every try and on a resend: skip an id you have already handled. - Tap Payload on a delivery to see what was sent (the homeowner’s contact details are hidden on screen), and Send again to resend it by hand.
- Send test can send sample data for any event you picked, marked
"test": true, so you can build a Zap before a real one happens.
For developers: check the signature
Every request carries X-RoofGo-Event, X-RoofGo-Delivery (unique per delivery) and X-RoofGo-Signature: t=<unix time>,v1=<hex>. v1 is the HMAC-SHA256, keyed with the webhook’s signing secret (shown once, when the webhook is added or the secret rotated), of the time, a full stop and the raw request body: t + "." + body. Compare it in constant time and refuse a t more than 5 minutes old. Zapier and Make users can skip this. In Node.js:
import crypto from "node:crypto";
// rawBody: the request body exactly as received, before JSON.parse.
function verifyRoofGo(rawBody, header, secret) {
const parts = header.split(",").map((p) => p.split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!Number.isInteger(t) || !sigs.length) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest();
return sigs.some((s) => {
const given = Buffer.from(s, "hex");
return given.length === expected.length &&
crypto.timingSafeEqual(given, expected);
});
}Rotate secret makes a new secret. For the next 24 hours the old one signs too, so the header carries two v1 values: accept the request when either matches the secret you hold, then switch to the new one.
Ask Roo first
Roo, RoofGo’s AI assistant, answers most questions instantly and can read screenshots.
Prefer a person? Contact support
Still stuck? Email support@roofgo.com. Tell us your company name and, if it is about a property, its address.