Skip to content
Developers

Webhooks

TrackOver can post a message to your URL the moment something happens: a new lead, an accepted estimate, a payment. Use it with Zapier, Make, or your own server. Add a webhook in Settings > Webhooks, or through the API. Webhooks and the API come with the plan that includes API access.

What a delivery looks like

Every delivery is a POST with a JSON body. The same envelope wraps every event, and data holds the fields listed below.

POST https://your-url.example.com/hook
content-type: application/json
x-trackover-event: invoice.paid
x-trackover-delivery: 0c2f7a6e-...
x-trackover-signature: 9f86d081...

{
  "event": "invoice.paid",
  "org": "your-company",
  "at": "2026-10-08T14:30:00.000Z",
  "data": { "number": 1042, "customer": "Dana Ortiz", "total": 4850 }
}
  • x-trackover-event: the event name.
  • x-trackover-delivery: one id per delivery, the same on every retry. Save it and skip any you have already handled.
  • x-trackover-signature: proves the message came from TrackOver. See below.

Answer with any 2xx status within 5 seconds. Do slow work after you answer.

Check the signature

Each webhook has its own signing secret, shown once when you make it. The signature is the HMAC-SHA256 of the raw request body using that secret, written as hex. Compute it yourself and compare. Reject the request if they differ.

import crypto from "node:crypto";

// rawBody is the request body exactly as received, before any JSON parsing.
export function isFromTrackOver(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Keep the secret on your server. If it leaks, delete the webhook and make a new one.

Retries and the delivery log

  • A 2xx answer counts as delivered.
  • A network error, 408, 429 or 5xx is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours. That is 6 tries in all.
  • Any other answer (such as 404 or 410) fails right away and is not retried.
  • Every retry sends the same body, signed with the webhook's current secret.
  • Deliveries are kept for 30 days. See them in Settings > Webhooks > Show recent deliveries, or read them with the API below.

Deliveries can arrive more than once or out of order, so use x-trackover-delivery to skip repeats.

Events

lead.created

A new lead comes in from your booking page, a website form, the API or an email forward.

FieldTypeNotes
idstringThe lead id. Left out on leads from the public booking form.
namestringWho asked for the work.
emailstringLeft out on leads from the public booking form.
phonestringLeft out on leads from the public booking form.
addressstringJob site, when given.
detailsstringWhat they asked for.
sourcestringWhere it came from, such as direct, referral or a campaign name.
{
  "event": "lead.created",
  "org": "your-company",
  "at": "2026-10-08T14:30:00.000Z",
  "data": {
    "id": "5b1c0f1e-3f0a-4d0e-9a53-2f1d6a9e7a10",
    "name": "Dana Ortiz",
    "email": "dana@example.com",
    "phone": "555-0100",
    "address": "42 Quarry Rd",
    "details": "Need a gravel driveway",
    "source": "website"
  }
}

job.created

A job is created by a recurring schedule or by the email inbox importer. Jobs saved from the phone do not fire it.

FieldTypeNotes
idstringThe job id.
customerstringCustomer name.
addressstringJob site.
sourcestringrecurring or email.
{
  "event": "job.created",
  "org": "your-company",
  "at": "2026-10-08T14:30:00.000Z",
  "data": {
    "id": "job_8f2a",
    "customer": "Dana Ortiz",
    "address": "42 Quarry Rd",
    "source": "recurring"
  }
}

estimate.sent

An estimate is emailed or texted to the customer.

FieldTypeNotes
idstringThe estimate id.
customerstring | nullCustomer name.
totalnumber | nullEstimate total in dollars.
{
  "event": "estimate.sent",
  "org": "your-company",
  "at": "2026-10-08T14:30:00.000Z",
  "data": {
    "id": "est_31c9",
    "customer": "Dana Ortiz",
    "total": 4850
  }
}

estimate.accepted

The customer accepts the estimate on their link.

FieldTypeNotes
customerstringCustomer name.
amountnumberAccepted amount in dollars.
{
  "event": "estimate.accepted",
  "org": "your-company",
  "at": "2026-10-08T14:30:00.000Z",
  "data": {
    "customer": "Dana Ortiz",
    "amount": 4850
  }
}

invoice.sent

An invoice is emailed or texted to the customer.

FieldTypeNotes
idstringThe invoice id.
numbernumber | nullInvoice number.
customerstring | nullCustomer name.
totalnumber | nullInvoice total in dollars.
{
  "event": "invoice.sent",
  "org": "your-company",
  "at": "2026-10-08T14:30:00.000Z",
  "data": {
    "id": "inv_77d1",
    "number": 1042,
    "customer": "Dana Ortiz",
    "total": 4850
  }
}

invoice.paid

An invoice is fully paid. Part payments do not fire it; use payment.received for those.

FieldTypeNotes
numbernumber | nullInvoice number.
customerstring | nullCustomer name.
totalnumber | nullInvoice total in dollars.
{
  "event": "invoice.paid",
  "org": "your-company",
  "at": "2026-10-08T14:30:00.000Z",
  "data": {
    "number": 1042,
    "customer": "Dana Ortiz",
    "total": 4850
  }
}

payment.received

An online payment is recorded, part or full. Once per payment.

FieldTypeNotes
numbernumber | nullInvoice number.
customerstring | nullCustomer name.
amountnumberThis payment in dollars.
methodstring | nullHow they paid, such as card or bank.
{
  "event": "payment.received",
  "org": "your-company",
  "at": "2026-10-08T14:30:00.000Z",
  "data": {
    "number": 1042,
    "customer": "Dana Ortiz",
    "amount": 2425,
    "method": "card"
  }
}

Manage webhooks with the API

Send your API key as Authorization: Bearer tok_live_.... Make a key in Settings > Webhooks.

  • GET /api/v1/webhooks: your webhooks (never the secret) and the event names you can subscribe to.
  • POST /api/v1/webhooks with { "url": "https://...", "events": ["lead.created"] }: makes one. The answer includes the signing secret once. The URL must be https. Up to 10 per company.
  • DELETE /api/v1/webhooks/:id: removes one.
  • GET /api/v1/webhooks/deliveries: the delivery log. Filter with hook_id, state (retrying, delivered, failed), since and limit.
  • POST /api/v1/webhooks/deliveries/:id/redeliver: sends that delivery again now.

More on the API and Zapier recipes: API help.