API & Webhooks

Everything in your account — customers, requests, jobs, estimates, invoices, payments — over a plain REST API, and a signed webhook for every state change. Built for Zapier, Make, a website form builder, or software your own developer writes.

Machine-readable: OpenAPI 3.1. This page and that document are generated from the same source.

Zapier / Make quick-start

Trigger a Zap when something happens here

  1. In Zapier, start a Zap with Webhooks by Zapier → Catch Hook and copy the hook URL. In Make, use Webhooks → Custom webhook.
  2. In TradesBackbone open Settings → Integrations → Webhooks, add an endpoint, paste the URL, and tick the events you want (or none, to receive everything).
  3. Copy the signing secret shown once. Verify the x-tb-signature header with it (snippet below) — or skip verification for a private hook URL you never share.
  4. Create a test job, estimate or invoice; the sample payload for every event is listed under Webhooks.

Push data into TradesBackbone from a Zap

  1. Open Settings → API keys and create a key (Elite plan). Choose read & write. Copy it — it is shown once.
  2. Add a Webhooks by Zapier → Custom Request step (Make: HTTP → Make a request).
  3. Method POST, URL https://tradesbackbone.com/api/v1/requests (or any endpoint below), header Authorization: Bearer YOUR_KEY, header Content-Type: application/json.
  4. Body: JSON with the fields in the endpoint's table — e.g. a lead from your website becomes a work request that rings the office.
curl -X POST https://tradesbackbone.com/api/v1/requests \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name":"Dana Reyes","phone":"801-555-0142","title":"Water heater leaking","source":"website"}'

Authentication

Every request carries Authorization: Bearer ms_tradesbackbone_…. Keys are created by the account owner at Settings → API keys and are included on the Elite plan; a key from a plan without API access answers 403 upgrade_required on every call, and a revoked key answers 401.

Scopes. A key is minted read or read & write. GET needs read; POST and PATCH need write. A read-only key on a write answers 403 insufficient_scope.

Tenant. A key belongs to one business and every call is scoped to it. A record that belongs to another business answers 404 — never 403, so an id cannot be probed.

Rate limit. 600 requests per minute per key. Past that: 429 rate_limited with Retry-After. Every keyed response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

Conventions

  • Lists return { data: [...], nextCursor }. Pass ?cursor= back to get the next page; limit is 1–200 (default 50). Most lists take ?updatedSince=<ISO-8601>, which is how a polling trigger asks for “what changed since my last run”.
  • Single records return { data: {...} }; creates answer 201.
  • Errors are { error: "human sentence", code: "machine_code" }validation_error, not_found, insufficient_scope, upgrade_required, rate_limited, checklist_incomplete.
  • Money is in dollars with cents as decimals: 189.5 is $189.50. Nothing is in cents.
  • Dates are ISO-8601 in UTC. A date without a time (2026-10-02) is accepted for job scheduling.
  • Ids are opaque strings; do not parse them.

Endpoints

Base URL https://tradesbackbone.com. $KEY in the examples is your API key.

Customers

GET/api/v1/customersList customers

Newest first. Use `email` or `phone` for a Zapier-style "find customer" step — both are exact matches (phone compares the last 10 digits).

Query

limitintegerPage size, 1–200. Default 50.
cursorstringThe `nextCursor` from the previous page.
updatedSinceISO-8601Only records changed at or after this time — the polling-trigger primitive.
emailstringExact email, case-insensitive.
phonestringPhone number; digits only are compared.

Returns { data: Customer[], nextCursor }

curl -H "Authorization: Bearer $KEY" "https://tradesbackbone.com/api/v1/[email protected]"
POST/api/v1/customersCreate a customer

An individual (firstName required) or a business (customerType BUSINESS, businessName required). `smsConsent: true` records that the customer agreed to texts; without it the record is opted out of SMS.

Body (JSON)

customerTypeINDIVIDUAL | BUSINESSDefault INDIVIDUAL.
firstNamestringRequired for an individual.
lastNamestring
businessNamestringRequired for a business.
emailstring
phonestring
addressstring
citystring
statestring
zipstring
notesstring
tagsstring[]Up to 100.
smsConsentbooleanThe business confirmed the customer agreed to texts.

Returns 201 { data: Customer }

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"firstName":"Dana","lastName":"Reyes","email":"[email protected]","phone":"801-555-0142"}' https://tradesbackbone.com/api/v1/customers
GET/api/v1/customers/{id}Get a customer

One record. A customer in another business answers 404.

Returns { data: Customer }

curl -H "Authorization: Bearer $KEY" https://tradesbackbone.com/api/v1/customers/CUSTOMER_ID
PATCH/api/v1/customers/{id}Update a customer

Send only the fields to change. Emits `customer.updated`.

Body (JSON)

firstName / lastName / businessNamestring
email / phone / address / city / state / zip / notesstring | null
tagsstring[]Replaces the tag list.
taxExemptboolean

Returns { data: Customer }

curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"phone":"801-555-0199"}' https://tradesbackbone.com/api/v1/customers/CUSTOMER_ID

Work requests

GET/api/v1/requestsList work requests

What customers asked for before it became a job or a quote.

Query

limitintegerPage size, 1–200. Default 50.
cursorstringThe `nextCursor` from the previous page.
updatedSinceISO-8601Only records changed at or after this time — the polling-trigger primitive.
statusNEW | CONTACTED | ASSESSMENT_SCHEDULED | CONVERTED | DECLINED
customerIdstring

Returns { data: WorkRequest[], nextCursor }

curl -H "Authorization: Bearer $KEY" "https://tradesbackbone.com/api/v1/requests?status=NEW"
POST/api/v1/requestsCreate a work request

The intake for a lead from a website form builder, an ads platform or a lead service. Finds or creates the customer by phone/email, then emits `request.created`. Needs a name, a title, and a phone or an email.

Body (JSON)

name*stringThe person asking.
title*stringWhat they need done.
phonestringPhone or email is required.
emailstring
detailsstring
preferredTimesstring
address / city / state / zipstring
customerIdstringAttach to an existing customer instead of matching by contact.
sourcestringWhere the lead came from. Default "api".

Returns 201 { data: WorkRequest }

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"name":"Dana Reyes","phone":"801-555-0142","title":"Water heater leaking","source":"website"}' https://tradesbackbone.com/api/v1/requests
GET/api/v1/requests/{id}Get a work request

Returns { data: WorkRequest }

curl -H "Authorization: Bearer $KEY" https://tradesbackbone.com/api/v1/requests/REQUEST_ID

Jobs

GET/api/v1/jobsList jobs

Each row also carries `lastActivityAt` and `missedWindow`.

Query

limitintegerPage size, 1–200. Default 50.
cursorstringThe `nextCursor` from the previous page.
updatedSinceISO-8601Only records changed at or after this time — the polling-trigger primitive.
statusSCHEDULED | IN_PROGRESS | COMPLETE | INVOICED | CANCELLED
customerIdstring

Returns { data: Job[], nextCursor }

curl -H "Authorization: Bearer $KEY" "https://tradesbackbone.com/api/v1/jobs?status=SCHEDULED&limit=20"
POST/api/v1/jobsCreate a job

Books work for an existing customer. Emits `job.created` (and `job.scheduled` when a date is given).

Body (JSON)

customerId*string
title*string
descriptionstring
scheduledDateISO-8601
scheduledStartTime / scheduledEndTimeHH:MM
address / city / state / zipstringJob site, if not the customer's address.
assignedToIdstringA team member id.
statusJobStatusDefault SCHEDULED.
notesstring

Returns 201 { data: Job }

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"customerId":"CUSTOMER_ID","title":"Furnace tune-up","scheduledDate":"2026-10-02"}' https://tradesbackbone.com/api/v1/jobs
GET/api/v1/jobs/{id}Get a job

With the customer and the assignee.

Returns { data: Job }

curl -H "Authorization: Bearer $KEY" https://tradesbackbone.com/api/v1/jobs/JOB_ID
PATCH/api/v1/jobs/{id}Update a job

Change the status, date, assignee, title or notes. Marking a job COMPLETE runs the same checklist gate as the field app (409 `checklist_incomplete` if required items are open) and emits `job.status_changed` + `job.completed`.

Body (JSON)

statusJobStatus
scheduledDateISO-8601Emits `job.scheduled` when it changes.
scheduledStartTime / scheduledEndTimeHH:MM
assignedToIdstring | ""Empty string unassigns.
title / description / notesstring
address / city / state / zipstring

Returns { data: Job }

curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"status":"COMPLETE"}' https://tradesbackbone.com/api/v1/jobs/JOB_ID

Estimates

GET/api/v1/estimatesList estimates

With line items.

Query

limitintegerPage size, 1–200. Default 50.
cursorstringThe `nextCursor` from the previous page.
updatedSinceISO-8601Only records changed at or after this time — the polling-trigger primitive.
statusDRAFT | SENT | APPROVED | DECLINED | EXPIRED
customerIdstring

Returns { data: Estimate[], nextCursor }

curl -H "Authorization: Bearer $KEY" "https://tradesbackbone.com/api/v1/estimates?status=SENT"
POST/api/v1/estimatesCreate an estimate

A DRAFT with one or more line items. Sending it to the customer is done from the dashboard.

Body (JSON)

customerId*string
jobIdstring
lineItems*{ description, quantity, unitPrice, category?, taxable? }[]unitPrice in dollars.
taxRatenumber 0–1Defaults to the business rate.
validUntilISO-8601
notesstring

Returns 201 { data: Estimate }

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"customerId":"CUSTOMER_ID","lineItems":[{"description":"Annual service","quantity":1,"unitPrice":189}]}' https://tradesbackbone.com/api/v1/estimates
GET/api/v1/estimates/{id}Get an estimate

With line items and pricing options.

Returns { data: Estimate }

curl -H "Authorization: Bearer $KEY" https://tradesbackbone.com/api/v1/estimates/ESTIMATE_ID
PATCH/api/v1/estimates/{id}Update an estimate

Record a decline, or change notes / validity. Approval is the customer's act and happens through their approval link, not the API. Declining emits `estimate.declined` and cancels queued quote follow-ups.

Body (JSON)

status"DECLINED"The only status the API may set.
notesstring
validUntilISO-8601 | null

Returns { data: Estimate }

curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"status":"DECLINED"}' https://tradesbackbone.com/api/v1/estimates/ESTIMATE_ID

Invoices

GET/api/v1/invoicesList invoices

Each row carries `total`, `amountPaid`, `balance`, `issuedAt`, `dueDate` and `daysOverdue`.

Query

limitintegerPage size, 1–200. Default 50.
cursorstringThe `nextCursor` from the previous page.
updatedSinceISO-8601Only records changed at or after this time — the polling-trigger primitive.
statusDRAFT | SCHEDULED | SENT | VIEWED | PAID | OVERDUE | CANCELLED
outstandingtrueShorthand for every status that still owes money.
customerIdstring

Returns { data: InvoiceSummary[], nextCursor }

curl -H "Authorization: Bearer $KEY" "https://tradesbackbone.com/api/v1/invoices?outstanding=true"
POST/api/v1/invoicesCreate an invoice

A DRAFT from line items — same numbering and tax rules as the dashboard. Emits `invoice.created`. Sending is a dashboard action.

Body (JSON)

customerId*string
jobIdstringThe job flips to INVOICED.
estimateIdstring
lineItems*{ description, quantity, unitPrice, category?, taxable? }[]unitPrice in dollars.
taxRatenumber 0–1Defaults to the business rate.
dueDateISO-8601
notesstring

Returns 201 { data: Invoice }

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"customerId":"CUSTOMER_ID","lineItems":[{"description":"Furnace tune-up","quantity":1,"unitPrice":189}]}' https://tradesbackbone.com/api/v1/invoices
GET/api/v1/invoices/{id}Get an invoice

With line items, payments, `amountPaid` and `balance`.

Returns { data: Invoice }

curl -H "Authorization: Bearer $KEY" https://tradesbackbone.com/api/v1/invoices/INVOICE_ID

Payments

GET/api/v1/paymentsList payments

Money received, newest first. Defaults to the last 30 days.

Query

limitintegerPage size, 1–200.
cursorstring
from / toISO-8601Received-at bounds.
invoiceIdstring
includeReturnedtrueInclude payments that bounced back.

Returns { data: Payment[], nextCursor }

curl -H "Authorization: Bearer $KEY" "https://tradesbackbone.com/api/v1/payments?from=2026-09-01T00:00:00Z"
POST/api/v1/paymentsRecord a payment

A payment taken by hand — cash, check, or other (Zelle, Venmo). Partial amounts are fine; the invoice flips PAID when the balance reaches zero. Emits `payment.recorded`, then `invoice.paid` if it settled. Card and bank payments are recorded by Stripe, not here.

Body (JSON)

invoiceId*string
amount*numberDollars. Not more than the balance.
method*CASH | CHECK | OTHER
referencestringCheck number, transfer id.
notestring
receivedAtISO-8601Defaults to now; never in the future.

Returns 201 { data: { paymentId, invoiceId, amount, settled, balance, invoiceStatus } }

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"invoiceId":"INVOICE_ID","amount":189,"method":"CHECK","reference":"1187"}' https://tradesbackbone.com/api/v1/payments

Business summary

GET/api/v1/business-summaryBusiness summary

Today's numbers: jobs, outstanding receivables, recent activity — the same read the Martello executive layer uses.

Returns { data: { … } }

curl -H "Authorization: Bearer $KEY" https://tradesbackbone.com/api/v1/business-summary

Webhook catalog

GET/api/v1/webhook-eventsWebhook event catalog

Every event an endpoint can subscribe to, with a sample payload each, plus the header names and the signing rule.

Returns { data: { event, label, sample }[], headers, signing }

curl -H "Authorization: Bearer $KEY" https://tradesbackbone.com/api/v1/webhook-events

Webhooks

Add endpoints at Settings → Integrations → Webhooks (Elite plan). Each receives a POST with the JSON below and three headers: x-tb-event, x-tb-timestamp (unix seconds) and x-tb-signature. An endpoint with no event list receives every event — what a catch-hook wants.

Delivery. 3 attempts over about 23 seconds (a fresh timestamp and signature on each), retrying only on a network failure, a timeout, 408, 429 or a 5xx. After that the delivery is marked failed; every attempt is listed under Deliveries on the endpoint. An endpoint that fails 15 deliveries in a row is switched off until you switch it back on. Answer 2xx quickly and do the work afterwards.

Signature. HMAC-SHA256 over `${timestamp}.${rawBody}` with the endpoint's secret, hex-encoded — the timestamp is inside the signed string, so a captured delivery cannot be replayed later. Reject anything older than five minutes.

import crypto from "node:crypto";

export function verifyTradesBackbone(req, secret) {
  const ts = Number(req.headers["x-tb-timestamp"]);
  const sig = req.headers["x-tb-signature"];
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;        // replay window
  const expected = crypto.createHmac("sha256", secret)
    .update(`${ts}.${req.rawBody}`)                                // timestamp + RAW body
    .digest("hex");
  return expected.length === sig.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

Envelope

{
  "event": "invoice.paid",
  "businessId": "clx1biz…",
  "createdAt": "2026-10-02T21:14:00.000Z",
  "data": { "invoiceId": "clx1inv…", "total": 1250, "paymentMethod": "CARD" }
}

Events

job.createdA job is booked
{
  "jobId": "clx1job…",
  "customerId": "clx1cus…",
  "title": "Furnace tune-up",
  "status": "SCHEDULED",
  "scheduledDate": "2026-10-02T00:00:00.000Z"
}
job.scheduledA job is given a date, or its date moves
{
  "jobId": "clx1job…",
  "customerId": "clx1cus…",
  "scheduledDate": "2026-10-02T00:00:00.000Z",
  "previousScheduledDate": null
}
job.status_changedA job changes status
{
  "jobId": "clx1job…",
  "customerId": "clx1cus…",
  "status": "IN_PROGRESS",
  "previousStatus": "SCHEDULED"
}
job.completedA job is completed
{
  "jobId": "clx1job…",
  "customerId": "clx1cus…",
  "completedAt": "2026-10-02T21:14:00.000Z"
}
estimate.sentAn estimate goes out
{
  "estimateId": "clx1est…",
  "customerId": "clx1cus…",
  "total": 1250,
  "channel": "EMAIL"
}
estimate.approvedA customer approves an estimate
{
  "estimateId": "clx1est…",
  "customerId": "clx1cus…",
  "total": 1250,
  "approverName": "Dana Reyes"
}
estimate.declinedAn estimate is declined
{
  "estimateId": "clx1est…",
  "customerId": "clx1cus…",
  "total": 1250
}
invoice.createdAn invoice is created
{
  "invoiceId": "clx1inv…",
  "number": "INV-1042",
  "customerId": "clx1cus…",
  "jobId": "clx1job…",
  "total": 1250,
  "status": "DRAFT"
}
invoice.sentAn invoice goes out
{
  "invoiceId": "clx1inv…",
  "customerId": "clx1cus…",
  "total": 1250,
  "channel": "EMAIL",
  "dueDate": "2026-11-01T00:00:00.000Z"
}
invoice.paidAn invoice is paid in full
{
  "invoiceId": "clx1inv…",
  "total": 1250,
  "paymentMethod": "CARD"
}
payment.recordedA payment is recorded (cash, check, card or bank)
{
  "paymentId": "clx1pay…",
  "invoiceId": "clx1inv…",
  "customerId": "clx1cus…",
  "amount": 500,
  "method": "CHECK",
  "reference": "1187",
  "settled": false
}
customer.createdA new customer is added
{
  "customerId": "clx1cus…",
  "firstName": "Dana",
  "lastName": "Reyes",
  "email": "[email protected]",
  "phone": "+18015550142"
}
customer.updatedA customer's details change
{
  "customerId": "clx1cus…",
  "changed": [
    "phone",
    "address"
  ]
}
request.createdA new work request comes in
{
  "requestId": "clx1req…",
  "customerId": "clx1cus…",
  "title": "Water heater leaking",
  "source": "website"
}
bill.recordedA vendor bill is recorded
{
  "billId": "clx1bil…",
  "vendorId": "clx1ven…",
  "total": 312.4,
  "dueOn": "2026-10-15T00:00:00.000Z"
}
timesheet.approvedHours are approved
{
  "timeEntryId": "clx1tim…",
  "memberId": "clx1mem…",
  "jobId": "clx1job…",
  "minutes": 255
}

Need an endpoint or an event that is not here?

Tell us what you are connecting from the feedback page — the surface grows with what customers plug in.