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
2xxanswer counts as delivered. - A network error,
408,429or5xxis tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours. That is 6 tries in all. - Any other answer (such as
404or410) 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.
| Field | Type | Notes |
|---|---|---|
id | string | The lead id. Left out on leads from the public booking form. |
name | string | Who asked for the work. |
email | string | Left out on leads from the public booking form. |
phone | string | Left out on leads from the public booking form. |
address | string | Job site, when given. |
details | string | What they asked for. |
source | string | Where 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.
| Field | Type | Notes |
|---|---|---|
id | string | The job id. |
customer | string | Customer name. |
address | string | Job site. |
source | string | recurring 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.
| Field | Type | Notes |
|---|---|---|
id | string | The estimate id. |
customer | string | null | Customer name. |
total | number | null | Estimate 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.
| Field | Type | Notes |
|---|---|---|
customer | string | Customer name. |
amount | number | Accepted 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.
| Field | Type | Notes |
|---|---|---|
id | string | The invoice id. |
number | number | null | Invoice number. |
customer | string | null | Customer name. |
total | number | null | Invoice 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.
| Field | Type | Notes |
|---|---|---|
number | number | null | Invoice number. |
customer | string | null | Customer name. |
total | number | null | Invoice 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.
| Field | Type | Notes |
|---|---|---|
number | number | null | Invoice number. |
customer | string | null | Customer name. |
amount | number | This payment in dollars. |
method | string | null | How 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/webhookswith{ "url": "https://...", "events": ["lead.created"] }: makes one. The answer includes the signingsecretonce. 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 withhook_id,state(retrying, delivered, failed),sinceandlimit.POST /api/v1/webhooks/deliveries/:id/redeliver: sends that delivery again now.
More on the API and Zapier recipes: API help.