Skip to main content

Developers

Plug TowSitePros into anything: a motor-club feed, your billing system, Zapier, your own dispatch software.

What you can build

Create, read, update and assign jobs, add notes and read the timeline through the API; photos, archiving and link management stay in the dashboard for now. Everything that happens to a job can be pushed to your system as it happens. A job created by the API shows up on the dispatch board, on the driver's page and on the customer's tracking page exactly as if a dispatcher had typed it, and it queues the same texts.

  • Push jobs in from a motor club, a police rotation, a fleet account or your own intake form, with your reference on each one.
  • Pull jobs out to billing, accounting or a data warehouse, with every milestone timestamp and the final price.
  • React in real time with webhooks: a customer message, a rating, a truck going en route.
  • Migrate history from a spreadsheet with the CSV import under Developers in the dashboard (up to 500 rows or 2 MB per file; imported jobs never text anyone).

Authentication

Create a key under Developers in the dashboard and send it as a bearer token. Give each system its own key with only the scopes it needs, so you can revoke one without touching the others. Keys are shown once; we store only a hash.

Authorization: Bearer tsp_live_…
ScopeAllows
jobs:readRead jobs, their timeline, units on them.
jobs:writeCreate and update jobs, assign units, add notes.
roster:readRead the roster and last positions.
services:readRead services and the rate card.
webhooks:manageAdd, pause, rotate and delete webhook endpoints.
accounts:readRead the commercial accounts directory: names, kinds and contacts. Never rates, billing addresses or internal notes.

Limits: 600 reads and 120 writes per minute per key. Over the limit you get 429 with a Retry-After header.

Endpoints

Base URL https://www.towsitepros.com/api/v1. JSON in, JSON out. Every error has the same shape: { "error": { "code", "message", "details" } }.

MethodPathScopeWhat it does
GET/mejobs:readWho this key is: account, sites, scopes.
GET/jobsjobs:readList jobs. Filter by status, since, updated_since, external_ref. Cursor-paged.
POST/jobsjobs:writeCreate a job. Mints the tracking link and queues the "we got your request" text.
GET/jobs/{id}jobs:readOne job with its unit, position and tracking link.
PATCH/jobs/{id}jobs:writeChange anything writable. A status change stamps the milestone and texts the customer.
POST/jobs/{id}/assignjobs:writeHand the job to a unit; the driver gets their job link.
DELETE/jobs/{id}/assignjobs:writeTake the job back off the board.
POST/jobs/{id}/notesjobs:writeAdd a note, optionally visible to the customer.
POST/jobs/{id}/reopenjobs:writeBring a finished job back to confirmed, with a reason. The only way out of completed, cancelled or goa.
GET/jobs/{id}/eventsjobs:readThe append-only timeline.
GET/unitsroster:readDrivers and trucks, with the last position each shared.
GET/servicesservices:readServices and the rate card.
GET · POST/webhookswebhooks:manageList or add endpoints. The secret comes back once.
PATCH · DELETE/webhooks/{id}webhooks:managePause, resume, change events, rotate the secret, delete.
POST/webhooks/{id}/testwebhooks:manageSend a ping now and see what your endpoint answered.
GET/webhooks/{id}/deliverieswebhooks:manageThe last 100 deliveries.

Create a job

curl -X POST https://www.towsitepros.com/api/v1/jobs \
  -H "Authorization: Bearer tsp_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: mc-1042" \
  -d '{
    "external_ref": "MC-1042",
    "source": "motor_club",
    "payer_type": "motor_club",
    "service_type": "flatbed",
    "urgency": "urgent",
    "customer": { "name": "Jane Doe", "phone": "(336) 555-0100", "sms_consent": true },
    "vehicle": { "year": "2019", "make": "Honda", "model": "Civic", "drive_type": "fwd" },
    "pickup": { "location": "I-40 exit 214, Greensboro NC" },
    "destination": "Bob'"'"'s Auto, High Point NC"
  }'

Send an Idempotency-Key and a retry with the same body returns the first response instead of a second truck. external_ref is unique per account: a duplicate answers 409 with the existing job's id. Set customer.sms_consent only when the customer agreed to texts; that consent is yours to hold, not ours.

How do I change a job's status safely?

Send status on PATCH /jobs/{id}. Every move is checked against the same rules the dispatch board, the driver page and the tracking page are checked against, so the API can never put a job somewhere a dispatcher could not have put it. Three rules are worth knowing before you write the integration.

  • A finished job cannot be moved. Once a job is completed, cancelled or goa, a PATCH that changes its status is refused with 409 job_transition_refused. Use POST /jobs/{id}/reopen instead, which asks you to say why.
  • Cancelling needs a reason. Send cancel_reason (3 to 500 characters) with "status": "cancelled". goa accepts one and does not insist, because a truck arriving to an empty shoulder is its own explanation. Both land in the same field and both show on the job.
  • Send expected_status and the write becomes conditional. If the job has moved since you read it, nothing is written and you get 409 job_stale with the status it is in now. This is how you stop a queued update from undoing a driver who got there first. It is optional: leave it out and the call behaves exactly as it always has.
curl -X PATCH https://www.towsitepros.com/api/v1/jobs/{id} \
  -H "Authorization: Bearer tsp_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "cancelled",
    "expected_status": "dispatched",
    "cancel_reason": "Customer found another truck"
  }'

PATCH ignores Idempotency-Key. expected_status is the safe way to retry a status change, because a retry that arrives after the job moved is refused rather than replayed onto a job that has moved on.

Reopen a finished job

A reopen brings the job back to confirmed, stamps timestamps.reopened_at, increments reopen_count, and clears the finished run's cancel_reason and the customer's rating. All four are written to the timeline first, so nothing is lost. The milestones (completed_at and the rest) are write-once and keep the first run's values, which is what reopen_count is for: without it a job that ran twice looks exactly like one that ran once.

curl -X POST https://www.towsitepros.com/api/v1/jobs/{id}/reopen \
  -H "Authorization: Bearer tsp_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reopen-MC-1042" \
  -d '{ "reason": "Winch line slipped, vehicle still on the shoulder" }'

What does each error code mean?

Every one of these is safe to retry after you re-read the job, and in every one of them nothing was written, with one exception: a PATCH that changes the status and other fields is two writes, so if the second fails the response carries details.transition_applied with the from and to that did commit. Re-read the job before retrying that one. Branch on error.code; error.message is the sentence a dispatcher would have been shown, and error.details repeats it as database_message alongside the fields worth branching on.

CodeHTTPWhen you get it
job_transition_refused409The job cannot make this move. A finished job (completed, cancelled, goa) cannot be moved by PATCH at all, and a reopen of a job that is not finished is refused the same way. details.from and details.to name the pair.
job_stale409You sent expected_status and the job is in another one. Nothing was written. details.expected_status and details.current_status name both.
job_reason_required422The move needs a reason and the body did not carry a usable one. Cancelling needs cancel_reason; reopening needs reason. Both are 3 to 500 characters after trimming.
invalid_state409The job is archived. Restore it in the dashboard, then try again.
conflict409Another job on this account already carries that external_ref.

The job.reopened webhook

Subscribe to job.reopened to hear about it. It is the one event whose detail carries what the move erased, so if you store ratings or cancellation reasons on your side you can keep them. The same move also emits job.status_changed from the finished status to confirmed, so an endpoint subscribed only to status changes still sees the job come back, just without the explanation.

{
  "type": "job.reopened",
  "api_version": "2026-09-01",
  "data": {
    "job": {
      "status": "confirmed",
      "cancel_reason": null,                 // cleared by the reopen
      "rating": null,                        // cleared by the reopen
      "reopen_count": 1,
      "timestamps": {
        "completed_at": "2026-09-14T17:02:11Z",   // still the first run
        "closed_at":    "2026-09-14T17:02:11Z",
        "reopened_at":  "2026-09-14T18:41:02Z"
      }
    },
    "event": {
      "type": "job.reopened",
      "from": "completed",
      "to": "confirmed",
      "actor": "system",
      "detail": {
        "reason": "Winch line slipped, vehicle still on the shoulder",
        "reopen_count": 1,
        "cancel_reason": null,
        "rating": 5,                         // what the reopen erased
        "rating_comment": "Fast and friendly",
        "rated_at": "2026-09-14T17:20:00Z"
      }
    }
  }
}

Webhooks

Add an https endpoint under Developers (or with POST /webhooks) and pick the events you want, or * for all of them. We POST one JSON event per delivery, retry a failure six times over the next 33 hours, and pause an endpoint after 25 failures in a row. Answer 2xx within 10 seconds; do the work afterwards.

{
  "id": "0b1f…",                       // delivery id: de-duplicate on it
  "object": "event",
  "type": "job.status_changed",
  "api_version": "2026-09-01",
  "account_id": "…",
  "attempt": 1,
  "created_at": "2026-09-05T18:41:02Z",
  "data": {
    "job":   { "id": "…", "status": "en_route", "unit": { "name": "Truck 2", … }, … },
    "event": { "type": "job.status_changed", "from": "dispatched", "to": "en_route", "actor": "driver" }
  }
}

Verify the signature

Every delivery carries X-TSP-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your endpoint's secret. Reject anything older than five minutes and compare in constant time.

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

export function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (age > 300) return false;                       // replay window
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)              // timestamp + raw body
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Events

  • ping
  • job.created
  • job.status_changed
  • job.assigned
  • job.reassigned
  • job.unassigned
  • job.quote_updated
  • job.eta_updated
  • job.note_added
  • job.customer_located
  • job.photo_added
  • job.payment_updated
  • job.driver_acknowledged
  • job.customer_message
  • job.rating_received
  • job.payment_link_opened
  • job.archived
  • job.restored
  • job.signature_captured
  • job.reopened

One note on job.payment_updated: it is written both when a payment is recorded on the job itself and when an invoice raised against that job is issued, paid, voided or written off. Check event.detail.invoice_id to tell them apart. When it is absent the event carries final_price_cents and payment_method; when it is present it carries invoice_number, total_cents and amount_paid_cents, and to is the invoice status (issued, paid, void, written_off).

No code? Use Zapier, Make or n8n

Any automation tool with a "webhook trigger" and an "HTTP request" step can drive this API. Point a catch-hook at our webhooks to react to job events; call POST /jobs with your bearer key to log one. Our GoHighLevel connection under Integrations already syncs leads and can text from your own number.

Versioning and support

The API version is 2026-09-01. New fields are added without notice; nothing is removed or renamed without a new version and a long overlap. Questions: support@www.towsitepros.com.