Developers
Webhooks
EstimateKit can send every approved estimate to an address you choose, the moment the customer approves it. The message carries the customer, the good, better or best option they picked, every line item, the equipment, the totals, and the job and deposit it created. Use it with an automation tool like Make, n8n or Zapier to create the customer and job in your field-service software, or receive it on your own server.
Set it up
- Sign in as the owner or an admin and open Settings → Integrations.
- Paste the https:// address that should receive approved estimates and save.
- Press Send test. It sends your most recent approved estimate marked
"test": true, or sample data if you have none yet.
Included in the $49 a month plan. There's nothing to install and no extra charge.
With Make
- Create a scenario. For the first module, choose Webhooks, then Custom webhook, and add a new webhook.
- Copy the address Make gives you and paste it into Settings → Integrations in EstimateKit.
- With the module waiting for data, press Send test in EstimateKit. Make learns every field from it.
- Add the modules for your field-service app: for example, find or create the customer, then create the job or quote using
customer.*,selected_option.title,totals.totalandsummary.
Make's free plan includes custom webhooks, within its monthly limits. Check Make's pricing for the current limits.
With Zapier
- Create a Zap. For the trigger, choose Webhooks by Zapier, event Catch Hook.
- Copy the webhook URL Zapier gives you and paste it into Settings → Integrations in EstimateKit.
- Press Send test in EstimateKit, then Test trigger in Zapier. Every field below shows up to map.
- Add the actions for your field-service app and map the fields, the same way as with Make.
Webhooks by Zapier is a premium Zapier app, available on Zapier's paid plans. That's Zapier's pricing; EstimateKit doesn't charge for webhooks.
Anything else that takes a webhook
n8n, Pabbly Connect, Microsoft Power Automate or your own server: anything that can receive an https POST with a JSON body works. Point it at the address, save it in Settings → Integrations, and send a test.
How delivery works
- One
POSTwith a JSON body when an estimate becomes approved, usually within a few seconds. An estimate is only approved once, so it's sent once. - Headers:
Content-Type: application/json,X-EstimateKit-Event: estimate.approved,X-EstimateKit-Delivery(the delivery id) andX-EstimateKit-Signature. - Any 2xx answer within 10 seconds counts as delivered. Otherwise it's tried again after 2 seconds and 6 seconds, three tries in all. A 4xx answer other than 408 or 429 isn't retried, since trying again won't fix it.
- Redirects aren't followed; save the final address.
- To avoid creating the same job twice, look up
estimate.idin your other system before creating anything. - Every delivery is listed in Settings → Integrations with its result, and any approved estimate can be sent again from there. A resend has a new delivery id and the same
estimate.id. - Addresses must be public https:// URLs.
Checking the signature
Optional, and not needed with automation tools like Make or Zapier. Every delivery is signed with your company's signing secret (Settings → Integrations). The header looks like t=1767225600,v1=5f2b…: t is a Unix timestamp and v1 is the hex HMAC-SHA256 of {t}.{raw body} using the secret. Compare it in constant time, and reject old timestamps.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (a string, before JSON.parse)
// header: the X-EstimateKit-Signature header
// secret: the signing secret from Settings > Integrations
export function verifyEstimateKit(rawBody, header, secret) {
const t = /t=(\d+)/.exec(header ?? "")?.[1];
const v1 = /v1=([0-9a-f]+)/.exec(header ?? "")?.[1];
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutes
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Made a new secret in Settings? Deliveries are signed with the new one straight away.
Fields
Money is in dollars as numbers (9000, 12.5). Dates are ISO 8601 in UTC. Anything unknown is null.
eventAlways estimate.approved.versionPayload version. Currently 1. New fields may be added; existing ones won't change meaning within a version.idThis delivery's id. Also sent as the X-EstimateKit-Delivery header.testtrue when sent from Send test in Settings.created_atWhen this delivery was made (ISO 8601, UTC).company.nameYour company name.estimateid, number (EST-1042), title, status, sent_at, approved_at, approved_by (the name the customer signed), valid_until, notes, url (opens it in EstimateKit).customername, email, phone, address, city, state, zip, and full_address as one line.selected_optionThe option the customer picked: id, number (1, 2, 3), title, description, subtotal, tax, total. null on a single-price estimate.options_offeredEvery option on the estimate: number, title, total, and selected true or false.line_items[]The lines in the chosen option: name, description, category, sku, quantity, unit_price, total, tax_rate, internal_cost.equipment[]The line items whose category is Equipment, same shape as line_items.customer_equipment[]Units already on file for this customer: kind, brand, model, serial, capacity, refrigerant, filter_size, location, install_date, install_year_only, warranty_until.totalssubtotal, tax, total, deposit_due.jobThe job the approval created: id, number, status, url. null if you don't create jobs on approval.deposit_invoicenumber, amount, paid, status, and url (the customer's pay link). null when there's no deposit.summaryThe whole quote as plain text, one line per item. Handy for a single notes or description field.internal_cost is what the line costs you, from your price book. It's for your own job costing; don't map it anywhere a customer sees.
Sample payload
{
"event": "estimate.approved",
"version": "1",
"id": "4f1c2a9e-7b3d-4e8a-9c21-6d0b5e8f3a17",
"test": false,
"created_at": "2026-03-12T17:04:11.482Z",
"company": { "name": "Summit Heating & Air" },
"estimate": {
"id": "7c2e9b14-3f6a-4d8e-b1c5-2a9f0e4d7b63",
"number": "EST-1042",
"title": "Heat pump replacement",
"status": "approved",
"sent_at": "2026-03-11T21:30:02.117Z",
"approved_at": "2026-03-12T17:04:09.905Z",
"approved_by": "Dana Hughes",
"valid_until": null,
"notes": null,
"url": "https://estimatekit.com/estimates/7c2e9b14-3f6a-4d8e-b1c5-2a9f0e4d7b63"
},
"customer": {
"name": "Dana Hughes",
"email": "dana@example.com",
"phone": "(555) 010-0142",
"address": "48 Cedar Lane",
"city": "Springfield",
"state": "IL",
"zip": "62704",
"full_address": "48 Cedar Lane, Springfield, IL 62704"
},
"selected_option": {
"id": "b8d4f2a1-6c3e-4a9b-8f17-0e2d5c9a4b36",
"number": 2,
"title": "Better",
"description": "Variable-speed heat pump with 10-year parts warranty",
"subtotal": 9000,
"tax": 0,
"total": 9000
},
"options_offered": [
{ "number": 1, "title": "Good", "total": 7500, "selected": false },
{ "number": 2, "title": "Better", "total": 9000, "selected": true },
{ "number": 3, "title": "Best", "total": 10500, "selected": false }
],
"line_items": [
{
"name": "Heat pump, 4 ton", "description": "Outdoor unit", "category": "Equipment",
"sku": "HP-48-VS", "quantity": 1, "unit_price": 7000, "total": 7000,
"tax_rate": 0, "internal_cost": 5000
},
{
"name": "Install labor", "description": "Set, braze, evacuate, start up", "category": "Labor",
"sku": null, "quantity": 16, "unit_price": 125, "total": 2000,
"tax_rate": 0, "internal_cost": 60
}
],
"equipment": [
{
"name": "Heat pump, 4 ton", "description": "Outdoor unit", "category": "Equipment",
"sku": "HP-48-VS", "quantity": 1, "unit_price": 7000, "total": 7000,
"tax_rate": 0, "internal_cost": 5000
}
],
"customer_equipment": [
{
"kind": "furnace", "brand": "Lennox", "model": "ML180UH070", "serial": "5811K04213",
"capacity": "70k BTU", "refrigerant": null, "filter_size": "16x25x1", "location": "Basement",
"install_date": "2011-01-01", "install_year_only": true, "warranty_until": null
}
],
"totals": { "subtotal": 9000, "tax": 0, "total": 9000, "deposit_due": 1000 },
"job": {
"id": "e3a7c1d9-2b4f-4c8e-a6d0-9f1b3e5c7a28", "number": "JOB-1042", "status": "unscheduled",
"url": "https://estimatekit.com/jobs/e3a7c1d9-2b4f-4c8e-a6d0-9f1b3e5c7a28"
},
"deposit_invoice": {
"number": "INV-1042-D", "amount": 1000, "paid": 0, "status": "sent",
"url": "https://estimatekit.com/invoice/view/…"
},
"summary": "EST-1042 approved by Dana Hughes\nOption: Better ($9,000.00)\n1 x Heat pump, 4 ton $7,000.00\n16 x Install labor $2,000.00\nSubtotal $9,000.00, tax $0.00, total $9,000.00\nDeposit $1,000.00 (INV-1042-D)"
}Questions, or need a field that isn't here?
Write to hello@estimatekit.com. You'll get Travis, who builds EstimateKit. Not a customer yet? Start the 14-day trial, no card needed.