VisitsTracker

VisitsTracker › API

API reference

Read your workspace's visits, jobs, attendance, sites and zones over HTTP, with scoped keys and signed webhooks.

Last updated 13 August 20267 sections

Base URL: your workspace's API origin. All requests and responses are JSON unless noted. Times are ISO-8601 UTC instants.

Authentication

Mint an API key in Settings → API keys & webhooks and send it as a bearer token. Keys are read-only by default; the optional jobs:write scope additionally allows creating and updating jobs — and nothing else.

curl -H "Authorization: Bearer sxk_..." https://api.visitstracker.com/visits

Reads (all keys)

EndpointReturns
GET /location/liveLatest position per tracked worker, quality-classified
GET /location/trail?user_id&from&toFix stream + total/moving distance
GET /location/nearest?lat&lngNearest workers by trusted position, with staleness
GET /visits?user_id&from&to&customer_idDerived + manual visits
GET /visits/:id/photosPhotos and signatures on a visit
GET /attendance?fromPunch sessions with site match + mock flags
GET /jobs?date&user_id&status&days&limit&offsetDispatch board. Windowed: without date it returns the last days (default 90) plus everything still ahead, limit 200 (max 500) — see the changelog
GET /zonesGeofence zones
GET /shifts?from&toPlanned rota
GET /reports/mileage?daysPer-worker daily distance (+ device odometer delta)
GET /reports/battery?daysBattery health per worker-day
GET /reports/anomalies?dateOwn-weekday anomaly findings
GET /reports/rota?daysPlanned vs actual with verdicts
GET /reports/evidence?user_id&daysWeekly evidence PDF (binary)
GET /reports/replay.svg?user_id&from&toAnimated route replay (SVG)
GET /evidence-seals · /evidence-seals/:day/verifyHash-chain seals; verify recomputes

Writes (jobs:write scope)

EndpointBody
POST /jobs {customer_id, title, due_date, assigned_user_id?, window_start?, window_end?, recurs?, checklist?}
PATCH /jobs/:id {status?, assigned_user_id?, notes?, recurs?, check_item?}

Job status path: assigned → en_route → done | cancelled. A job with an incomplete checklist refuses done; a derived visit at the job's site auto-completes it.

Webhooks

Configured in Settings. Events: attendance.punch, alert.fired (kinds: sos, mock_location, gone_dark, job_missed_window), zone.transition. Payloads are signed:

X-SenseNxt-Signature: sha256=HMAC_SHA256(secret, rawBody)
X-SenseNxt-Event: alert.fired

{"event":"alert.fired","tenant_id":"…","at":"…",
 "data":{"kind":"gone_dark","user":"Finn Field"}}

Verify by recomputing the HMAC over the exact raw body. Slack-format hooks receive {"text": "…"} instead (unsigned-friendly, still signed).

Public token surfaces

EndpointWhat
/public/share/:token(.json)Expiring live-position page (ETA); 410 when expired/revoked
/public/portal/:token(.json)Standing per-customer service record (visits, photos, signatures)

Errors

Non-2xx responses carry {"error": "machine_code", "message": "human sentence"}. Notable codes: totp_required, tenant_locked, week_approved, checklist_incomplete, read_only, rate_limited (with Retry-After).

Some refusals carry a detail object alongside the sentence, for the cases where a client has to act on the numbers rather than print them — site_has_open_jobs returns {"open_jobs": 3, "first_due": "2026-09-21"}.

Changelog

Breaking changes are announced here before they ship, and this page is the record of what already did. If something you depend on is not written down, tell us — an undocumented behaviour you rely on is our problem, not yours.

DateChange
2026-08-13 Archiving a site with open jobs is refused (409 site_has_open_jobs). Previously it succeeded and left the jobs pointing at a site that no longer appeared in any list. Pass {"archived": true, "force": true} to archive anyway — the open jobs are cancelled, with a note saying why.
2026-08-13 GET /jobs rows carry site_archived, so a job booked against a site archived earlier can be shown as such.
2026-08-13 GET /evidence-seals now returns state (verified · unchecked · broken · expired), last_verified_at and a top-level broken list. /:day/verify answers intact: null with status: "expired" for a day whose fixes have passed the workspace's retention window — previously such a day reported as broken, which read as tampering.
2026-08-13 Decisions on leave, expenses and timesheet amendments are conditional writes: a second decider racing the first now gets 409 decided / 409 already_decided instead of silently overwriting.
2026-08-13 Passwords must be 10+ characters, off the common list, and must not contain the person's name, address or workspace (400 weak_password). Existing passwords are unaffected until they are next changed. Ten failed sign-ins lock an account for fifteen minutes (423 account_locked for a caller with the right password; guessers still see 401 bad_credentials).
2026-08-11 GET /jobs became windowed: without date it returns the last days (default 90) plus everything still ahead, limit 200 (max 500). It previously returned every job ever created.