TradesWork API and webhooks

Version 2026-10-14. Read and write customers, locations, jobs, dispatch, estimates, invoices, payments, the price book and technicians; get a signed webhook for every lifecycle event. Everything you can do here goes through the same rules, permissions and audit trail as the office app.

Authentication

Create an API key in Settings › Integrations › API Application Access (owners and admins) and pick its scopes. Send it as a bearer token. Apps that other companies install use OAuth access tokens instead (below).

curl https://tradesos-three.vercel.app/api/v1/platform/me \
  -H "Authorization: Bearer tos_YOUR_KEY"
customers:readLook up customers
jobs:readRead jobs and their status
jobs:bookBook new jobs (creates customers and locations)
reports:readRead revenue and attribution reports
availability:readRead open arrival windows and capacity
demand:readRead the demand forecast and demand requests (fill the board, throttle ads)
demand:respondReport campaigns launched or declined for demand requests
customers:writeCreate customers and service locations
jobs:writeCreate jobs
dispatch:writeSchedule, assign and move appointments through their statuses (dispatch, arrive, complete)
estimates:readRead estimates
invoices:readRead invoices and payments
payments:writeRecord payments and create card payment links
pricebook:readRead the price book
technicians:readRead the technician list
webhooks:manageSubscribe to webhooks (Zapier, n8n and other REST hooks)
leads:writeSend new leads to the lead inbox (answered by the text agent like any other lead)
marketing.readRead campaigns, campaign performance, lead status, conversions, audience counts, capacity and review summaries
marketing.writeCreate and update campaigns, record ad spend and send in new leads
booking.writeBook jobs and appointments (creates customers and locations)
audiences.exportExport hashed contact lists of customers who agreed to marketing (only if an owner turns it on)

Conventions

Endpoints

Account

GET/meThe company and access behind this credentialany

Customers

GET/customersList customerscustomers:read
GET/customers/{id}Get a customercustomers:read
POST/customersCreate a customer (optionally with a first location)customers:write

Locations

GET/locationsList service locationscustomers:read
GET/locations/{id}Get a locationcustomers:read
POST/customers/{id}/locationsAdd a service location to a customercustomers:write

Jobs

GET/jobsList jobsjobs:read
GET/jobs/{id}Get a jobjobs:read
POST/jobsBook a job for an existing customer and location
Lands unscheduled for the dispatcher. Idempotent on externalRef and on the Idempotency-Key header.
jobs:write

Appointments

GET/appointmentsList appointmentsjobs:read
GET/appointments/{id}Get an appointmentjobs:read

Dispatch

POST/appointmentsSchedule an appointment on a job (arrival window, technicians)dispatch:write
POST/appointments/{id}/assignAssign technicians (replaces the crew)dispatch:write
POST/appointments/{id}/statusMove an appointment: dispatched, en_route, working (arrived), done, canceled
Repeating the current status is a no-op. Completing needs an outcome and summary, and the company's required forms and photos.
dispatch:write

Estimates

GET/estimatesList estimates (one per option)estimates:read
GET/estimates/{id}Get an estimateestimates:read

Invoices

GET/invoicesList invoicesinvoices:read
GET/invoices/{id}Get an invoiceinvoices:read

Payments

POST/invoices/{id}/paymentsRecord a cash, check or other payment
Paid status follows payments. Use the Idempotency-Key header so a retry records the payment once.
payments:write
POST/invoices/{id}/checkoutCreate a card payment link for the balance
409 processor_unavailable when no card processor is connected (never a fake link).
payments:write
GET/paymentsList paymentsinvoices:read
GET/payments/{id}Get a paymentinvoices:read

Price book

GET/pricebook/itemsList price book itemspricebook:read
GET/pricebook/items/{id}Get a price book itempricebook:read

Technicians

GET/techniciansPeople who can be assigned to appointmentstechnicians:read

Webhooks

GET/webhooks/event-typesEvery event type a webhook can receiveany
GET/webhooksWebhook endpoints this credential createdwebhooks:manage
POST/webhooksSubscribe (REST hook: Zapier, n8n, Make)
Returns the signing secret once. Event types are limited to what this credential may read.
webhooks:manage
DELETE/webhooks/{id}Unsubscribewebhooks:manage
POST/webhooks/{id}/testSend a signed ping event nowwebhooks:manage

Full request and response schemas: /api/v1/openapi.json (OpenAPI 3.1; import it into Postman, Insomnia or a client generator).

Webhooks

Add an endpoint in Settings › Integrations › API Application Access (or subscribe through the API). Within about a minute of a change we POST an event. Respond with any 2xx within 10 seconds. Anything else is retried after 1 min, 5 min, 30 min, 2 h, 8 h, 24 h, then marked failed; you can resend it from the delivery log. An endpoint that fails continuously for 3 days is turned off and the owner sees why.

POST https://your-app.example.com/hooks
tradesos-signature: t=1760400000,v1=5f8c…
tradesos-event-id: 3b1c…   tradesos-event-type: job.completed

{ "id": "3b1c…", "type": "job.completed", "apiVersion": "2026-10-14", "createdAt": "…",
  "company": { "id": "…" },
  "data": { "objectType": "job", "objectId": "…", "object": { …same shape as GET /jobs/{id}… }, "change": { "before": …, "after": … } } }

Verify every delivery: compute HMAC-SHA256 of `${t}.${rawBody}` with your endpoint’s signing secret and compare it (constant time) with each v1. Reject timestamps more than 5 minutes old, and skip event ids you have already processed (deliveries can repeat). During a secret rotation two v1 values are sent for 24 hours.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, header, rawBody, now = Date.now()) {
  const parts = Object.groupBy(header.split(","), (p) => p.split("=")[0]);
  const t = Number(parts.t?.[0].split("=")[1]);
  if (!t || Math.abs(now / 1000 - t) > 300) return false;
  const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return (parts.v1 ?? []).some((p) => {
    const got = Buffer.from(p.split("=")[1], "hex");
    return got.length === want.length && timingSafeEqual(got, want);
  });
}
customer.createdCustomer createdcustomer
location.createdService location addedlocation
job.createdJob bookedjob
job.completedJob completedjob
job.canceledJob canceledjob
appointment.scheduledAppointment scheduledappointment
appointment.rescheduledAppointment rescheduledappointment
appointment.assignedTechnicians assigned (dispatch)appointment
appointment.status_changedAppointment status changed (dispatched, en route, arrived, done, canceled)appointment
estimate.createdEstimate createdestimate
estimate.soldEstimate soldestimate
estimate.dismissedEstimate dismissedestimate
invoice.createdInvoice createdinvoice
invoice.sentInvoice sentinvoice
payment.recordedPayment recorded (cash, check, other)payment
payment.succeededCard or ACH payment succeededpayment
payment.failedCard or ACH payment failedpayment
payment.refundedRefund issuedinvoice
membership.soldMembership soldmembership
membership.renewedMembership renewedmembership
membership.canceledMembership canceledmembership
membership.expiredMembership expiredmembership
lead.receivedLead receivedlead
lead.convertedLead converted to a joblead
pricebook_item.createdPrice book item createdpricebook_item
pricebook_item.updatedPrice book item updatedpricebook_item

Zapier, n8n and Make

Use REST hooks: with a key that has webhooks:manage, POST /webhooks with the catch URL and event types to subscribe, and DELETE /webhooks/{id} to unsubscribe. A key only receives events for records its scopes can read. Actions map to the POST endpoints (create customer, book job, schedule, record payment).

curl -X POST https://tradesos-three.vercel.app/api/v1/platform/webhooks \
  -H "Authorization: Bearer tos_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.zapier.com/hooks/catch/123/abc","eventTypes":["job.completed","invoice.sent"]}'

OAuth apps

Register your app in Settings › Integrations › API Application Access to get a client id and secret. Authorization code flow with PKCE (S256) is required; access tokens last one hour, refresh tokens rotate on every use, and a replayed code ends the grant. The app never gets more than the approving owner or admin can do.

1. Send the person to:
   https://tradesos-three.vercel.app/oauth/authorize?response_type=code&client_id=tosapp_…&redirect_uri=https://you/cb
     &scope=jobs:read%20invoices:read&state=…&code_challenge=…&code_challenge_method=S256
2. Exchange the code:
   curl -X POST https://tradesos-three.vercel.app/api/oauth/token -u tosapp_…:tossec_… \
     -d grant_type=authorization_code -d code=… -d redirect_uri=https://you/cb -d code_verifier=…
3. Refresh:  -d grant_type=refresh_token -d refresh_token=tosrt_…
4. Revoke:   curl -X POST https://tradesos-three.vercel.app/api/oauth/revoke -u tosapp_…:tossec_… -d token=…