Endpoints

Every endpoint of the TempClock API v1.3.0: 31 operations on 21 resources. Parameters, fields and examples on this page are generated from the OpenAPI 3.0.3 file, and every example is a real reply that an automated test calls again to check the code still answers the same way.

Endpoint index

Each endpoint needs the scope shown (write includes read). Scopes explains what each one opens.

Keys Test a key.

MethodPathWhat it doesScope
GET/me.phpThe key making the requestany key

Workers Worker records.

MethodPathWhat it doesScope
GET/workers.phpList workers, or one workerworkers:read
POST/workers.phpCreate a workerworkers:write
PUT/workers.phpChange a workerworkers:write

Lookups Sites, departments and cost codes.

MethodPathWhat it doesScope
GET/locations.phpList sites, or one sitelocations:read
GET/departments.phpList departments, or one departmentdepartments:read
GET/cost-codes.phpList cost codes, or one cost codecost_codes:read

Time Clocks, time entries, who is on site, exceptions and client amendments.

MethodPathWhat it doesScope
GET/timesheets.phpList time entries, or onetimesheets:read
POST/clock.phpClock a worker in or outclock:write
GET/on-site-now.phpWho is on site right nowonsite:read or timesheets:read
GET/exceptions.phpExceptions (the Exceptions page checks)timesheets:read
GET/amendments.phpClient amendment requeststimesheets:read

Payroll Pay weeks, payroll lines, export batches, charges and rate rules.

MethodPathWhat it doesScope
GET/payroll.phpPayroll lines per worker per pay weekpayroll:read
GET/pay-weeks.phpIs the pay week final, and what was exported?payroll:read
GET/export-batches.phpExport batches (Sage, Xero, payroll files)payroll:read
GET/charges.phpCharge schedule (what the agency bills)charges:read
GET/rate-rules.phpRate rules (read only)pay:read or charges:read

Bookings Shifts, requirements, offers, availability and absences.

MethodPathWhat it doesScope
GET/shifts.phpList bookings, or one bookingshifts:read
POST/shifts.phpBook a worker, or cancel a bookingshifts:write
PUT/shifts.phpChange a bookingshifts:write
GET/demands.phpList requirements, or one with its bookingsshifts:read
POST/demands.phpCreate a requirementshifts:write
GET/offers.phpShift offers sent to workers (read only)shifts:read
GET/availability.phpAvailability rules, or who is free for a slotavailability:read
POST/availability.phpAdd an availability ruleavailability:write
DELETE/availability.phpRetire an availability ruleavailability:write
GET/absences.phpHoliday and sickness requestsabsences:read

Webhooks Subscriptions and deliveries.

MethodPathWhat it doesScope
GET/webhooks.phpSubscriptions, one subscription, its delivery log, or the event listwebhooks:read
POST/webhooks.phpCreate a subscription, send a test, or rotate its secretwebhooks:write
PUT/webhooks.phpChange a subscriptionwebhooks:write
DELETE/webhooks.phpDelete a subscriptionwebhooks:write

Base URL and set-up

Every path is relative to https://<your TempClock address>/api/v1: the address you sign in at, for example https://tempclock.com/api/v1/workers.php. Setup > API keys shows the exact base URL for your account. The .php form is the documented one; the same paths also answer without it. The examples below use two shell variables:

export TC_BASE="https://tempclock.com/api/v1"   # your TempClock address + /api/v1
export TC_KEY="tc_live_..."                       # from Setup > API keys

curl -s "$TC_BASE/me.php" -H "Authorization: Bearer $TC_KEY"

Read Conventions once: the reply envelope, paging, dates and times, filters and Idempotency-Key work the same way on every endpoint. Example replies come from a test copy with fake demo data; ids in them are from that copy.

Keys

GET /me.php

The key making the request

Any valid key

Any valid key may call it (no scope needed). Use it to test a key: it answers who the key belongs to, which sites it can see, what it may do, its rate limit and its expiry.

Reply: data is Me.

Errors: 401, 403, 429, 500. See Errors for the codes.

Examples

Test a key: who it belongs to, its sites, scopes, limit and expiry 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/me.php" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "api_key_id": 30,
    "key_name": "Docs examples 77E88",
    "key_prefix": "tc_live_7d1f46d1",
    "kind": "client",
    "client_id": 21,
    "client_ids": [
      21
    ],
    "client_name": "Apex Staffing Solutions",
    "site_limited": false,
    "location_ids": null,
    "locations": [
      {
        "id": 39,
        "name": "Birmingham Office",
        "active": true
      },
      {
        "id": 37,
        "name": "London HQ",
        "active": true
      },
      {
        "id": 38,
        "name": "Manchester Warehouse",
        "active": true
      }
    ],
    "scopes": [
      "workers:read",
      "workers:write",
      "timesheets:read",
      "payroll:read",
      "pay:read",
      "charges:read",
      "rates:write",
      "shifts:read",
      "shifts:write",
      "onsite:read",
      "locations:read",
      "departments:read",
      "cost_codes:read",
      "availability:read",
      "availability:write",
      "absences:read",
      "clock:write",
      "webhooks:read",
      "webhooks:write"
    ],
    "effective_scopes": [
      "absences:read",
      "availability:read",
      "availability:write",
      "charges:read",
      "clock:write",
      "cost_codes:read",
      "departments:read",
      "locations:read",
  ... (22 more lines)

Workers

GET /workers.php

List workers, or one worker

Scope: workers:read

Workers of the key's client, by name then id. Pay and charge rates are part of the record only for keys that may see them: hourly_rate needs pay:read, charge_rate needs charges:read (the fields are left out otherwise). A site-limited key sees workers based at its sites.

Query parameters

NameTypeDescription
idintegerOne worker by id (the reply's data is then an object, not a list).
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
department_idintegerThe worker's department.
statusstring: active, pendingpending = registered themselves and not approved yet.
activetrue or false (or 1, 0)true or false (also 1 or 0).
dormanttrue or false (or 1, 0)true = only dormant workers.
updated_sincestringOnly records created or changed since then. ISO 8601 with Z or an offset (2026-10-06T09:00:00Z), or local time. De-duplicate on id. The bound is compared as an exact instant with the exact time of each change (updated_at_utc). A local time inside the repeated hour when the clocks go back is read as its first pass, so no change is missed but a few can come back twice; send Z or an offset to be exact.
searchstringName, employee ID or email contains.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Worker (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

List active workers (a key with pay:read and charges:read sees both rates) 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/workers.php?active=true&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 351,
      "name": "Aisha Rahman",
      "employee_id": "AX025",
      "location_id": 39,
      "location_name": "Birmingham Office",
      "department_id": 42,
      "department_name": "Customer Service",
      "cost_code_id": null,
      "cost_code": null,
      "email": null,
      "phone": null,
      "job_role": "Customer Service Agent",
      "hourly_rate": 12.7,
      "charge_rate": 20.07,
      "max_hours_per_week": 48,
      "status": "active",
      "active": true,
      "dormant": false,
      "dormant_since": null,
      "dormant_since_utc": null,
      "start_date": "2026-03-18",
      "end_date": null,
      "leave_reason": null,
      "preferred_language": null,
      "has_face": false,
      "has_pin": true,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z",
      "updated_at": "2026-09-30 10:58:11",
      "updated_at_utc": "2026-09-30T09:58:11Z"
    },
    {
      "id": 337,
      "name": "Amira Patel",
      "employee_id": "AX011",
      "location_id": 39,
      "location_name": "Birmingham Office",
      "department_id": 42,
      "department_name": "Customer Service",
      "cost_code_id": null,
      "cost_code": null,
      "email": null,
      "phone": null,
      "job_role": "Administrator",
      "hourly_rate": 13.5,
      "charge_rate": 21.47,
      "max_hours_per_week": 48,
      "status": "active",
      "active": true,
      "dormant": false,
      "dormant_since": null,
      "dormant_since_utc": null,
      "start_date": "2025-08-02",
      "end_date": null,
      "leave_reason": null,
      "preferred_language": null,
      "has_face": false,
  ... (14 more lines)
The same list with a workers:read key: no rates 200

Key used: a key with workers:read only.

Request

curl -s "$TC_BASE/workers.php?active=true&limit=1" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 351,
      "name": "Aisha Rahman",
      "employee_id": "AX025",
      "location_id": 39,
      "location_name": "Birmingham Office",
      "department_id": 42,
      "department_name": "Customer Service",
      "cost_code_id": null,
      "cost_code": null,
      "email": null,
      "phone": null,
      "job_role": "Customer Service Agent",
      "max_hours_per_week": 48,
      "status": "active",
      "active": true,
      "dormant": false,
      "dormant_since": null,
      "dormant_since_utc": null,
      "start_date": "2026-03-18",
      "end_date": null,
      "leave_reason": null,
      "preferred_language": null,
      "has_face": false,
      "has_pin": true,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z",
      "updated_at": "2026-09-30 10:58:11",
      "updated_at_utc": "2026-09-30T09:58:11Z"
    }
  ],
  "pagination": {
    "total": 25,
    "limit": 1,
    "offset": 0,
    "has_more": true
  }
}
One worker 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/workers.php?id=327" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 327,
    "name": "James Wilson",
    "employee_id": "AX001",
    "location_id": 37,
    "location_name": "London HQ",
    "department_id": 41,
    "department_name": "Office Administration",
    "cost_code_id": null,
    "cost_code": null,
    "email": null,
    "phone": null,
    "job_role": "Team Leader",
    "hourly_rate": 16.5,
    "charge_rate": 22.94,
    "max_hours_per_week": 48,
    "status": "active",
    "active": true,
    "dormant": false,
    "dormant_since": null,
    "dormant_since_utc": null,
    "start_date": "2025-03-28",
    "end_date": null,
    "leave_reason": null,
    "preferred_language": null,
    "has_face": false,
    "has_pin": true,
    "created_at": "2026-09-30 10:58:11",
    "created_at_utc": "2026-09-30T09:58:11Z",
    "updated_at": "2026-09-30 10:58:11",
    "updated_at_utc": "2026-09-30T09:58:11Z"
  }
}
Workers changed since a time (nightly sync) 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/workers.php?updated_since=2026-01-01T00%3A00%3A00Z&limit=1" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 351,
      "name": "Aisha Rahman",
      "employee_id": "AX025",
      "location_id": 39,
      "location_name": "Birmingham Office",
      "department_id": 42,
      "department_name": "Customer Service",
      "cost_code_id": null,
      "cost_code": null,
      "email": null,
      "phone": null,
      "job_role": "Customer Service Agent",
      "hourly_rate": 12.7,
      "charge_rate": 20.07,
      "max_hours_per_week": 48,
      "status": "active",
      "active": true,
      "dormant": false,
      "dormant_since": null,
      "dormant_since_utc": null,
      "start_date": "2026-03-18",
      "end_date": null,
      "leave_reason": null,
      "preferred_language": null,
      "has_face": false,
      "has_pin": true,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z",
      "updated_at": "2026-09-30 10:58:11",
      "updated_at_utc": "2026-09-30T09:58:11Z"
    }
  ],
  "pagination": {
    "total": 25,
    "limit": 1,
    "offset": 0,
    "has_more": true
  }
}

POST /workers.php

Create a worker

Scope: workers:write Idempotency-Key accepted

The client always comes from the key. Workers created or changed with a key are written to the audit log, named "API key #N", and announced with the worker.created (and legacy worker_created) webhook event. Setting a rate needs rates:write. Accepts an Idempotency-Key header.

JSON body: WorkerCreate

POST /workers.php body. The client always comes from the key.

FieldTypeDescription
namestringRequired, 1 to 100 characters.
employee_id sometimesstring1 to 32 letters, numbers, dashes or underscores. Generated when left out. 409 duplicate_employee_id when taken.
email sometimesstring or null
phone sometimesstring or null
job_role sometimesstring or null
location_id sometimesinteger or nullHome site. Required for a site-limited key.
department_id sometimesinteger or nullYour own or a global department.
cost_code_id sometimesinteger or null
start_date sometimesdate or null
end_date sometimesdate or null
preferred_language sometimesstring: en, pl, ro, lt or null
hourly_rate sometimesnumber or nullNeeds rates:write (403 insufficient_scope otherwise). 0 to 9999.99.
charge_rate sometimesnumber or nullNeeds rates:write.

Reply: data is Worker.

Errors: 400, 401, 403, 404, 409, 422, 429, 500. See Errors for the codes.

Examples

Create a worker (Idempotency-Key makes a retry safe) 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/workers.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: docs-worker-77e88" \
  -d '{"name":"Jordan Example","employee_id":"DOCS-77E88","email":"jordan.example@example.test","phone":"07700 900123","job_role":"Warehouse Operative","location_id":37,"department_id":40,"start_date":"2026-10-02"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "id": 409,
    "name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "location_id": 37,
    "location_name": "London HQ",
    "department_id": 40,
    "department_name": "Warehouse Operations",
    "cost_code_id": null,
    "cost_code": null,
    "email": "jordan.example@example.test",
    "phone": "07700 900123",
    "job_role": "Warehouse Operative",
    "hourly_rate": null,
    "charge_rate": null,
    "max_hours_per_week": 48,
    "status": "active",
    "active": true,
    "dormant": false,
    "dormant_since": null,
    "dormant_since_utc": null,
    "start_date": "2026-10-02",
    "end_date": null,
    "leave_reason": null,
    "preferred_language": null,
    "has_face": false,
    "has_pin": false,
    "created_at": "2026-10-02 07:33:35",
    "created_at_utc": "2026-10-02T06:33:35Z",
    "updated_at": "2026-10-02 07:33:35",
    "updated_at_utc": "2026-10-02T06:33:35Z"
  },
  "message": "Worker created."
}

PUT /workers.php

Change a worker

Scope: workers:write Idempotency-Key accepted

Partial update: only the fields sent change. The same checks as create (a badge number already in use is 409 duplicate_employee_id, a name cannot be blanked, a rate must be a number from 0 to 9999.99 or null to clear it). Rate fields need rates:write. A pay or charge rate change is written to the audit log with the values before and after. Accepts an Idempotency-Key header.

Query parameters

NameTypeDescription
id requiredintegerThe worker.

JSON body: WorkerUpdate

PUT /workers.php body: any of these. null (or an empty string) clears an optional field; name and employee_id cannot be cleared. active false is the leaver event, the same as the console (face data is removed by the offboarding job). An anonymised worker cannot be changed (409).

FieldTypeDescription
name sometimesstring
employee_id sometimesstring
email sometimesstring or null
phone sometimesstring or null
job_role sometimesstring or null
location_id sometimesinteger or null
department_id sometimesinteger or null
cost_code_id sometimesinteger or null
start_date sometimesdate or null
end_date sometimesdate or null
leave_reason sometimesstring or null
preferred_language sometimesstring: en, pl, ro, lt or null
hourly_rate sometimesnumber or nullNeeds rates:write.
charge_rate sometimesnumber or nullNeeds rates:write.
active sometimesboolean

Reply: data is Worker.

Errors: 400, 401, 403, 404, 409, 422, 429, 500. See Errors for the codes.

Examples

Change a worker (only the fields sent change) 200

Key used: a key holding every scope.

Request

curl -s -X PUT "$TC_BASE/workers.php?id=409" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_role":"Forklift Driver","cost_code_id":69}'

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 409,
    "name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "location_id": 37,
    "location_name": "London HQ",
    "department_id": 40,
    "department_name": "Warehouse Operations",
    "cost_code_id": 69,
    "cost_code": "WH-OPS",
    "email": "jordan.example@example.test",
    "phone": "07700 900123",
    "job_role": "Forklift Driver",
    "hourly_rate": null,
    "charge_rate": null,
    "max_hours_per_week": 48,
    "status": "active",
    "active": true,
    "dormant": false,
    "dormant_since": null,
    "dormant_since_utc": null,
    "start_date": "2026-10-02",
    "end_date": null,
    "leave_reason": null,
    "preferred_language": null,
    "has_face": false,
    "has_pin": false,
    "created_at": "2026-10-02 07:33:35",
    "created_at_utc": "2026-10-02T06:33:35Z",
    "updated_at": "2026-10-02 07:33:35",
    "updated_at_utc": "2026-10-02T06:33:35Z"
  },
  "message": "Worker updated."
}

Lookups

GET /locations.php

List sites, or one site

Scope: locations:read

Sites of the key's client, by name then id (a site-limited key sees only its sites). Billing fields need charges:read.

Query parameters

NameTypeDescription
idintegerOne site by id (the reply's data is then an object, not a list).
activetrue or false (or 1, 0)true or false (also 1 or 0).
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Location (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

List sites 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/locations.php?limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 39,
      "name": "Birmingham Office",
      "address": "8 Colmore Row",
      "city": "Birmingham",
      "postcode": "B3 2QD",
      "capacity": 50,
      "operates_24x7": false,
      "active": true,
      "latitude": 52.4814,
      "longitude": -1.8998,
      "geofence_radius": 200,
      "geofence_mode": "disabled",
      "kiosk_language": "en",
      "gps_accuracy_threshold": 100,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z",
      "billing_name": null,
      "billing_contact": null,
      "billing_email": null,
      "po_reference": null,
      "sage_account_ref": null,
      "sage_department": null,
      "nominal_code": null
    },
    {
      "id": 37,
      "name": "London HQ",
      "address": "45 Canary Wharf",
      "city": "London",
      "postcode": "E14 5AB",
      "capacity": 120,
      "operates_24x7": false,
      "active": true,
      "latitude": 51.5054,
      "longitude": -0.0235,
      "geofence_radius": 200,
      "geofence_mode": "disabled",
      "kiosk_language": "en",
      "gps_accuracy_threshold": 100,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z",
      "billing_name": null,
      "billing_contact": null,
      "billing_email": null,
      "po_reference": null,
      "sage_account_ref": null,
      "sage_department": null,
      "nominal_code": null
    }
  ],
  "pagination": {
    "total": 3,
    "limit": 2,
    "offset": 0,
    "has_more": true
  }
}
One site (billing fields need charges:read) 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/locations.php?id=37" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 37,
    "name": "London HQ",
    "address": "45 Canary Wharf",
    "city": "London",
    "postcode": "E14 5AB",
    "capacity": 120,
    "operates_24x7": false,
    "active": true,
    "latitude": 51.5054,
    "longitude": -0.0235,
    "geofence_radius": 200,
    "geofence_mode": "disabled",
    "kiosk_language": "en",
    "gps_accuracy_threshold": 100,
    "created_at": "2026-09-30 10:58:11",
    "created_at_utc": "2026-09-30T09:58:11Z",
    "billing_name": null,
    "billing_contact": null,
    "billing_email": null,
    "po_reference": null,
    "sage_account_ref": null,
    "sage_department": null,
    "nominal_code": null
  }
}

GET /departments.php

List departments, or one department

Scope: departments:read

The client's own departments plus global ones (shared by every client), the same choice the consoles offer for a worker. A site-limited key sees the departments of its sites and the ones with no site.

Query parameters

NameTypeDescription
idintegerOne department by id (the reply's data is then an object, not a list).
activetrue or false (or 1, 0)true or false (also 1 or 0).
location_idintegerDepartments of one site.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Department (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

List departments, including global ones 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/departments.php?limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 42,
      "name": "Customer Service",
      "description": null,
      "location_id": 39,
      "location_name": "Birmingham Office",
      "global": false,
      "active": true,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z"
    },
    {
      "id": 43,
      "name": "Logistics & Transport",
      "description": null,
      "location_id": 38,
      "location_name": "Manchester Warehouse",
      "global": false,
      "active": true,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z"
    }
  ],
  "pagination": {
    "total": 4,
    "limit": 2,
    "offset": 0,
    "has_more": true
  }
}
One department 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/departments.php?id=40" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 40,
    "name": "Warehouse Operations",
    "description": null,
    "location_id": 38,
    "location_name": "Manchester Warehouse",
    "global": false,
    "active": true,
    "created_at": "2026-09-30 10:58:11",
    "created_at_utc": "2026-09-30T09:58:11Z"
  }
}

GET /cost-codes.php

List cost codes, or one cost code

Scope: cost_codes:read

The client's own cost codes plus global ones, by code then id.

Query parameters

NameTypeDescription
idintegerOne cost code by id (the reply's data is then an object, not a list).
activetrue or false (or 1, 0)true or false (also 1 or 0).
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is CostCode (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

List cost codes 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/cost-codes.php?limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 70,
      "code": "ADMIN",
      "description": "Office Admin",
      "global": false,
      "active": true,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z"
    },
    {
      "id": 71,
      "code": "CS-TEAM",
      "description": "Customer Service",
      "global": false,
      "active": true,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z"
    }
  ],
  "pagination": {
    "total": 6,
    "limit": 2,
    "offset": 0,
    "has_more": true
  }
}
One cost code 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/cost-codes.php?id=69" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 69,
    "code": "WH-OPS",
    "description": "Warehouse Operations",
    "global": false,
    "active": true,
    "created_at": "2026-09-30 10:58:11",
    "created_at_utc": "2026-09-30T09:58:11Z"
  }
}

Time

GET /timesheets.php

List time entries, or one

Scope: timesheets:read

Which entries a key sees: those owned by its client, where a shift belongs to the client that owns the site worked, else the worker's home client (the payroll and Sage rule). A pooled worker's shift at this client's site is here; this client's worker at another client's site is not. A site-limited key sees its sites only. Every completed shift carries its approval state, its paid minutes from the pricing engine and its payroll status from the same gate as the Sage 50 Payroll export. Newest clock-in first.

Query parameters

NameTypeDescription
idintegerOne time entry by id (the reply's data is then an object, not a list).
date_fromdateOn the clock-in date, YYYY-MM-DD.
date_todateOn the clock-in date, YYYY-MM-DD.
updated_sincestringOnly records created or changed since then. ISO 8601 with Z or an offset (2026-10-06T09:00:00Z), or local time. De-duplicate on id: on the night the clocks go back a row can come back twice.
worker_idintegerOne worker.
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
department_idintegerThe worker's department.
entry_filterstring: shifts, active, breaks, allshifts (default) = completed shifts; active = open shifts clocked in within 18 hours (on site now); breaks; all.
approval_statusstring: pending, approved, disputedAgency approval.
client_approvalstring: pending, approved, rejectedClient sign-off.
payroll_statusstring: payable, on_hold, excludedCompleted shifts only, at most 2,000 matching shifts per request (narrow with dates).
include_pricingtrue or false (or 1, 0)Add the pricing block: pay figures need pay:read, charge figures charges:read (403 insufficient_scope with neither).
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is TimeEntry (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Completed shifts on one day 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/timesheets.php?date_from=2026-09-25&date_to=2026-09-25&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 36711,
      "worker_id": 343,
      "worker_name": "Zara Hussain",
      "employee_id": "AX017",
      "department_id": 42,
      "location_id": 39,
      "location_name": "Birmingham Office",
      "client_id": 21,
      "entry_type": "shift",
      "clock_in": "2026-09-25 09:21:00",
      "clock_out": "2026-09-25 17:12:00",
      "clock_in_utc": "2026-09-25T08:21:00Z",
      "clock_out_utc": "2026-09-25T16:12:00Z",
      "total_minutes": 471,
      "hours": 7.85,
      "paid_minutes": 399,
      "paid_hours": 6.65,
      "unpaid_break_minutes": 60,
      "clock_method": "face",
      "clock_out_method": null,
      "clock_source": null,
      "kiosk_id": null,
      "notes": null,
      "match_score": 0.24,
      "cost_code_id": 73,
      "scheduled_shift_id": null,
      "approval_status": "pending",
      "approved": false,
      "approved_at": null,
      "approved_by": null,
      "client_approval": "pending",
      "client_approved": null,
      "client_approved_at": null,
      "client_approved_by": null,
      "client_rejected_reason": null,
      "queried": false,
      "open_query": false,
      "payroll_status": "on_hold",
      "payroll_excluded_reason": "pending",
      "locked": true,
      "exported_batch_id": null,
      "risk_flags": [],
      "manual_break_minutes": null,
      "pay_adjustment_minutes": null,
      "edited": false,
      "edit_count": 0,
      "edited_at": null,
      "original_clock_in": null,
      "original_clock_out": null,
      "within_geofence": 1,
      "distance_meters": null,
      "gps_accuracy": 25,
      "low_gps_confidence": false,
      "clock_out_within_geofence": null,
      "clock_out_distance_meters": null,
      "created_at": "2026-09-30 10:58:11",
      "updated_at": "2026-09-30 10:58:11",
  ... (70 more lines)
One shift with its pricing block 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/timesheets.php?id=36686&include_pricing=true" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 36686,
    "worker_id": 327,
    "worker_name": "James Wilson",
    "employee_id": "AX001",
    "department_id": 41,
    "location_id": 37,
    "location_name": "London HQ",
    "client_id": 21,
    "entry_type": "shift",
    "clock_in": "2026-09-25 06:01:00",
    "clock_out": "2026-09-25 14:00:00",
    "clock_in_utc": "2026-09-25T05:01:00Z",
    "clock_out_utc": "2026-09-25T13:00:00Z",
    "total_minutes": 479,
    "hours": 7.98,
    "paid_minutes": 449,
    "paid_hours": 7.48,
    "unpaid_break_minutes": 30,
    "clock_method": "face",
    "clock_out_method": null,
    "clock_source": null,
    "kiosk_id": null,
    "notes": null,
    "match_score": 0.15,
    "cost_code_id": 69,
    "scheduled_shift_id": null,
    "approval_status": "approved",
    "approved": true,
    "approved_at": null,
    "approved_by": null,
    "client_approval": "pending",
    "client_approved": null,
    "client_approved_at": null,
    "client_approved_by": null,
    "client_rejected_reason": null,
    "queried": false,
    "open_query": false,
    "payroll_status": "payable",
    "payroll_excluded_reason": null,
    "locked": true,
    "exported_batch_id": 8,
    "risk_flags": [],
    "manual_break_minutes": null,
    "pay_adjustment_minutes": null,
    "edited": false,
    "edit_count": 0,
    "edited_at": null,
    "original_clock_in": null,
    "original_clock_out": null,
    "within_geofence": 1,
    "distance_meters": null,
    "gps_accuracy": 12,
    "low_gps_confidence": false,
    "clock_out_within_geofence": null,
    "clock_out_distance_meters": null,
    "created_at": "2026-09-30 10:58:11",
    "updated_at": "2026-10-02 07:33:35",
    "updated_at_utc": "2026-10-02T06:33:35Z",
  ... (41 more lines)
Shifts created or changed in the last hour (sync feed) 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/timesheets.php?updated_since=2026-10-02T05%3A33%3A35Z&entry_filter=all&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 40786,
      "worker_id": 409,
      "worker_name": "Jordan Example",
      "employee_id": "DOCS-77E88",
      "department_id": 40,
      "location_id": 39,
      "location_name": "Birmingham Office",
      "client_id": 21,
      "entry_type": "shift",
      "clock_in": "2026-10-02 07:33:36",
      "clock_out": "2026-10-02 07:33:36",
      "clock_in_utc": "2026-10-02T06:33:36Z",
      "clock_out_utc": "2026-10-02T06:33:36Z",
      "total_minutes": 0,
      "hours": 0,
      "paid_minutes": 0,
      "paid_hours": 0,
      "unpaid_break_minutes": null,
      "clock_method": "api",
      "clock_out_method": "api",
      "clock_source": "api",
      "kiosk_id": null,
      "notes": null,
      "match_score": null,
      "cost_code_id": 69,
      "scheduled_shift_id": null,
      "approval_status": "pending",
      "approved": false,
      "approved_at": null,
      "approved_by": null,
      "client_approval": "pending",
      "client_approved": null,
      "client_approved_at": null,
      "client_approved_by": null,
      "client_rejected_reason": null,
      "queried": false,
      "open_query": false,
      "payroll_status": "on_hold",
      "payroll_excluded_reason": "pending",
      "locked": false,
      "exported_batch_id": null,
      "risk_flags": [
        "geofence_outside",
        "low_gps"
      ],
      "manual_break_minutes": null,
      "pay_adjustment_minutes": null,
      "edited": false,
      "edit_count": 0,
      "edited_at": null,
      "original_clock_in": null,
      "original_clock_out": null,
      "within_geofence": 0,
      "distance_meters": 5004,
      "gps_accuracy": 150,
      "low_gps_confidence": true,
      "clock_out_within_geofence": 1,
  ... (75 more lines)
Shifts payroll is waiting on, with the reason 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/timesheets.php?date_from=2026-09-19&date_to=2026-09-25&payroll_status=on_hold&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 36711,
      "worker_id": 343,
      "worker_name": "Zara Hussain",
      "employee_id": "AX017",
      "department_id": 42,
      "location_id": 39,
      "location_name": "Birmingham Office",
      "client_id": 21,
      "entry_type": "shift",
      "clock_in": "2026-09-25 09:21:00",
      "clock_out": "2026-09-25 17:12:00",
      "clock_in_utc": "2026-09-25T08:21:00Z",
      "clock_out_utc": "2026-09-25T16:12:00Z",
      "total_minutes": 471,
      "hours": 7.85,
      "paid_minutes": 399,
      "paid_hours": 6.65,
      "unpaid_break_minutes": 60,
      "clock_method": "face",
      "clock_out_method": null,
      "clock_source": null,
      "kiosk_id": null,
      "notes": null,
      "match_score": 0.24,
      "cost_code_id": 73,
      "scheduled_shift_id": null,
      "approval_status": "pending",
      "approved": false,
      "approved_at": null,
      "approved_by": null,
      "client_approval": "pending",
      "client_approved": null,
      "client_approved_at": null,
      "client_approved_by": null,
      "client_rejected_reason": null,
      "queried": false,
      "open_query": false,
      "payroll_status": "on_hold",
      "payroll_excluded_reason": "pending",
      "locked": true,
      "exported_batch_id": null,
      "risk_flags": [],
      "manual_break_minutes": null,
      "pay_adjustment_minutes": null,
      "edited": false,
      "edit_count": 0,
      "edited_at": null,
      "original_clock_in": null,
      "original_clock_out": null,
      "within_geofence": 1,
      "distance_meters": null,
      "gps_accuracy": 25,
      "low_gps_confidence": false,
      "clock_out_within_geofence": null,
      "clock_out_distance_meters": null,
      "created_at": "2026-09-30 10:58:11",
      "updated_at": "2026-09-30 10:58:11",
  ... (70 more lines)
Open shifts (on site now) as time entries 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/timesheets.php?entry_filter=active&location_id=37&limit=1" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 40785,
      "worker_id": 409,
      "worker_name": "Jordan Example",
      "employee_id": "DOCS-77E88",
      "department_id": 40,
      "location_id": 37,
      "location_name": "London HQ",
      "client_id": 21,
      "entry_type": "shift",
      "clock_in": "2026-10-02 07:19:35",
      "clock_out": null,
      "clock_in_utc": "2026-10-02T06:19:35Z",
      "clock_out_utc": null,
      "total_minutes": null,
      "hours": null,
      "paid_minutes": null,
      "paid_hours": null,
      "unpaid_break_minutes": null,
      "clock_method": "api",
      "clock_out_method": null,
      "clock_source": "api",
      "kiosk_id": null,
      "notes": null,
      "match_score": null,
      "cost_code_id": 69,
      "scheduled_shift_id": null,
      "approval_status": "pending",
      "approved": false,
      "approved_at": null,
      "approved_by": null,
      "client_approval": "pending",
      "client_approved": null,
      "client_approved_at": null,
      "client_approved_by": null,
      "client_rejected_reason": null,
      "queried": false,
      "open_query": false,
      "payroll_status": "on_hold",
      "payroll_excluded_reason": "open_clock_out",
      "locked": false,
      "exported_batch_id": null,
      "risk_flags": [
        "api_backdated"
      ],
      "manual_break_minutes": null,
      "pay_adjustment_minutes": null,
      "edited": false,
      "edit_count": 0,
      "edited_at": null,
      "original_clock_in": null,
      "original_clock_out": null,
      "within_geofence": null,
      "distance_meters": null,
      "gps_accuracy": null,
      "low_gps_confidence": false,
      "clock_out_within_geofence": null,
      "clock_out_distance_meters": null,
  ... (12 more lines)
The shift just clocked, flagged api_backdated (occurred_at was in the past) 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/timesheets.php?id=40785" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 40785,
    "worker_id": 409,
    "worker_name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "department_id": 40,
    "location_id": 37,
    "location_name": "London HQ",
    "client_id": 21,
    "entry_type": "shift",
    "clock_in": "2026-10-02 07:19:35",
    "clock_out": "2026-10-02 07:33:36",
    "clock_in_utc": "2026-10-02T06:19:35Z",
    "clock_out_utc": "2026-10-02T06:33:36Z",
    "total_minutes": 14,
    "hours": 0.23,
    "paid_minutes": 0,
    "paid_hours": 0,
    "unpaid_break_minutes": 0,
    "clock_method": "api",
    "clock_out_method": "api",
    "clock_source": "api",
    "kiosk_id": null,
    "notes": "Left by the main gate turnstile",
    "match_score": null,
    "cost_code_id": 69,
    "scheduled_shift_id": null,
    "approval_status": "pending",
    "approved": false,
    "approved_at": null,
    "approved_by": null,
    "client_approval": "pending",
    "client_approved": null,
    "client_approved_at": null,
    "client_approved_by": null,
    "client_rejected_reason": null,
    "queried": false,
    "open_query": false,
    "payroll_status": "on_hold",
    "payroll_excluded_reason": "pending",
    "locked": false,
    "exported_batch_id": null,
    "risk_flags": [
      "api_backdated"
    ],
    "manual_break_minutes": null,
    "pay_adjustment_minutes": null,
    "edited": false,
    "edit_count": 0,
    "edited_at": null,
    "original_clock_in": null,
    "original_clock_out": null,
    "within_geofence": null,
    "distance_meters": null,
    "gps_accuracy": null,
    "low_gps_confidence": false,
    "clock_out_within_geofence": null,
    "clock_out_distance_meters": null,
    "created_at": "2026-10-02 07:33:35",
  ... (4 more lines)

POST /clock.php

Clock a worker in or out

Scope: clock:write Idempotency-Key accepted

The same rules as the kiosk: an open shift older than 18 hours is ignored (it is a missing clock-out, not the shift being closed now); a pending worker is refused (409 worker_pending), an inactive one too (409 worker_inactive); a dormant worker is accepted, flagged dormant_worker and set back to active; a clock in a locked pay week is kept, flagged locked_week and listed for payroll as an adjustment. The worker's clock state is checked first: a clock-out with no open clock-in (in the last 18 hours) is 409 no_open_clock_in, and a clock-in while clocked in is 409 already_clocked_in, before any booking, site, survey or geofence check. So neither is recorded as a refusal, and neither sends clock.geofence_refused or clock.blocked_by_survey. already_clocked_in names the open shift in entry_id, except to a site-limited key when that shift is at a site the key cannot see. The clock and the open-shift check run in one transaction with the worker row locked, so two requests at once can never open two shifts. occurred_at lets a turnstile or another terminal send the real time (up to 15 minutes back; more than 60 seconds back flags the entry api_backdated for review). Webhooks (clock_in, clock_out, clock.*) are queued for the client that owns the site, with the kiosk clock fields plus client_id, source api, clock_in_utc and clock_out_utc and risk_flags. clock.geofence_refused carries data.action (in or out), as it does from the kiosk and the worker app. Accepts an Idempotency-Key header: a retry with the same key and body returns the first answer.

JSON body: ClockRequest

POST /clock.php body.

FieldTypeDescription
worker_id sometimesintegerThe worker, or
employee_id sometimesstringtheir employee ID.
actionstring: in, out
location_id sometimesintegerClock-in: default the booked shift's site, else the worker's home site. Clock-out always uses the open shift's site.
scheduled_shift_id sometimesintegerClock-in: the booking this clock is for. Default: the booking that covers the clock time.
occurred_at sometimesstringWhen it happened (ISO 8601 with Z or an offset, or local time). At most 15 minutes in the past and 60 seconds in the future. More than 60 seconds in the past flags the entry api_backdated.
notes sometimesstringAt most 255 characters. A clock-out note is added after the clock-in note.
latitude sometimesnumber
longitude sometimesnumberBoth needed at a strict-geofence site (403 gps_required). A strict site with no map pin refuses every clock (403 site_not_mapped).
gps_accuracy sometimesnumberMetres.
survey_id sometimesinteger
survey_answers sometimeslist of SurveyAnswerA blocking answer is 403 blocked_by_survey.

Reply: data is ClockResult.

Errors: 400, 401, 403, 404, 409, 422, 429, 500. See Errors for the codes.

Examples

Clock in by employee ID, with the time it really happened 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/clock.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: docs-clock-77e88" \
  -d '{"employee_id":"DOCS-77E88","action":"in","location_id":37,"occurred_at":"2026-10-02T06:19:35Z"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "entry_id": 40785,
    "worker_id": 409,
    "employee_id": "DOCS-77E88",
    "worker_name": "Jordan Example",
    "action": "clock_in",
    "clock_in": "2026-10-02 07:19:35",
    "clock_in_utc": "2026-10-02T06:19:35Z",
    "location_id": 37,
    "scheduled_shift_id": null,
    "risk_flags": [
      "api_backdated"
    ],
    "backdated": true
  },
  "message": "Jordan Example clocked in at 07:19"
}
The same request sent again with the same Idempotency-Key: the first answer, replayed 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/clock.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: docs-clock-77e88" \
  -d '{"employee_id":"DOCS-77E88","action":"in","location_id":37,"occurred_at":"2026-10-02T06:19:35Z"}'

Real reply (local test copy, fake demo data)

HTTP 201
Idempotent-Replayed: true

{
  "data": {
    "entry_id": 40785,
    "worker_id": 409,
    "employee_id": "DOCS-77E88",
    "worker_name": "Jordan Example",
    "action": "clock_in",
    "clock_in": "2026-10-02 07:19:35",
    "clock_in_utc": "2026-10-02T06:19:35Z",
    "location_id": 37,
    "scheduled_shift_id": null,
    "risk_flags": [
      "api_backdated"
    ],
    "backdated": true
  },
  "message": "Jordan Example clocked in at 07:19"
}
Clock out (the note is added after the clock-in note) 200

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/clock.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"action":"out","notes":"Left by the main gate turnstile"}'

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "entry_id": 40785,
    "worker_id": 409,
    "employee_id": "DOCS-77E88",
    "worker_name": "Jordan Example",
    "action": "clock_out",
    "clock_in": "2026-10-02 07:19:35",
    "clock_out": "2026-10-02 07:33:36",
    "clock_in_utc": "2026-10-02T06:19:35Z",
    "clock_out_utc": "2026-10-02T06:33:36Z",
    "total_minutes": 14,
    "hours": 0.23,
    "location_id": 37,
    "scheduled_shift_id": null,
    "risk_flags": [
      "api_backdated"
    ],
    "backdated": false
  },
  "message": "Jordan Example clocked out. Duration: 0h 14m"
}
Clock in from outside a warning-mode geofence, with a poor GPS reading: accepted and flagged 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/clock.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"action":"in","location_id":39,"latitude":52.5264,"longitude":-1.8998,"gps_accuracy":150}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "entry_id": 40786,
    "worker_id": 409,
    "employee_id": "DOCS-77E88",
    "worker_name": "Jordan Example",
    "action": "clock_in",
    "clock_in": "2026-10-02 07:33:36",
    "clock_in_utc": "2026-10-02T06:33:36Z",
    "location_id": 39,
    "scheduled_shift_id": null,
    "risk_flags": [
      "geofence_outside",
      "low_gps"
    ],
    "backdated": false,
    "within_geofence": 0,
    "distance_meters": 5004,
    "low_gps_confidence": true,
    "warning": "Clocked in 5004m from Birmingham Office, recorded."
  },
  "message": "Jordan Example clocked in at 07:33 (Clocked in 5004m from Birmingham Office, recorded.)"
}
Clock out with coordinates 200

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/clock.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"action":"out","latitude":52.4814,"longitude":-1.8998,"gps_accuracy":12}'

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "entry_id": 40786,
    "worker_id": 409,
    "employee_id": "DOCS-77E88",
    "worker_name": "Jordan Example",
    "action": "clock_out",
    "clock_in": "2026-10-02 07:33:36",
    "clock_out": "2026-10-02 07:33:36",
    "clock_in_utc": "2026-10-02T06:33:36Z",
    "clock_out_utc": "2026-10-02T06:33:36Z",
    "total_minutes": 0,
    "hours": 0,
    "location_id": 39,
    "scheduled_shift_id": null,
    "risk_flags": [
      "geofence_outside",
      "low_gps"
    ],
    "backdated": false,
    "within_geofence": 1,
    "distance_meters": 0
  },
  "message": "Jordan Example clocked out. Duration: 0h 0m"
}

GET /on-site-now.php

Who is on site right now

Scope: onsite:read or timesheets:read

Workers clocked in now, by site: an open shift clocked in within the last 18 hours (the same rule as the kiosk, Live View and fire registers). Everyone at this client's sites, by the site's client. No pay data. Needs onsite:read; timesheets:read is also accepted.

Query parameters

NameTypeDescription
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
include_workerstrue or false (or 1, 0)false = totals only (default true).

Reply: data is OnSiteNow.

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Who is on site now, by site 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/on-site-now.php?location_id=37" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "as_of": "2026-10-02T06:33:35Z",
    "totals": {
      "clocked_in": 1,
      "on_site": 1,
      "off_site": 0,
      "geofence_unknown": 1,
      "low_gps_confidence": 0
    },
    "locations": [
      {
        "location_id": 37,
        "location_name": "London HQ",
        "clocked_in": 1,
        "on_site": 1,
        "off_site": 0,
        "geofence_unknown": 1,
        "workers": [
          {
            "worker_id": 409,
            "name": "Jordan Example",
            "employee_id": "DOCS-77E88",
            "entry_id": 40785,
            "clock_in": "2026-10-02 07:19:35",
            "clock_in_utc": "2026-10-02T06:19:35Z",
            "minutes_on_shift": 14,
            "within_geofence": null,
            "distance_meters": null,
            "gps_accuracy": null,
            "low_gps_confidence": false
          }
        ]
      }
    ],
    "unassigned": []
  }
}
Totals only, with an onsite:read key (a site board) 200

Key used: a key with onsite:read only.

Request

curl -s "$TC_BASE/on-site-now.php?include_workers=false" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "as_of": "2026-10-02T06:33:35Z",
    "totals": {
      "clocked_in": 1,
      "on_site": 1,
      "off_site": 0,
      "geofence_unknown": 1,
      "low_gps_confidence": 0
    },
    "locations": [
      {
        "location_id": 37,
        "location_name": "London HQ",
        "clocked_in": 1,
        "on_site": 1,
        "off_site": 0,
        "geofence_unknown": 1
      }
    ],
    "unassigned": []
  }
}

GET /exceptions.php

Exceptions (the Exceptions page checks)

Scope: timesheets:read

Late starts, no-shows, early finishes, missing clock-outs, long shifts and weekly overtime, missed breaks, disputes, shifts not signed off, flagged clocks, clocks into locked weeks and offline conflicts, newest first. meta.counts gives the open count per category.

Query parameters

NameTypeDescription
fromdateDefault 7 days ago.
todateDefault today; at most 93 days after from.
typestringOne category or one type.
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
worker_idintegerOne worker.
include_resolvedtrue or false (or 1, 0)Also the ones dismissed or settled (default false).
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Exception (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Exceptions in a week 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/exceptions.php?from=2026-09-19&to=2026-09-25&limit=3" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "category": "no_show",
      "type": "no_show",
      "entity_type": "scheduled_shift",
      "entity_id": 27263,
      "period_key": null,
      "worker_id": 330,
      "worker_name": "Sophie Brown",
      "employee_id": "AX004",
      "location_name": "Birmingham Office",
      "date": "2026-09-25",
      "booked": "14:00-22:00",
      "entry_id": null,
      "shift_id": 27263,
      "clock_in": null,
      "clock_out": null,
      "clock_in_utc": null,
      "clock_out_utc": null,
      "minutes": null,
      "detail": "No clock-in for the booked shift",
      "label": "No show",
      "resolution": null
    },
    {
      "category": "no_show",
      "type": "no_show",
      "entity_type": "scheduled_shift",
      "entity_id": 27271,
      "period_key": null,
      "worker_id": 339,
      "worker_name": "Charlotte Evans",
      "employee_id": "AX013",
      "location_name": "Birmingham Office",
      "date": "2026-09-25",
      "booked": "14:00-22:00",
      "entry_id": null,
      "shift_id": 27271,
      "clock_in": null,
      "clock_out": null,
      "clock_in_utc": null,
      "clock_out_utc": null,
      "minutes": null,
      "detail": "No clock-in for the booked shift",
      "label": "No show",
      "resolution": null
    },
    {
      "category": "no_show",
      "type": "no_show",
      "entity_type": "scheduled_shift",
      "entity_id": 27274,
      "period_key": null,
      "worker_id": 342,
      "worker_name": "Hannah Williams",
      "employee_id": "AX016",
      "location_name": "Birmingham Office",
      "date": "2026-09-25",
      "booked": "14:00-22:00",
  ... (36 more lines)

GET /amendments.php

Client amendment requests

Scope: timesheets:read

Changes a client asked for on a shift from its portal (corrected clock-in, clock-out or break), and what the agency decided. Accepting or declining is done in TempClock and announced by the timesheet.amended webhook. Newest first.

Query parameters

NameTypeDescription
idintegerOne request by id (the reply's data is then an object, not a list).
statusstring: pending, accepted, declined, superseded, withdrawnDefault: all.
entry_idintegerOne time entry.
worker_idintegerOne worker.
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
fromdateOn the date the request was made.
todateOn the date the request was made.
updated_sincestringOnly records created or changed since then. ISO 8601 with Z or an offset (2026-10-06T09:00:00Z), or local time. De-duplicate on id: on the night the clocks go back a row can come back twice.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Amendment (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Client amendment requests waiting for a decision 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/amendments.php?status=pending&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 11,
      "entry_id": 36686,
      "worker_id": 327,
      "worker_name": "James Wilson",
      "employee_id": "AX001",
      "location_id": 37,
      "location_name": "London HQ",
      "status": "pending",
      "reason": "Left 30 minutes early, agreed with the shift supervisor",
      "proposed": {
        "clock_in": null,
        "clock_out": "2026-09-25 13:30:00",
        "break_minutes": null,
        "clock_in_utc": null,
        "clock_out_utc": "2026-09-25T12:30:00Z"
      },
      "current": {
        "clock_in": "2026-09-25 06:01:00",
        "clock_out": "2026-09-25 14:00:00",
        "break_minutes": null,
        "clock_in_utc": "2026-09-25T05:01:00Z",
        "clock_out_utc": "2026-09-25T13:00:00Z"
      },
      "changes": {
        "clock_out": "2026-09-25 13:30:00"
      },
      "proposed_by": "portal",
      "created_at": "2026-10-02 07:33:35",
      "created_at_utc": "2026-10-02T06:33:35Z",
      "resolved_at": null,
      "resolved_at_utc": null,
      "resolved_by": null,
      "resolution_note": null
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 2,
    "offset": 0,
    "has_more": false
  }
}
One amendment request 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/amendments.php?id=11" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 11,
    "entry_id": 36686,
    "worker_id": 327,
    "worker_name": "James Wilson",
    "employee_id": "AX001",
    "location_id": 37,
    "location_name": "London HQ",
    "status": "pending",
    "reason": "Left 30 minutes early, agreed with the shift supervisor",
    "proposed": {
      "clock_in": null,
      "clock_out": "2026-09-25 13:30:00",
      "break_minutes": null,
      "clock_in_utc": null,
      "clock_out_utc": "2026-09-25T12:30:00Z"
    },
    "current": {
      "clock_in": "2026-09-25 06:01:00",
      "clock_out": "2026-09-25 14:00:00",
      "break_minutes": null,
      "clock_in_utc": "2026-09-25T05:01:00Z",
      "clock_out_utc": "2026-09-25T13:00:00Z"
    },
    "changes": {
      "clock_out": "2026-09-25 13:30:00"
    },
    "proposed_by": "portal",
    "created_at": "2026-10-02 07:33:35",
    "created_at_utc": "2026-10-02T06:33:35Z",
    "resolved_at": null,
    "resolved_at_utc": null,
    "resolved_by": null,
    "resolution_note": null
  }
}

Payroll

GET /payroll.php

Payroll lines per worker per pay week

Scope: payroll:read

Approved, priced hours. Which shifts count: exactly the shifts the Sage 50 Payroll export pays for the same week, the same totals: closed, approved by the agency, not rejected by the client, not disputed and with no open query, signed off by the client when the client requires it, with a pay rate, and not already paid as a late adjustment in a later week (so nothing is paid twice). A shift belongs to the client that owns the site it was worked at, else the worker's home client, so a pooled worker at this client's site counts here. The pay week of a shift is the pay week of its clock-in date. Each line has the five legacy buckets in elements and the Sage 50 file's lines in pay_elements. Money comes from the one pricing engine; a closed and locked week returns its frozen figures (source locked). Charge figures need charges:read.

Query parameters

NameTypeDescription
weekdateAny date inside the pay week wanted.
date_fromdateWith date_to: a range, widened to whole pay weeks (at most 26).
date_todateSee date_from.
worker_idintegerOne worker.
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
department_idintegerThe worker's department.
include_entriestrue or false (or 1, 0)1 = list the time entry ids behind each line and pay element.
adjustmentstrue or false (or 1, 0)1 = add adjustments[]: clocks that landed in a locked week, paid as an adjustment in a later week.
limitintegerLines per page, 1 to 200 (default 50).
offsetintegerLines to skip.

Reply: data is PayrollLine (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Payroll lines for one pay week (payroll:read key: pay figures, no charges) 200

Key used: a key with payroll:read only (pay figures, no charges).

Request

curl -s "$TC_BASE/payroll.php?week=2026-09-25&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "worker_id": 351,
      "employee_id": "AX025",
      "worker_name": "Aisha Rahman",
      "pay_week_start": "2026-09-21",
      "pay_week_end": "2026-09-27",
      "shifts": 1,
      "paid_minutes": 168,
      "paid_hours": 2.8,
      "gross_pay": 37.8,
      "base_pay_rate": 13.5,
      "department": "WH-OPS",
      "cost_centre": "WH-OPS",
      "elements": {
        "regular": {
          "minutes": 168,
          "hours": 2.8,
          "pay": 37.8,
          "pay_rate": 13.5
        },
        "overtime": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null
        },
        "weekend": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null
        },
        "bank_holiday": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null
        },
        "night": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null
        }
      },
      "pay_elements": [
        {
          "element": "regular",
          "label": "Basic",
          "sage_ref": "Basic",
          "minutes": 168,
          "hours": 2.8,
          "pay_rate": 13.5,
          "pay": 37.8,
          "department": "WH-OPS",
          "cost_centre": "WH-OPS"
        }
      ],
  ... (102 more lines)
With entry ids and late adjustments, charges included 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/payroll.php?week=2026-09-25&include_entries=1&adjustments=1&limit=1" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "worker_id": 351,
      "employee_id": "AX025",
      "worker_name": "Aisha Rahman",
      "pay_week_start": "2026-09-21",
      "pay_week_end": "2026-09-27",
      "shifts": 1,
      "paid_minutes": 168,
      "paid_hours": 2.8,
      "gross_pay": 37.8,
      "charge_total": 52.92,
      "margin": 15.12,
      "base_pay_rate": 13.5,
      "department": "WH-OPS",
      "cost_centre": "WH-OPS",
      "elements": {
        "regular": {
          "minutes": 168,
          "hours": 2.8,
          "pay": 37.8,
          "pay_rate": 13.5,
          "charge": 52.92,
          "charge_rate": 18.9
        },
        "overtime": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null,
          "charge": 0,
          "charge_rate": null
        },
        "weekend": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null,
          "charge": 0,
          "charge_rate": null
        },
        "bank_holiday": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null,
          "charge": 0,
          "charge_rate": null
        },
        "night": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null,
          "charge": 0,
          "charge_rate": null
        }
      },
      "pay_elements": [
  ... (84 more lines)

GET /pay-weeks.php

Is the pay week final, and what was exported?

Scope: payroll:read

One row per pay week, newest first: status, lock, counts of what still stops it closing, late adjustments, and the export files made for it. Neither week_start nor from/to: the current pay week and the 7 before it. A site-limited key gets its sites' counts and no batch totals.

Query parameters

NameTypeDescription
week_startdateAny date inside the pay week wanted.
fromdateWith to: a range, widened to whole pay weeks (at most 26).
todateSee from.

Reply: data is PayWeek.

Errors: 400, 401, 403, 429, 500. See Errors for the codes.

Examples

Is the pay week final, and what was exported? 200

Key used: a key with payroll:read only (pay figures, no charges).

Request

curl -s "$TC_BASE/pay-weeks.php?week_start=2026-09-25" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "week_start": "2026-09-21",
      "week_end": "2026-09-27",
      "status": "exported",
      "ended": true,
      "locked": true,
      "locked_at": "2026-10-01 07:33:35",
      "locked_at_utc": "2026-10-01T06:33:35Z",
      "locked_by": null,
      "lock_id": 19,
      "frozen": false,
      "shifts": 107,
      "shifts_not_approved": 29,
      "open_clock_outs": 0,
      "exceptions_open": 0,
      "late_adjustments_due": 0,
      "late_adjustments_paid": 0,
      "late_clocks_in_week": 1,
      "batches": [
        {
          "id": 8,
          "kind": "sage",
          "format": "sage50",
          "client_id": 21,
          "period_start": "2026-09-21",
          "period_end": "2026-09-27",
          "entry_count": 2,
          "line_count": 4,
          "file_name": "sage50_payroll_2026-09-21.csv",
          "file_sha256": "2393f9791223b6c74c664b8ad9549911439b7e9f10057d5c5ae9b9807b36f322",
          "reason": null,
          "created_at": "2026-10-02 07:33:35",
          "created_at_utc": "2026-10-02T06:33:35Z",
          "stamps_entries": true,
          "total_pay": 241.5
        }
      ]
    }
  ],
  "meta": {
    "period_from": "2026-09-21",
    "period_to": "2026-09-27",
    "pay_week_start_day": 1,
    "approval_rule": "agency_approved"
  }
}
The current pay week and the 7 before it 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/pay-weeks.php" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "week_start": "2026-09-28",
      "week_end": "2026-10-04",
      "status": "open",
      "ended": false,
      "locked": false,
      "locked_at": null,
      "locked_at_utc": null,
      "locked_by": null,
      "lock_id": null,
      "frozen": false,
      "shifts": 23,
      "shifts_not_approved": 6,
      "open_clock_outs": 3,
      "exceptions_open": 9,
      "late_adjustments_due": 1,
      "late_adjustments_paid": 0,
      "late_clocks_in_week": 0,
      "batches": []
    },
    {
      "week_start": "2026-09-21",
      "week_end": "2026-09-27",
      "status": "exported",
      "ended": true,
      "locked": true,
      "locked_at": "2026-10-01 07:33:35",
      "locked_at_utc": "2026-10-01T06:33:35Z",
      "locked_by": null,
      "lock_id": 19,
      "frozen": false,
      "shifts": 107,
      "shifts_not_approved": 29,
      "open_clock_outs": 0,
      "exceptions_open": 0,
      "late_adjustments_due": 0,
      "late_adjustments_paid": 0,
      "late_clocks_in_week": 1,
      "batches": [
        {
          "id": 8,
          "kind": "sage",
          "format": "sage50",
          "client_id": 21,
          "period_start": "2026-09-21",
          "period_end": "2026-09-27",
          "entry_count": 2,
          "line_count": 4,
          "file_name": "sage50_payroll_2026-09-21.csv",
          "file_sha256": "2393f9791223b6c74c664b8ad9549911439b7e9f10057d5c5ae9b9807b36f322",
          "reason": null,
          "created_at": "2026-10-02 07:33:35",
          "created_at_utc": "2026-10-02T06:33:35Z",
          "stamps_entries": true,
          "total_pay": 241.5,
          "total_charge": 362.25
        }
      ]
  ... (129 more lines)

GET /export-batches.php

Export batches (Sage, Xero, payroll files)

Scope: payroll:read

The payroll and accounts files made for this client, newest first, to reconcile against what was imported. A batch made for every client at once is not listed (it holds other clients' figures). One batch (?id=) adds entry_ids: the shifts the file held, recorded when it was made, for Sage, Xero and payroll files alike. Exporting the week again, or sending it to Xero after Sage, makes a new batch and does not change an older batch's list.

Query parameters

NameTypeDescription
idintegerOne batch by id (the reply's data is then an object, not a list).
fromdateBatches whose period overlaps from and to.
todateSee from.
kindstring: sage, xero, payrollOne kind of file.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is ExportBatch or ExportBatchDetail (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Sage, Xero and payroll files made for this client 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/export-batches.php?limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 8,
      "kind": "sage",
      "format": "sage50",
      "client_id": 21,
      "period_start": "2026-09-21",
      "period_end": "2026-09-27",
      "entry_count": 2,
      "line_count": 4,
      "file_name": "sage50_payroll_2026-09-21.csv",
      "file_sha256": "2393f9791223b6c74c664b8ad9549911439b7e9f10057d5c5ae9b9807b36f322",
      "reason": null,
      "created_at": "2026-10-02 07:33:35",
      "created_at_utc": "2026-10-02T06:33:35Z",
      "stamps_entries": true,
      "total_pay": 241.5,
      "total_charge": 362.25
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 2,
    "offset": 0,
    "has_more": false
  }
}
One batch with the shifts it paid 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/export-batches.php?id=8" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 8,
    "kind": "sage",
    "format": "sage50",
    "client_id": 21,
    "period_start": "2026-09-21",
    "period_end": "2026-09-27",
    "entry_count": 2,
    "line_count": 4,
    "file_name": "sage50_payroll_2026-09-21.csv",
    "file_sha256": "2393f9791223b6c74c664b8ad9549911439b7e9f10057d5c5ae9b9807b36f322",
    "reason": null,
    "created_at": "2026-10-02 07:33:35",
    "created_at_utc": "2026-10-02T06:33:35Z",
    "stamps_entries": true,
    "total_pay": 241.5,
    "total_charge": 362.25,
    "entry_ids": [
      36686,
      36688
    ],
    "file_ok": false
  }
}

GET /charges.php

Charge schedule (what the agency bills)

Scope: charges:read

Charge lines per site, worker, role and pay element, with PO, Sage account and VAT, for invoicing. Closed shifts at this client's sites, approved by the agency (and by the client when it signs off hours), not rejected, not held by an open query, not already invoiced as a late adjustment. A locked pay week returns its frozen charge lines. The same builder as the Charge schedule page and its Sage and Xero files.

Query parameters

NameTypeDescription
week_startdateAny date inside the pay week wanted.
fromdateWith to: any period up to 93 days.
todateSee from.
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
vattrue or false (or 1, 0)true = add VAT per site.
vat_ratenumberVAT percentage, 0 to 100 (default 20).
include_entriestrue or false (or 1, 0)true = list the time entry ids behind each line.

Reply: data is Charges.

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Charge schedule for one pay week, with VAT 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/charges.php?week_start=2026-09-25&vat=true" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "reference": "CS21-20260921",
    "client_id": 21,
    "from": "2026-09-21",
    "to": "2026-09-27",
    "frozen": false,
    "partly_frozen": false,
    "require_client_approval": false,
    "currency": "GBP",
    "lines": [
      {
        "site_id": 39,
        "site_name": "Birmingham Office",
        "worker_id": 351,
        "worker_name": "Aisha Rahman",
        "employee_id": "AX025",
        "role": "Customer Service Agent",
        "cost_code": "WH-OPS",
        "po": null,
        "element": "regular",
        "element_label": "Standard",
        "hours": 2.8,
        "rate": 18.9,
        "amount": 52.92,
        "shifts": 1,
        "first_date": "2026-09-24",
        "last_date": "2026-09-24",
        "frozen": false,
        "no_charge": false
      },
      {
        "site_id": 39,
        "site_name": "Birmingham Office",
        "worker_id": 337,
        "worker_name": "Amira Patel",
        "employee_id": "AX011",
        "role": "Administrator",
        "cost_code": "MAINT",
        "po": null,
        "element": "regular",
        "element_label": "Standard",
        "hours": 34.97,
        "rate": 18.9,
        "amount": 660.87,
        "shifts": 5,
        "first_date": "2026-09-21",
        "last_date": "2026-09-25",
        "frozen": false,
        "no_charge": false
      },
      {
        "site_id": 39,
        "site_name": "Birmingham Office",
        "worker_id": 339,
        "worker_name": "Charlotte Evans",
        "employee_id": "AX013",
        "role": "Customer Service Agent",
        "cost_code": "WH-OPS",
        "po": null,
  ... (581 more lines)

GET /rate-rules.php

Rate rules (read only)

Scope: pay:read or charges:read

How shifts are priced: base rates, overtime, weekend, Sunday, bank holiday, night bands, guarantees and break rules. Needs pay:read or charges:read; each half of the money is shown only with its own scope. meta.settings gives the engine settings that change how rules combine.

Query parameters

NameTypeDescription
idintegerOne rule by id (the reply's data is then an object, not a list).
location_idintegerRules for one site plus the client-wide ones.
activetrue or false (or 1, 0)Default: active rules only.
ondateRules valid on that date.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is RateRule (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Rate rules 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/rate-rules.php?limit=1" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 46,
      "name": "Standard Rate - Birmingham Office",
      "client_id": 21,
      "location_id": 39,
      "job_role": null,
      "cost_code_id": null,
      "valid_from": null,
      "valid_to": null,
      "priority": 2,
      "active": true,
      "overtime_threshold": 40,
      "overtime_multiplier": 1.5,
      "overtime_tiers": null,
      "daily_overtime_threshold": null,
      "daily_overtime_multiplier": null,
      "weekend_multiplier": 1.25,
      "sunday_multiplier": null,
      "bank_holiday_multiplier": 2,
      "guarantee_hours": null,
      "paid_break_minutes": 0,
      "auto_lunch_minutes": 30,
      "auto_lunch_after_hours": 6,
      "pay": {
        "pay_rate": 13.5,
        "overtime_pay_rate": null,
        "daily_overtime_pay_rate": null,
        "weekend_pay_rate": null,
        "sunday_pay_rate": null,
        "bank_holiday_pay_rate": null,
        "pre_parity_pay_rate": null,
        "post_parity_pay_rate": null
      },
      "charge": {
        "charge_rate": 18.9,
        "overtime_charge_rate": null,
        "daily_overtime_charge_rate": null,
        "weekend_charge_rate": null,
        "sunday_charge_rate": null,
        "bank_holiday_charge_rate": null,
        "pre_parity_charge_rate": null,
        "post_parity_charge_rate": null
      },
      "night_bands": []
    }
  ],
  "pagination": {
    "total": 3,
    "limit": 1,
    "offset": 0,
    "has_more": true
  },
  "meta": {
    "settings": {
      "premium_stacking": "stack",
      "premium_day_basis": "shift_start",
      "site_overtime_mode": "off",
      "walk_in_policy": "no_pay",
  ... (7 more lines)
One rate rule 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/rate-rules.php?id=44" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 44,
    "name": "Standard Rate - London HQ",
    "client_id": 21,
    "location_id": 37,
    "job_role": null,
    "cost_code_id": null,
    "valid_from": null,
    "valid_to": null,
    "priority": 2,
    "active": true,
    "overtime_threshold": 40,
    "overtime_multiplier": 1.5,
    "overtime_tiers": null,
    "daily_overtime_threshold": null,
    "daily_overtime_multiplier": null,
    "weekend_multiplier": 1.25,
    "sunday_multiplier": null,
    "bank_holiday_multiplier": 2,
    "guarantee_hours": null,
    "paid_break_minutes": 0,
    "auto_lunch_minutes": 30,
    "auto_lunch_after_hours": 6,
    "pay": {
      "pay_rate": 12.5,
      "overtime_pay_rate": null,
      "daily_overtime_pay_rate": null,
      "weekend_pay_rate": null,
      "sunday_pay_rate": null,
      "bank_holiday_pay_rate": null,
      "pre_parity_pay_rate": null,
      "post_parity_pay_rate": null
    },
    "charge": {
      "charge_rate": 17.5,
      "overtime_charge_rate": null,
      "daily_overtime_charge_rate": null,
      "weekend_charge_rate": null,
      "sunday_charge_rate": null,
      "bank_holiday_charge_rate": null,
      "pre_parity_charge_rate": null,
      "post_parity_charge_rate": null
    },
    "night_bands": []
  },
  "meta": {
    "settings": {
      "premium_stacking": "stack",
      "premium_day_basis": "shift_start",
      "site_overtime_mode": "off",
      "walk_in_policy": "no_pay",
      "cancelled_policy": "no_pay",
      "multi_shift_priority": "closest",
      "pay_week_start_day": 1
    }
  }
}

Bookings

GET /shifts.php

List bookings, or one booking

Scope: shifts:read

A booking belongs to the client that owns its site, else the worker's home client (a pooled worker's booking at this client's site is listed here). A site-limited key sees bookings at its sites. Order: shift_date newest first, then start_time, then id.

Query parameters

NameTypeDescription
idintegerOne booking by id (the reply's data is then an object, not a list).
date_fromdateOn shift_date.
date_todateOn shift_date.
worker_idintegerOne worker.
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
department_idintegerThe worker's department.
demand_idintegerBookings of one requirement.
statusstringOne status (see Shift.status). status=cancelled also returns older bookings stored with a blank status, which are shown as cancelled.
updated_sincestringOnly records created or changed since then. ISO 8601 with Z or an offset (2026-10-06T09:00:00Z), or local time. De-duplicate on id. The bound is compared as an exact instant with the exact time of each change (updated_at_utc). A local time inside the repeated hour when the clocks go back is read as its first pass, so no change is missed but a few can come back twice; send Z or an offset to be exact.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Shift (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Bookings on one day 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/shifts.php?date_from=2026-09-25&date_to=2026-09-25&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 27260,
      "client_id": 21,
      "worker_id": 327,
      "worker_name": "James Wilson",
      "employee_id": "AX001",
      "department_id": 41,
      "location_id": 37,
      "location_name": "London HQ",
      "demand_id": 1589,
      "shift_date": "2026-09-25",
      "start_time": "06:00:00",
      "end_time": "14:00:00",
      "start_utc": "2026-09-25T05:00:00Z",
      "end_utc": "2026-09-25T13:00:00Z",
      "break_minutes": 30,
      "job_role": null,
      "notes": null,
      "status": "completed",
      "cancel_reason": null,
      "cancelled_at": null,
      "acknowledged_at": null,
      "declined_at": null,
      "confirmation": "confirmed",
      "created_at": "2026-09-14 00:00:00",
      "created_at_utc": "2026-09-13T23:00:00Z",
      "updated_at": "2026-09-14 00:00:00",
      "updated_at_utc": "2026-09-13T23:00:00Z"
    },
    {
      "id": 27262,
      "client_id": 21,
      "worker_id": 329,
      "worker_name": "Mohammed Khan",
      "employee_id": "AX003",
      "department_id": 40,
      "location_id": 38,
      "location_name": "Manchester Warehouse",
      "demand_id": 1591,
      "shift_date": "2026-09-25",
      "start_time": "06:00:00",
      "end_time": "14:00:00",
      "start_utc": "2026-09-25T05:00:00Z",
      "end_utc": "2026-09-25T13:00:00Z",
      "break_minutes": 30,
      "job_role": null,
      "notes": null,
      "status": "completed",
      "cancel_reason": null,
      "cancelled_at": null,
      "acknowledged_at": null,
      "declined_at": null,
      "confirmation": "confirmed",
      "created_at": "2026-09-11 00:00:00",
      "created_at_utc": "2026-09-10T23:00:00Z",
      "updated_at": "2026-09-11 00:00:00",
      "updated_at_utc": "2026-09-10T23:00:00Z"
    }
  ... (8 more lines)
One booking 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/shifts.php?id=27260" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 27260,
    "client_id": 21,
    "worker_id": 327,
    "worker_name": "James Wilson",
    "employee_id": "AX001",
    "department_id": 41,
    "location_id": 37,
    "location_name": "London HQ",
    "demand_id": 1589,
    "shift_date": "2026-09-25",
    "start_time": "06:00:00",
    "end_time": "14:00:00",
    "start_utc": "2026-09-25T05:00:00Z",
    "end_utc": "2026-09-25T13:00:00Z",
    "break_minutes": 30,
    "job_role": null,
    "notes": null,
    "status": "completed",
    "cancel_reason": null,
    "cancelled_at": null,
    "acknowledged_at": null,
    "declined_at": null,
    "confirmation": "confirmed",
    "created_at": "2026-09-14 00:00:00",
    "created_at_utc": "2026-09-13T23:00:00Z",
    "updated_at": "2026-09-14 00:00:00",
    "updated_at_utc": "2026-09-13T23:00:00Z"
  }
}

POST /shifts.php

Book a worker, or cancel a booking

Scope: shifts:write Idempotency-Key accepted

Without ?action: book a worker (create a shift), on a slot or onto a requirement (demand_id). The same eligibility rules as the Shifts board: a hard block (do not return, approved absence, marked unavailable, overlapping booking, inactive, pending or dormant) is 409 not_eligible with block_codes and warning_codes; only codes are returned, never the reason text. A requirement that is full or has finished, and a date in a locked pay week (reason_code week_locked), are 409. With ?id=&action=cancel: cancel a booking or record sickness or a no-show (body ShiftCancel); a worked booking or one in a locked week cannot be cancelled (409, reason_code worked or week_locked). Every write is in the audit log (named "API key #N") and queues shift.created (and legacy shift_created) or shift.cancelled. The worker is told about a change, as in the console. Accepts an Idempotency-Key header.

Query parameters

NameTypeDescription
idintegerThe booking to cancel (with action=cancel).
actionstring: cancelcancel = cancel the booking given by id.

JSON body: ShiftCreate

POST /shifts.php body. Either the slot, or demand_id to book onto a requirement (its site, date, times, break and role are used).

FieldTypeDescription
worker_idintegerYour own worker (based at one of a site-limited key's sites).
demand_id sometimesinteger
location_id sometimesintegerRequired without demand_id.
shift_date sometimesdate
start_time sometimesstringHH:MM
end_time sometimesstringHH:MM
break_minutes sometimesinteger
job_role sometimesstring
status sometimesstring: draft, publishedDefault draft.
notes sometimesstring

JSON body: ShiftCancel

Cancel body. The status drives the cancelled-booking pay policy and the reliability score.

FieldTypeDescription
status sometimesstring: cancelled, client_cancelled, worker_cancelled, sick, awol, no_showDefault cancelled.
cancel_reason sometimesstring

Reply: data is Shift.

Errors: 400, 401, 403, 404, 409, 422, 429, 500. See Errors for the codes.

Examples

Book a worker 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/shifts.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"location_id":37,"shift_date":"2026-10-27","start_time":"08:00","end_time":"16:00","break_minutes":30,"job_role":"Warehouse Operative","status":"published","notes":"Bring safety boots"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "id": 31187,
    "client_id": 21,
    "worker_id": 409,
    "worker_name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "department_id": 40,
    "location_id": 37,
    "location_name": "London HQ",
    "demand_id": 3057,
    "shift_date": "2026-10-27",
    "start_time": "08:00:00",
    "end_time": "16:00:00",
    "start_utc": "2026-10-27T08:00:00Z",
    "end_utc": "2026-10-27T16:00:00Z",
    "break_minutes": 30,
    "job_role": "Warehouse Operative",
    "notes": "Bring safety boots",
    "status": "published",
    "cancel_reason": null,
    "cancelled_at": null,
    "acknowledged_at": null,
    "declined_at": null,
    "confirmation": "unconfirmed",
    "created_at": "2026-10-02 07:33:38",
    "created_at_utc": "2026-10-02T06:33:38Z",
    "updated_at": "2026-10-02 07:33:38",
    "updated_at_utc": "2026-10-02T06:33:38Z",
    "warning_codes": [
      "role"
    ]
  },
  "message": "Shift created."
}
Book a worker onto the requirement 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/shifts.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"demand_id":3066,"status":"published"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "id": 31188,
    "client_id": 21,
    "worker_id": 409,
    "worker_name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "department_id": 40,
    "location_id": 37,
    "location_name": "London HQ",
    "demand_id": 3066,
    "shift_date": "2026-10-28",
    "start_time": "06:00:00",
    "end_time": "14:00:00",
    "start_utc": "2026-10-28T06:00:00Z",
    "end_utc": "2026-10-28T14:00:00Z",
    "break_minutes": 30,
    "job_role": "Picker",
    "notes": null,
    "status": "published",
    "cancel_reason": null,
    "cancelled_at": null,
    "acknowledged_at": null,
    "declined_at": null,
    "confirmation": "unconfirmed",
    "created_at": "2026-10-02 07:33:39",
    "created_at_utc": "2026-10-02T06:33:39Z",
    "updated_at": "2026-10-02 07:33:39",
    "updated_at_utc": "2026-10-02T06:33:39Z",
    "warning_codes": [
      "role"
    ]
  },
  "message": "Shift created."
}
A second booking that overlaps: refused with codes 409

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/shifts.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"location_id":38,"shift_date":"2026-10-27","start_time":"12:00","end_time":"20:00"}'

Real reply (local test copy, fake demo data)

HTTP 409

{
  "error": true,
  "message": "Worker not eligible for this slot.",
  "status": 409,
  "error_code": "not_eligible",
  "block_codes": [
    "overlap"
  ],
  "warning_codes": []
}
Cancel a booking (the client no longer needs it) 200

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/shifts.php?id=31187&action=cancel" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"client_cancelled","cancel_reason":"Client reduced headcount"}'

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 31187,
    "client_id": 21,
    "worker_id": 409,
    "worker_name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "department_id": 40,
    "location_id": 37,
    "location_name": "London HQ",
    "demand_id": null,
    "shift_date": "2026-10-27",
    "start_time": "09:00:00",
    "end_time": "17:00:00",
    "start_utc": "2026-10-27T09:00:00Z",
    "end_utc": "2026-10-27T17:00:00Z",
    "break_minutes": 30,
    "job_role": "Warehouse Operative",
    "notes": "Bring safety boots",
    "status": "client_cancelled",
    "cancel_reason": "Client reduced headcount",
    "cancelled_at": "2026-10-02 07:33:39",
    "acknowledged_at": null,
    "declined_at": null,
    "confirmation": "n/a",
    "created_at": "2026-10-02 07:33:38",
    "created_at_utc": "2026-10-02T06:33:38Z",
    "updated_at": "2026-10-02 07:33:39",
    "updated_at_utc": "2026-10-02T06:33:39Z"
  },
  "message": "Booking client cancelled."
}

PUT /shifts.php

Change a booking

Scope: shifts:write Idempotency-Key accepted

Times, break, role, worker, site, notes or draft/published. A new worker or new times are checked again. A booking that moves to another date, time or site no longer fills its requirement (detached_demand_id); the requirement shows the open place again. A worked booking or one in a locked pay week can only have its notes changed (409 conflict, reason_code worked or week_locked). Queues shift.updated. Accepts an Idempotency-Key header.

Query parameters

NameTypeDescription
id requiredintegerThe booking.

JSON body: ShiftUpdate

PUT /shifts.php body: any of these. A worked booking, or one in a locked pay week, can only have its notes changed (409 conflict, reason_code worked or week_locked).

FieldTypeDescription
worker_id sometimesinteger
location_id sometimesinteger
shift_date sometimesdate
start_time sometimesstring
end_time sometimesstring
break_minutes sometimesinteger
job_role sometimesstring or null
notes sometimesstring or null
status sometimesstring: draft, published

Reply: data is Shift.

Errors: 400, 401, 403, 404, 409, 422, 429, 500. See Errors for the codes.

Examples

Change the booking times 200

Key used: a key holding every scope.

Request

curl -s -X PUT "$TC_BASE/shifts.php?id=31187" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"start_time":"09:00","end_time":"17:00"}'

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 31187,
    "client_id": 21,
    "worker_id": 409,
    "worker_name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "department_id": 40,
    "location_id": 37,
    "location_name": "London HQ",
    "demand_id": null,
    "shift_date": "2026-10-27",
    "start_time": "09:00:00",
    "end_time": "17:00:00",
    "start_utc": "2026-10-27T09:00:00Z",
    "end_utc": "2026-10-27T17:00:00Z",
    "break_minutes": 30,
    "job_role": "Warehouse Operative",
    "notes": "Bring safety boots",
    "status": "published",
    "cancel_reason": null,
    "cancelled_at": null,
    "acknowledged_at": null,
    "declined_at": null,
    "confirmation": "unconfirmed",
    "created_at": "2026-10-02 07:33:38",
    "created_at_utc": "2026-10-02T06:33:38Z",
    "updated_at": "2026-10-02 07:33:39",
    "updated_at_utc": "2026-10-02T06:33:39Z",
    "warning_codes": [
      "role"
    ],
    "detached_demand_id": 3057
  },
  "message": "Shift updated. It no longer fills its requirement, which shows the open place again."
}

GET /demands.php

List requirements, or one with its bookings

Scope: shifts:read

Client requirements: N people for a site and time, with how many are booked. Order: shift_date, start_time, id. A booking added, cancelled, moved or re-timed on a requirement counts as a change to it (updated_at moves), so updated_since picks up a change to filled and open even when status stays the same.

Query parameters

NameTypeDescription
idintegerOne requirement by id (the reply's data is then an object, not a list).
date_fromdateOn shift_date.
date_todateOn shift_date.
location_idintegerOne site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed.
statusstring: open, partial, filled, cancelledOne status.
unfilledtrue or false (or 1, 0)true = open or partial.
series_idintegerOne repeating series.
job_rolestringOne role.
updated_sincestringOnly records created or changed since then. ISO 8601 with Z or an offset (2026-10-06T09:00:00Z), or local time. De-duplicate on id. The bound is compared as an exact instant with the exact time of each change (updated_at_utc). A local time inside the repeated hour when the clocks go back is read as its first pass, so no change is missed but a few can come back twice; send Z or an offset to be exact. A booking added to, cancelled on, moved off or re-timed on a requirement counts as a change to the requirement, even when its status stays the same.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Demand (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Open requirements 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/demands.php?unfilled=true&date_from=2026-10-02&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 261,
      "client_id": 21,
      "location_id": 38,
      "location_name": "Manchester Warehouse",
      "shift_date": "2026-10-06",
      "start_time": "14:00:00",
      "end_time": "22:00:00",
      "start_utc": "2026-10-06T13:00:00Z",
      "end_utc": "2026-10-06T21:00:00Z",
      "break_minutes": 30,
      "job_role": null,
      "headcount": 1,
      "requested": null,
      "filled": 0,
      "open": 1,
      "status": "open",
      "series_id": null,
      "source": "admin",
      "notes": null,
      "cancel_reason": null,
      "cancelled_at": null,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z",
      "updated_at": "2026-09-30 10:58:11",
      "updated_at_utc": "2026-09-30T09:58:11Z"
    },
    {
      "id": 298,
      "client_id": 21,
      "location_id": 38,
      "location_name": "Manchester Warehouse",
      "shift_date": "2026-10-13",
      "start_time": "14:00:00",
      "end_time": "22:00:00",
      "start_utc": "2026-10-13T13:00:00Z",
      "end_utc": "2026-10-13T21:00:00Z",
      "break_minutes": 30,
      "job_role": null,
      "headcount": 1,
      "requested": null,
      "filled": 0,
      "open": 1,
      "status": "open",
      "series_id": null,
      "source": "admin",
      "notes": null,
      "cancel_reason": null,
      "cancelled_at": null,
      "created_at": "2026-09-30 10:58:11",
      "created_at_utc": "2026-09-30T09:58:11Z",
      "updated_at": "2026-09-30 10:58:11",
      "updated_at_utc": "2026-09-30T09:58:11Z"
    }
  ],
  "pagination": {
    "total": 22,
    "limit": 2,
  ... (4 more lines)
One requirement with its bookings 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/demands.php?id=3066" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 3066,
    "client_id": 21,
    "location_id": 37,
    "location_name": "London HQ",
    "shift_date": "2026-10-28",
    "start_time": "06:00:00",
    "end_time": "14:00:00",
    "start_utc": "2026-10-28T06:00:00Z",
    "end_utc": "2026-10-28T14:00:00Z",
    "break_minutes": 30,
    "job_role": "Picker",
    "headcount": 3,
    "requested": 3,
    "filled": 1,
    "open": 2,
    "status": "partial",
    "series_id": null,
    "source": "api",
    "notes": null,
    "cancel_reason": null,
    "cancelled_at": null,
    "created_at": "2026-10-02 07:33:39",
    "created_at_utc": "2026-10-02T06:33:39Z",
    "updated_at": "2026-10-02 07:33:39",
    "updated_at_utc": "2026-10-02T06:33:39Z",
    "bookings": [
      {
        "shift_id": 31188,
        "worker_id": 409,
        "worker_name": "Jordan Example",
        "employee_id": "DOCS-77E88",
        "status": "published",
        "confirmation": "unconfirmed"
      }
    ]
  }
}

POST /demands.php

Create a requirement

Scope: shifts:write Idempotency-Key accepted

One date. Book workers onto it with POST /shifts.php and demand_id. A date in a locked pay week is 409 conflict, reason_code week_locked. Accepts an Idempotency-Key header.

JSON body: DemandCreate

FieldTypeDescription
location_idinteger
shift_datedate
start_timestringHH:MM
end_timestringHH:MM
headcount sometimesinteger1 to 500 (default 1).
break_minutes sometimesinteger
job_role sometimesstring
notes sometimesstring

Reply: data is Demand.

Errors: 400, 401, 403, 404, 409, 422, 429, 500. See Errors for the codes.

Examples

Create a requirement: 3 people for an early shift 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/demands.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"location_id":37,"shift_date":"2026-10-28","start_time":"06:00","end_time":"14:00","headcount":3,"break_minutes":30,"job_role":"Picker"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "id": 3066,
    "client_id": 21,
    "location_id": 37,
    "location_name": "London HQ",
    "shift_date": "2026-10-28",
    "start_time": "06:00:00",
    "end_time": "14:00:00",
    "start_utc": "2026-10-28T06:00:00Z",
    "end_utc": "2026-10-28T14:00:00Z",
    "break_minutes": 30,
    "job_role": "Picker",
    "headcount": 3,
    "requested": 3,
    "filled": 0,
    "open": 3,
    "status": "open",
    "series_id": null,
    "source": "api",
    "notes": null,
    "cancel_reason": null,
    "cancelled_at": null,
    "created_at": "2026-10-02 07:33:39",
    "created_at_utc": "2026-10-02T06:33:39Z",
    "updated_at": "2026-10-02 07:33:39",
    "updated_at_utc": "2026-10-02T06:33:39Z"
  },
  "message": "Requirement created."
}

GET /offers.php

Shift offers sent to workers (read only)

Scope: shifts:read

Offers sent from the Shifts board for a requirement, or as cover for a booking, and how workers answered. Sending offers stays in TempClock. Reply codes and links are never returned. Newest first.

Query parameters

NameTypeDescription
idintegerOne offer by id (the reply's data is then an object, not a list).
demand_idintegerOffers for one requirement.
shift_idintegerCover offers for one booking.
worker_idintegerOne worker.
statusstring: sent, accepted, declined, filled, expiredOne status.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Offer (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Offers sent for a requirement 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/offers.php?demand_id=3066" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 4,
      "demand_id": 3066,
      "worker_id": 329,
      "worker_name": "Mohammed Khan",
      "employee_id": "AX003",
      "status": "sent",
      "via": "email",
      "sent_at": "2026-10-02 07:33:39",
      "sent_at_utc": "2026-10-02T06:33:39Z",
      "responded_at": null,
      "responded_at_utc": null,
      "expires_at": "2026-10-02 13:33:39",
      "expires_at_utc": "2026-10-02T12:33:39Z",
      "shift_id": null,
      "cover_for_shift_id": null
    },
    {
      "id": 3,
      "demand_id": 3066,
      "worker_id": 327,
      "worker_name": "James Wilson",
      "employee_id": "AX001",
      "status": "declined",
      "via": "sms",
      "sent_at": "2026-10-02 07:33:39",
      "sent_at_utc": "2026-10-02T06:33:39Z",
      "responded_at": "2026-10-02 07:28:39",
      "responded_at_utc": "2026-10-02T06:28:39Z",
      "expires_at": "2026-10-02 13:33:39",
      "expires_at_utc": "2026-10-02T12:33:39Z",
      "shift_id": null,
      "cover_for_shift_id": null
    }
  ],
  "pagination": {
    "total": 2,
    "limit": 50,
    "offset": 0,
    "has_more": false
  }
}

GET /availability.php

Availability rules, or who is free for a slot

Scope: availability:read

Without available_from/available_to: the rules (weekly pattern, rolling pattern, exceptions). With them: who is free for that slot, using the Shifts board's own eligibility rules, best matches first (the reply's data is then a list of AvailabilityMatch and meta.slot describes the slot). A worker with no pattern has unknown availability, which never blocks. Codes are returned, never reason text.

Query parameters

NameTypeDescription
worker_idintegerOne worker's rules.
rule_typestring: weekly, rolling, exceptionOne kind of rule.
include_retiredtrue or false (or 1, 0)Also retired rules.
available_fromstringWho is free: the slot start (ISO 8601 with an offset or Z, or local time).
available_tostringThe slot end: after available_from and at most 24 hours later, measured on the instants sent (so a slot across the clock change is measured in real time). A slot that would be exactly 24 hours on the clock is refused (400): split it in two.
location_idintegerWho is free: the slot's site (its bans and rest rule apply).
job_rolestringWho is free: the role wanted.
department_idintegerWho is free: one department.
include_unavailabletrue or false (or 1, 0)Who is free: also list workers who cannot take it, with block_codes.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is AvailabilityRule or AvailabilityMatch (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

One worker's rules 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/availability.php?worker_id=409" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 82,
      "worker_id": 409,
      "rule_type": "weekly",
      "days": [
        1,
        2,
        3,
        4,
        5
      ],
      "days_label": "Mon to Fri",
      "start_time": "06:00",
      "end_time": "18:00",
      "on_days": null,
      "off_days": null,
      "effective_from": "2026-10-02",
      "effective_to": null,
      "active": true,
      "created_at": "2026-10-02 07:33:39",
      "created_at_utc": "2026-10-02T06:33:39Z"
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 50,
    "offset": 0,
    "has_more": false
  }
}
Who is free for a slot at a site, best matches first 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/availability.php?available_from=2026-10-28T07%3A00%3A00%2B01%3A00&available_to=2026-10-28T15%3A00%3A00%2B01%3A00&location_id=37&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "worker_id": 328,
      "name": "Emma Thompson",
      "employee_id": "AX002",
      "location_id": 37,
      "department_id": 41,
      "job_role": "Administrator",
      "eligible": true,
      "available": true,
      "availability": [
        "06:00-22:00"
      ],
      "role_match": null,
      "week_hours": 28,
      "max_hours_per_week": 48,
      "warning_codes": [],
      "block_codes": []
    },
    {
      "worker_id": 340,
      "name": "George Taylor",
      "employee_id": "AX014",
      "location_id": 38,
      "department_id": 40,
      "job_role": "Warehouse Operative",
      "eligible": true,
      "available": true,
      "availability": [
        "06:00-22:00"
      ],
      "role_match": null,
      "week_hours": 28.5,
      "max_hours_per_week": 48,
      "warning_codes": [],
      "block_codes": []
    }
  ],
  "pagination": {
    "total": 10,
    "limit": 2,
    "offset": 0,
    "has_more": true
  },
  "meta": {
    "slot": {
      "shift_date": "2026-10-28",
      "start_time": "06:00:00",
      "end_time": "14:00:00",
      "start_utc": "2026-10-28T06:00:00Z",
      "end_utc": "2026-10-28T14:00:00Z",
      "location_id": 37,
      "job_role": null
    }
  }
}

POST /availability.php

Add an availability rule

Scope: availability:write Idempotency-Key accepted

weekly replaces the worker's weekly pattern (old rules are retired); rolling replaces the rolling pattern; exception adds a date or range the worker is not available. Every write is in the audit log. Accepts an Idempotency-Key header.

JSON body: AvailabilityCreate

POST /availability.php body.

FieldTypeDescription
worker_idinteger
typestring: weekly, rolling, exception
slots sometimeslist of AvailabilitySlotweekly: replaces the weekly pattern. An empty list clears it.
on_days sometimesintegerrolling: 1 to 60.
off_days sometimesintegerrolling: 0 to 60.
effective_from sometimesdateDefault today.
effective_to sometimesdate
start_time sometimesstringrolling or exception: HH:MM.
end_time sometimesstring
reason sometimesstringexception: kept for the agency, never returned.

Reply: data is AvailabilityRules.

Errors: 400, 401, 403, 404, 422, 429, 500. See Errors for the codes.

Examples

Set a weekly pattern: Monday to Friday, 06:00 to 18:00 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/availability.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"type":"weekly","effective_from":"2026-10-02","slots":[{"days":"Mon-Fri","start_time":"06:00","end_time":"18:00"}]}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "worker_id": 409,
    "rules": [
      {
        "id": 82,
        "worker_id": 409,
        "rule_type": "weekly",
        "days": [
          1,
          2,
          3,
          4,
          5
        ],
        "days_label": "Mon to Fri",
        "start_time": "06:00",
        "end_time": "18:00",
        "on_days": null,
        "off_days": null,
        "effective_from": "2026-10-02",
        "effective_to": null,
        "active": true,
        "created_at": "2026-10-02 07:33:39",
        "created_at_utc": "2026-10-02T06:33:39Z"
      }
    ]
  },
  "message": "Availability saved."
}

DELETE /availability.php

Retire an availability rule

Scope: availability:write

The rule is kept for history (active false) and no longer applies.

Query parameters

NameTypeDescription
id requiredintegerThe rule.

Reply: data is AvailabilityRules.

Errors: 400, 401, 403, 404, 409, 429, 500. See Errors for the codes.

Examples

Retire a rule 200

Key used: a key holding every scope.

Request

curl -s -X DELETE "$TC_BASE/availability.php?id=82" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "worker_id": 409,
    "rules": []
  },
  "message": "Rule removed."
}

GET /absences.php

Holiday and sickness requests

Scope: absences:read

Requests of the key's client's workers, start date newest first. An approved absence already blocks bookings and the "who is free" answer. The worker's reason and the reviewer's notes are never returned: they can hold health details.

Query parameters

NameTypeDescription
idintegerOne request by id (the reply's data is then an object, not a list).
worker_idintegerOne worker.
typestring: holiday, sick, unpaid, compassionate, training, otherOne type.
statusstring: pending, approved, rejected, cancelledOne status.
fromdateRequests that overlap from and to.
todateSee from.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is Absence (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Holiday and sickness requests 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/absences.php?limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 55,
      "worker_id": 334,
      "worker_name": "Sarah Clarke",
      "employee_id": "AX008",
      "location_id": 39,
      "type": "holiday",
      "start_date": "2026-10-07",
      "end_date": "2026-10-11",
      "days": 5,
      "status": "pending",
      "reviewed_at": null,
      "reviewed_at_utc": null,
      "created_at": "2026-09-30 10:58:12",
      "created_at_utc": "2026-09-30T09:58:12Z"
    },
    {
      "id": 54,
      "worker_id": 330,
      "worker_name": "Sophie Brown",
      "employee_id": "AX004",
      "location_id": 39,
      "type": "sick",
      "start_date": "2026-09-25",
      "end_date": "2026-09-26",
      "days": 2,
      "status": "approved",
      "reviewed_at": "2026-09-25 10:58:12",
      "reviewed_at_utc": "2026-09-25T09:58:12Z",
      "created_at": "2026-09-30 10:58:12",
      "created_at_utc": "2026-09-30T09:58:12Z"
    }
  ],
  "pagination": {
    "total": 3,
    "limit": 2,
    "offset": 0,
    "has_more": true
  }
}

Webhooks

GET /webhooks.php

Subscriptions, one subscription, its delivery log, or the event list

Scope: webhooks:read

No action: list subscriptions (?active=), or one with ?id= (with its 10 latest deliveries). action=deliveries with id: the delivery log, paged, with payloads (the payload of an event this key could not subscribe to is left out and payload_hidden says why). action=events: the event catalogue. A site-limited key can list subscriptions but not read the delivery log.

Query parameters

NameTypeDescription
idintegerOne subscription.
actionstring: deliveries, eventsdeliveries (with id) or events.
activetrue or false (or 1, 0)List: active or paused subscriptions.
statusstring: delivered, pending, failedDelivery log: one status.
eventstringDelivery log: one event.
limitintegerRows per page, 1 to 100 (default 50).
offsetintegerRows to skip (default 0).

Reply: data is WebhookSubscription or WebhookDelivery or WebhookEvent (a list with pagination, or one object with ?id=).

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

List subscriptions 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/webhooks.php" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 31,
      "url": "https://hooks.example.com/tempclock",
      "events": [
        "pay_week.locked",
        "export.created",
        "timesheet.approved"
      ],
      "active": true,
      "description": "Payroll system",
      "api_key_id": 30,
      "secret": "68d11aea...",
      "created_at": "2026-10-02 07:33:40",
      "created_at_utc": "2026-10-02T06:33:40Z",
      "updated_at": null,
      "updated_at_utc": null
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 50,
    "offset": 0,
    "has_more": false
  }
}
One subscription with its latest deliveries 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/webhooks.php?id=31" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 31,
    "url": "https://hooks.example.com/tempclock",
    "events": [
      "pay_week.locked",
      "export.created",
      "timesheet.approved"
    ],
    "active": true,
    "description": "Payroll system",
    "api_key_id": 30,
    "secret": "68d11aea...",
    "created_at": "2026-10-02 07:33:40",
    "created_at_utc": "2026-10-02T06:33:40Z",
    "updated_at": null,
    "updated_at_utc": null,
    "recent_deliveries": []
  }
}
The delivery log, with payloads 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/webhooks.php?id=31&action=deliveries&limit=5" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "id": 233,
      "event": "ping",
      "status": "failed",
      "attempt": 1,
      "max_attempts": 5,
      "response_code": 0,
      "delivered_at": null,
      "delivered_at_utc": null,
      "next_retry_at": null,
      "next_retry_at_utc": null,
      "created_at": "2026-10-02 07:33:40",
      "created_at_utc": "2026-10-02T06:33:40Z",
      "response_body": "Blocked: URL resolves to a disallowed internal/loopback address",
      "payload": {
        "event": "ping",
        "timestamp": "2026-10-02T06:33:40Z",
        "data": {
          "client_id": 21,
          "message": "This is a test delivery from the TempClock API.",
          "subscription_id": 31
        },
        "delivery_id": 233
      }
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 5,
    "offset": 0,
    "has_more": false
  }
}
The event catalogue 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/webhooks.php?action=events" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "event": "clock_in",
      "group": "Clock",
      "description": "A worker clocked in (kiosk, worker app or API).",
      "legacy": false
    },
    {
      "event": "clock_out",
      "group": "Clock",
      "description": "A worker clocked out (kiosk, worker app or API). total_minutes is the real time worked.",
      "legacy": false
    },
    {
      "event": "clock.blocked_by_survey",
      "group": "Clock",
      "description": "A clock-in was refused because a pre-clock-in survey answer blocks it.",
      "legacy": false
    },
    {
      "event": "clock.geofence_breach",
      "group": "Clock",
      "description": "A clock-in was recorded outside the geofence of a site in warning mode.",
      "legacy": false
    },
    {
      "event": "clock.geofence_refused",
      "group": "Clock",
      "description": "A clock was refused outside a site's geofence: at a strict site from the kiosk or the API (also with no GPS), or at any mapped site from the worker app. A clock-out is checked as well as a clock-in. data.action says which: in or out, or at the kiosk break_start or break_end.",
      "legacy": false
    },
    {
      "event": "clock.low_gps_confidence",
      "group": "Clock",
      "description": "A clock-in was recorded with a GPS reading worse than the site threshold.",
      "legacy": false
    },
    {
      "event": "time_entry.created",
      "group": "Time records",
      "description": "A time record was added by hand (a manual clock-in in the agency or client console).",
      "legacy": false
    },
    {
      "event": "time_entry.updated",
      "group": "Time records",
      "description": "A time record was corrected, clocked out by hand or deleted. data.change says which.",
      "legacy": false
    },
    {
      "event": "time_entry.auto_closed",
      "group": "Time records",
      "description": "An open shift was clocked out by the site's auto clock-out rule. Check and correct it.",
      "legacy": false
    },
    {
      "event": "timesheet.approved",
      "group": "Approval",
      "description": "One or more shifts were approved in the agency console or the client console. data.via says where; data.entries lists them.",
  ... (130 more lines)

POST /webhooks.php

Create a subscription, send a test, or rotate its secret

Scope: webhooks:write Idempotency-Key accepted

No action: create a subscription (the signing secret is in the reply once). Subscribing to an event also needs the read scope for its data: timesheets:read for clock, time entry and approval events, shifts:read for bookings, workers:read for workers and documents, payroll:read for pay week, period and export events; * needs all four (403 insufficient_scope with required_scope and field events). action=test with id: deliver a ping to this subscription only, now, and answer what the endpoint said. action=rotate_secret with id: a new signing secret (shown once; the old one stops at once). test and rotate_secret work only on a subscription this key made, or one made with a key this key replaced by rotation: one made in the agency console (api_key_id null) or with any other key is 403 forbidden, and nothing changes. A site-limited key cannot create or change subscriptions (they cover every site). At most 25 subscriptions per client. Accepts an Idempotency-Key header.

Query parameters

NameTypeDescription
idintegerThe subscription (with an action).
actionstring: test, rotate_secrettest or rotate_secret.

JSON body: WebhookCreate

A new subscription is always active. To pause it, send PUT ?id= with active: false.

FieldTypeDescription
urlstringA public HTTPS address on port 443.
eventslist of stringNames from GET ?action=events, or ["*"]. Each needs the read scope for its data (see Webhooks).
description sometimesstringUp to 255 characters.

Reply: data is WebhookTestResult or WebhookSecret or WebhookSubscription.

Errors: 400, 401, 403, 404, 409, 422, 429, 500. See Errors for the codes.

Examples

Subscribe to payroll events (the secret is shown once) 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/webhooks.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/tempclock","events":["pay_week.locked","export.created","timesheet.approved"],"description":"Payroll system"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "secret": "68d11aea9cfc6db302a5cbd128eaa91a0be0d7c187fa592fbbf362fb12272383",
    "id": 31,
    "url": "https://hooks.example.com/tempclock",
    "events": [
      "pay_week.locked",
      "export.created",
      "timesheet.approved"
    ],
    "active": true,
    "description": "Payroll system",
    "api_key_id": 30,
    "created_at": "2026-10-02 07:33:40",
    "created_at_utc": "2026-10-02T06:33:40Z",
    "updated_at": null,
    "updated_at_utc": null
  },
  "message": "Webhook subscription created. Store the secret securely: it will not be shown again."
}
Send a test delivery to this subscription now (here the endpoint could not be reached) 200

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/webhooks.php?id=31&action=test" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "subscription_id": 31,
    "delivery_id": 233,
    "delivered": false,
    "response_code": 0,
    "error": "Blocked: URL resolves to a disallowed internal/loopback address"
  },
  "subscription_id": 31,
  "message": "Test delivery could not reach your endpoint: Blocked: URL resolves to a disallowed internal/loopback address"
}
Give the subscription a new signing secret 200

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/webhooks.php?id=31&action=rotate_secret" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 31,
    "secret": "cbfa923d89be879050e5273737429c3204be17f47138c8d9afc78cf3cdcb1a7b"
  },
  "message": "New signing secret created. Store it securely: it will not be shown again. The old secret no longer signs deliveries."
}

PUT /webhooks.php

Change a subscription

Scope: webhooks:write Idempotency-Key accepted

url, events, active or description. The same checks as create. Only the key that made the subscription, or a key that replaced it by rotation (directly or through later rotations), can change it: one made in the agency console (api_key_id null) or with any other key is 403 forbidden, and nothing changes. So after rotating a key, the new key can pause the old key's subscriptions. Accepts an Idempotency-Key header.

Query parameters

NameTypeDescription
id requiredintegerThe subscription.

JSON body: WebhookUpdate

FieldTypeDescription
url sometimesstring
events sometimeslist of string
active sometimesboolean
description sometimesstring or null

Reply: data is WebhookSubscription.

Errors: 400, 401, 403, 404, 422, 429, 500. See Errors for the codes.

Examples

Change the events 200

Key used: a key holding every scope.

Request

curl -s -X PUT "$TC_BASE/webhooks.php?id=31" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":["pay_week.locked","pay_week.unlocked","export.created","timesheet.approved"]}'

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 31,
    "url": "https://hooks.example.com/tempclock",
    "events": [
      "pay_week.locked",
      "pay_week.unlocked",
      "export.created",
      "timesheet.approved"
    ],
    "active": true,
    "description": "Payroll system",
    "api_key_id": 30,
    "secret": "68d11aea...",
    "created_at": "2026-10-02 07:33:40",
    "created_at_utc": "2026-10-02T06:33:40Z",
    "updated_at": "2026-10-02 07:33:40",
    "updated_at_utc": "2026-10-02T06:33:40Z"
  },
  "message": "Webhook subscription updated."
}

DELETE /webhooks.php

Delete a subscription

Scope: webhooks:write

Its delivery log goes too. Only the key that made the subscription, or a key that replaced it by rotation (directly or through later rotations), can delete it: one made in the agency console (api_key_id null) or with any other key is 403 forbidden, and nothing changes. So after rotating a key, the new key can delete the old key's subscriptions, which stop when the old key's overlap ends.

Query parameters

NameTypeDescription
id requiredintegerThe subscription.

Errors: 400, 401, 403, 404, 429, 500. See Errors for the codes.

Examples

Delete a subscription 200

Key used: a key holding every scope.

Request

curl -s -X DELETE "$TC_BASE/webhooks.php?id=31" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "message": "Webhook subscription deleted."
}

Objects

The fields of every object the API returns or accepts. sometimes marks a field that is only there in some replies (the description says when, usually a scope). New fields can be added at any time: ignore fields you do not know.

Me

The key making the request. Never returns the key or its hash.

FieldTypeDescription
api_key_idintegerThe key id, as shown in the audit log ("API key #N").
key_namestring
key_prefixstringThe first 16 characters (12 for keys made before v1.3), as shown on the key list.
kindstring: clientThe key kind. Only 'client' in v1.3 (an agency-wide key is planned).
client_idintegerThe client account the key belongs to.
client_idslist of integerEvery client the key may see. Always [client_id] in v1.3.
client_namestring or null
site_limitedbooleanTrue when the key is limited to some sites.
location_idslist of integer or nullThe sites of a site-limited key, else null (every site of the client).
locationslist of MeLocationThe sites the key can see.
scopeslist of stringThe scopes stored on the key (empty for a legacy key).
effective_scopeslist of stringEvery scope the key really has, after the legacy, write-includes-read and payroll-includes-pay rules.
legacybooleanTrue for a key made before scopes existed: fixed to the v1.1 scope set.
rate_limit_per_minuteinteger
allowed_originslist of stringBrowser origins allowed to call the API with this key (CORS). Empty = server to server only.
expires_atlocal time or nullExpiry, or null for none.
expires_at_utcUTC time or nullexpires_at in UTC.
created_atlocal time or null
created_at_utcUTC time or null
api_versionstringThe API minor version serving the request (also the X-API-Version header).

MeLocation

FieldTypeDescription
idinteger
namestring
activeboolean

Location

A site.

FieldTypeDescription
idinteger
namestring
addressstring or null
citystring or null
postcodestring or null
capacityinteger or nullMost people allowed on site at once (fire register), or null.
operates_24x7boolean
activeboolean
latitudenumber or nullWGS84.
longitudenumber or nullWGS84.
geofence_radiusintegerMetres (default 200).
geofence_modestring: disabled, warning, strictdisabled = no check; warning = a clock outside the radius is allowed and flagged; strict = refused outside the radius.
kiosk_languagestring: en, pl, ro, lt or nullThe kiosk language, or null for English.
gps_accuracy_thresholdinteger or nullMetres. A worse GPS reading is flagged low_gps_confidence.
created_atlocal time
created_at_utcUTC time
billing_name sometimesstring or nullOnly with charges:read.
billing_contact sometimesstring or nullOnly with charges:read.
billing_email sometimesstring or nullOnly with charges:read.
po_reference sometimesstring or nullOnly with charges:read.
sage_account_ref sometimesstring or nullOnly with charges:read.
sage_department sometimesstring or nullOnly with charges:read.
nominal_code sometimesstring or nullOnly with charges:read.

Department

FieldTypeDescription
idinteger
namestring
descriptionstring or null
location_idinteger or nullThe site it belongs to, or null (every site).
location_namestring or null
globalbooleanTrue for a department shared by every client.
activeboolean
created_atlocal time
created_at_utcUTC time

CostCode

A cost code: the Sage cost centre / analysis key. Send its id as cost_code_id on a worker.

FieldTypeDescription
idinteger
codestring
descriptionstring or null
globalbooleanTrue for a cost code shared by every client.
activeboolean
created_atlocal time
created_at_utcUTC time

Worker

FieldTypeDescription
idinteger
namestring
employee_idstringThe badge number and payroll reference. Unique within your company (another company on TempClock may use the same number).
location_idinteger or nullHome site.
location_namestring or null
department_idinteger or null
department_namestring or null
cost_code_idinteger or null
cost_codestring or null
emailstring or null
phonestring or null
job_rolestring or null
hourly_rate sometimesnumber or nullPay rate. Only with pay:read; null = rate rules apply.
charge_rate sometimesnumber or nullCharge rate. Only with charges:read; null = rate rules apply.
max_hours_per_weeknumber or null
statusstring: active, pendingpending = registered themselves and not approved yet (cannot clock or be booked).
activebooleanFalse for a leaver.
dormantbooleanNot worked for a while; a clock sets it back to active.
dormant_sincelocal time or null
dormant_since_utcUTC time or null
start_datedate or null
end_datedate or null
leave_reasonstring or null
preferred_languagestring: en, pl, ro, lt or null
has_facebooleanFace enrolled for kiosk clocking.
has_pinbooleanA kiosk PIN is set.
created_atlocal time
created_at_utcUTC time
updated_atlocal time or nullLast change to the record (use with updated_since).
updated_at_utcUTC time or nullThe exact UTC of that change, right in the repeated hour when the clocks go back.

WorkerCreate

POST /workers.php body. The client always comes from the key.

FieldTypeDescription
namestringRequired, 1 to 100 characters.
employee_id sometimesstring1 to 32 letters, numbers, dashes or underscores. Generated when left out. 409 duplicate_employee_id when taken.
email sometimesstring or null
phone sometimesstring or null
job_role sometimesstring or null
location_id sometimesinteger or nullHome site. Required for a site-limited key.
department_id sometimesinteger or nullYour own or a global department.
cost_code_id sometimesinteger or null
start_date sometimesdate or null
end_date sometimesdate or null
preferred_language sometimesstring: en, pl, ro, lt or null
hourly_rate sometimesnumber or nullNeeds rates:write (403 insufficient_scope otherwise). 0 to 9999.99.
charge_rate sometimesnumber or nullNeeds rates:write.

WorkerUpdate

PUT /workers.php body: any of these. null (or an empty string) clears an optional field; name and employee_id cannot be cleared. active false is the leaver event, the same as the console (face data is removed by the offboarding job). An anonymised worker cannot be changed (409).

FieldTypeDescription
name sometimesstring
employee_id sometimesstring
email sometimesstring or null
phone sometimesstring or null
job_role sometimesstring or null
location_id sometimesinteger or null
department_id sometimesinteger or null
cost_code_id sometimesinteger or null
start_date sometimesdate or null
end_date sometimesdate or null
leave_reason sometimesstring or null
preferred_language sometimesstring: en, pl, ro, lt or null
hourly_rate sometimesnumber or nullNeeds rates:write.
charge_rate sometimesnumber or nullNeeds rates:write.
active sometimesboolean

TimeEntry

A time entry (a shift or a break). Times are Europe/London local time; every *_utc twin is ISO 8601 UTC.

FieldTypeDescription
idinteger
worker_idinteger
worker_namestring
employee_idstring or null
department_idinteger or nullThe worker's department.
location_idinteger or null
location_namestring or null
client_idinteger or nullThe client that owns the shift: the site's client, else the worker's home client.
entry_typestring: shift, break
clock_inlocal time
clock_outlocal time or nullnull while the shift is open.
clock_in_utcUTC time
clock_out_utcUTC time or null
total_minutesinteger or nullReal elapsed time, right across a clock change.
hoursnumber or nulltotal_minutes / 60.
paid_minutesinteger or nullFrom the pricing engine (breaks, rounding to the booking and manual adjustments applied). Completed shifts only.
paid_hoursnumber or null
unpaid_break_minutesinteger or null
clock_methodstring: face, pin, manual, api, mobile, backfill
clock_out_methodstring or nullHow the shift was closed (for example face, pin, manual, api, auto).
clock_sourcestring or nullWhere the clock came from (for example kiosk, app, api).
kiosk_idstring or null
notesstring or null
match_scorenumber or nullFace match score of a face clock.
cost_code_idinteger or null
scheduled_shift_idinteger or nullThe booking this clock is for.
approval_statusstring: pending, approved, disputedAgency approval.
approvedboolean
approved_atlocal time or null
approved_byinteger or nullAdmin user id.
client_approvalstring: pending, approved, rejectedClient sign-off in the portal.
client_approvedboolean or nullnull = not signed off yet.
client_approved_atlocal time or null
client_approved_byinteger or null
client_rejected_reasonstring or null
queriedbooleanA query was ever raised.
open_querybooleanA query is open: payroll holds the shift.
payroll_statusstring: payable, on_hold, excluded or nullCompleted shifts: what the Sage 50 payroll gate does with it. null for a break.
payroll_excluded_reasonstring: pending, client_pending, disputed, adjustment_pending, rejected, open_clock_out, walk_in, cancelled, no_pay_rate, paid_adjustment or nullWhy a shift is on_hold or excluded; null when payable.
lockedbooleanInside a locked pay period: it can no longer be edited.
exported_batch_idinteger or nullThe latest Sage or Xero file that held it (GET /export-batches.php?id=). Exporting the week again, or sending it to Xero after Sage, moves it to the newer file; each batch keeps its own list of shifts. A file made for every client at once (all_clients) is not served by the API (404 not_found) because it holds other clients' figures.
risk_flagslist of stringFor example api_backdated, locked_week, dormant_worker, offsite, low_gps_confidence.
manual_break_minutesinteger or null
pay_adjustment_minutesinteger or null
editedboolean
edit_countinteger
edited_atlocal time or null
original_clock_inlocal time or nullThe times before the first correction.
original_clock_outlocal time or null
within_geofenceinteger or null1 inside, 0 outside, null no GPS or no geofence.
distance_metersinteger or null
gps_accuracyinteger or null
low_gps_confidenceboolean or null
clock_out_within_geofenceinteger or null
clock_out_distance_metersinteger or null
created_atlocal time
updated_atlocal time or nullLast change (clock-out, approval, sign-off, correction, export).
updated_at_utcUTC time or null
pricing sometimesTimeEntryPricingOnly with include_pricing=true, on completed shifts.

TimeEntryPricing

The pricing engine result for one shift. Pay figures need pay:read, charge figures charges:read.

FieldTypeDescription
pay_amount sometimesnumberpay:read only.
charge_amount sometimesnumbercharges:read only.
pay_week_startdate or null
sourcestring: live, lockedlocked = the frozen figures of a closed pay week.
elementsobject of PricingElementBy element: regular, overtime, weekend, sunday, bank_holiday, night, guarantee.
flagslist of stringFor example walk_in_no_pay, no_pay_rate.

PricingElement

FieldTypeDescription
minutesnumber
pay sometimesnumberpay:read only.
charge sometimesnumbercharges:read only.

ClockResult

FieldTypeDescription
entry_idinteger
worker_idinteger
employee_idstring or null
worker_namestring
actionstring: clock_in, clock_out
clock_inlocal time
clock_out sometimeslocal timeClock-out only.
clock_in_utcUTC time
clock_out_utc sometimesUTC timeClock-out only.
total_minutes sometimesintegerClock-out only.
hours sometimesnumberClock-out only.
location_idinteger or null
scheduled_shift_idinteger or nullThe booking the clock was linked to.
risk_flagslist of string
backdatedbooleanoccurred_at was more than 60 seconds in the past: the entry is flagged api_backdated for review.
within_geofence sometimesinteger1 inside, 0 outside: when coordinates were sent and the site has a geofence.
distance_meters sometimesintegerMetres from the site.
low_gps_confidence sometimesbooleangps_accuracy is worse than the site threshold.
warning sometimesstringA warning-mode geofence breach.

ClockRequest

POST /clock.php body.

FieldTypeDescription
worker_id sometimesintegerThe worker, or
employee_id sometimesstringtheir employee ID.
actionstring: in, out
location_id sometimesintegerClock-in: default the booked shift's site, else the worker's home site. Clock-out always uses the open shift's site.
scheduled_shift_id sometimesintegerClock-in: the booking this clock is for. Default: the booking that covers the clock time.
occurred_at sometimesstringWhen it happened (ISO 8601 with Z or an offset, or local time). At most 15 minutes in the past and 60 seconds in the future. More than 60 seconds in the past flags the entry api_backdated.
notes sometimesstringAt most 255 characters. A clock-out note is added after the clock-in note.
latitude sometimesnumber
longitude sometimesnumberBoth needed at a strict-geofence site (403 gps_required). A strict site with no map pin refuses every clock (403 site_not_mapped).
gps_accuracy sometimesnumberMetres.
survey_id sometimesinteger
survey_answers sometimeslist of SurveyAnswerA blocking answer is 403 blocked_by_survey.

SurveyAnswer

FieldTypeDescription
question_idinteger
answer_valuestring

OnSiteNow

FieldTypeDescription
as_ofUTC time
totalsOnSiteTotals
locationslist of OnSiteLocation
unassignedlist of OnSiteWorkerWorkers clocked in with no site.

OnSiteTotals

FieldTypeDescription
clocked_ininteger
on_siteinteger
off_siteinteger
geofence_unknownintegerNo GPS, or geofence off: counted as on site.
low_gps_confidenceinteger

OnSiteLocation

FieldTypeDescription
location_idinteger
location_namestring
clocked_ininteger
on_siteinteger
off_siteinteger
geofence_unknowninteger
workers sometimeslist of OnSiteWorkerLeft out with include_workers=false.

OnSiteWorker

FieldTypeDescription
worker_idinteger
namestring
employee_idstring or null
entry_idinteger
clock_inlocal time
clock_in_utcUTC time
minutes_on_shiftinteger
within_geofenceinteger or null1 inside, 0 outside, null unknown.
distance_metersinteger or null
gps_accuracyinteger or null
low_gps_confidenceboolean

Exception

One check from the Exceptions page.

FieldTypeDescription
categorystring: late, no_show, early, missing_clockout, excessive, missed_break, dispute, not_approved, flagged, locked_week, offline_conflict
typestring: late_start, no_show, early_finish, missing_clockout, long_shift, overtime, missed_break, client_dispute, not_approved, flagged_clock, locked_week, offline_conflict
entity_typestringWhat the exception is about (for example shift, entry, worker_week).
entity_idinteger
period_keystring or nullThe week, for weekly checks.
worker_idinteger or null
worker_namestring or null
employee_idstring or null
location_namestring or null
datedate or null
bookedstring or nullThe booked times, for example 08:00-16:00.
entry_idinteger or null
shift_idinteger or null
clock_inlocal time or null
clock_outlocal time or null
clock_in_utcUTC time or null
clock_out_utcUTC time or null
minutesinteger or null
detailstringPlain English, no pay figures.
labelstring or null
resolutionobject or nullOnce dismissed or settled (include_resolved=true).

ExceptionResolution

FieldTypeDescription
resolutionstring or null
notesstring or null
resolved_atlocal time or null

Amendment

A change a client asked for on a shift from the portal, and what the agency decided.

FieldTypeDescription
idinteger
entry_idinteger
worker_idinteger
worker_namestring
employee_idstring or null
location_idinteger or null
location_namestring or null
statusstring: pending, accepted, declined, superseded, withdrawn
reasonstring or null
proposedAmendmentTimesnull values: no change asked for.
currentAmendmentTimesThe shift as it is now.
changesobjectOnly the values that differ from the shift now. The times are UK local, with no UTC twin (proposed has them).
proposed_bystringportal for a client user.
created_atlocal time
created_at_utcUTC time
resolved_atlocal time or null
resolved_at_utcUTC time or null
resolved_bystring or null
resolution_notestring or null

AmendmentTimes

FieldTypeDescription
clock_inlocal time or null
clock_outlocal time or null
break_minutesinteger or null
clock_in_utcUTC time or null
clock_out_utcUTC time or null

PayWeek

Where one pay week stands for this client.

FieldTypeDescription
week_startdate
week_enddate
statusstring: open, checking, ready_to_lock, locked, exportedopen = not ended; checking = ended, shifts still to sort out; ready_to_lock; locked; exported = locked and a Sage file made.
endedboolean
lockedboolean
locked_atlocal time or null
locked_at_utcUTC time or null
locked_bystring or nullName of the person who locked it.
lock_idinteger or null
frozenbooleanFrozen figures are stored: GET /payroll.php returns them.
shiftsintegerCompleted shifts in the week at this client's sites.
shifts_not_approvedintegerCompleted shifts the payroll gate leaves out for an approval reason (not approved, waiting for client sign-off, disputed or queried, rejected).
open_clock_outsinteger
exceptions_openintegerShifts that stop the week being locked, each counted once (0 once locked).
late_adjustments_dueintegerClocks that landed in an earlier locked week, due to be paid as an adjustment in this week.
late_adjustments_paidinteger
late_clocks_in_weekintegerClocks recorded in this week after it was locked.
batcheslist of ExportBatchExport files made for this client that cover the week.

PayWeeksMeta

FieldTypeDescription
period_fromdate
period_todate
pay_week_start_dayinteger1 = Monday ... 7 = Sunday.
approval_rulestring: agency_approved, agency_and_client_approvedagency_and_client_approved when the client must also sign off hours before payroll.

ExportBatch

A payroll or accounts file made for this client.

FieldTypeDescription
idinteger
kindstring: sage, xero, payroll
formatstring or nullFor example sage50.
client_idinteger or null
period_startdate
period_enddate
entry_countinteger
line_countinteger or null
file_namestring or null
file_sha256string or nullSHA-256 of the exact file kept on the server.
reasonstring or nullGiven when a period was exported again.
created_atlocal time or null
created_at_utcUTC time or null
stamps_entriesbooleanTrue for Sage and Xero files: each shift in the file carries this batch id as exported_batch_id until a later Sage or Xero file of it replaces it. A payroll file does not stamp shifts. Every kind lists its shifts on GET ?id= (entry_ids).
total_pay sometimesnumberpay:read, and not for a site-limited key.
total_charge sometimesnumbercharges:read, and not for a site-limited key.

ExportBatchDetail

An export batch with the shifts it held. Every field of ExportBatch, plus:

FieldTypeDescription
entry_idslist of integerThe shifts the file held, recorded when the batch was made (exporting the week again or sending it to Xero after Sage does not change it), in this key's view, owned by the client of the frozen week. Sage, Xero and payroll files alike.
file_okbooleanThe stored file is present and matches its sha256.

PayrollLine

Approved, priced hours for one worker in one pay week.

FieldTypeDescription
worker_idinteger
employee_idstring or null
worker_namestring
pay_week_startdate
pay_week_enddate
shiftsinteger
paid_minutesinteger
paid_hoursnumber
gross_paynumber
charge_total sometimesnumbercharges:read only.
margin sometimesnumbercharges:read only.
base_pay_ratenumber or null
departmentstring or nullThe Sage department (analysis value) of the line.
cost_centrestring or null
elementsobject of PayrollBucketThe five legacy buckets (regular, overtime, weekend, bank_holiday, night; Sunday is in weekend and guarantee in regular), kept for existing integrations.
pay_elementslist of PayElementThe Sage 50 Payroll file's lines for this worker and week.
sourcestring: live, lockedlocked = the frozen figures of a closed week.
flagslist of string
entry_ids sometimeslist of integerinclude_entries=1 only.

PayrollBucket

FieldTypeDescription
minutesnumber
hoursnumber
paynumber
pay_ratenumber or null
charge sometimesnumbercharges:read only.
charge_rate sometimesnumber or nullcharges:read only.

PayElement

One line of the Sage 50 Payroll file.

FieldTypeDescription
elementstring: regular, overtime, weekend, sunday, bank_holiday, night, guarantee
labelstring
sage_refstringYour Sage payment reference for the element.
minutesnumber
hoursnumber
pay_ratenumber or null
paynumber
departmentstring or null
cost_centrestring or null
charge sometimesnumbercharges:read only.
entry_ids sometimeslist of integerinclude_entries=1 only.

PayrollAdjustment

A clock that landed in a locked week, paid as an adjustment in a later week.

FieldTypeDescription
entry_idinteger
worker_idinteger
worker_namestring
employee_idstring
location_idinteger or null
location_namestring or null
clock_inlocal time
clock_outlocal time or null
clock_in_utcUTC time
clock_out_utcUTC time or null
week_startdateThe locked week it belongs to.
adjust_weekdateThe week it is (or was) paid in.
statestring: due, week_unlocked, paid_adjustment, paid_own_week
sourcestring or nullkiosk, api, manual ...
detected_atlocal time or null
resolved_atlocal time or null
resolved_weekdate or null
paid_minutesinteger or null
pay sometimesnumber or nullpay:read only.
charge sometimesnumber or nullcharges:read only.

PayrollMeta

FieldTypeDescription
period_fromdate
period_todate
pay_week_start_dayinteger
approval_rulestring: agency_approved, agency_and_client_approvedagency_approved: a shift is paid once the agency approves it, unless the client rejected or disputed it or a query is open. agency_and_client_approved: the client must also sign it off.
shifts_not_approvedintegerCompleted shifts in the period left out for an approval reason: not approved, waiting for client sign-off, rejected by the client, disputed, or held by an open query.
currencystring
weekslist of PayrollWeek
sage_refsobject of stringYour Sage payment reference per element (empty = folded into another element, as in the Sage file).

PayrollWeek

FieldTypeDescription
week_startdate
week_enddate
statusstring: open, checking, ready_to_lock, locked, exported
lockedboolean
locked_atlocal time or null
locked_at_utcUTC time or null
shifts_not_approvedinteger
exceptions_openinteger
late_adjustments_dueinteger

Charges

The client charge schedule: what the agency bills for labour, per site, worker, role and pay element.

FieldTypeDescription
referencestring
client_idinteger
fromdate
todate
frozenbooleanEvery week in the period is locked: the frozen charge lines are returned.
partly_frozenboolean
require_client_approvalboolean
currencystring
lineslist of ChargeLine
siteslist of ChargeSite
totalsChargeTotals
weekslist of ChargeWeek
heldobject of ChargeHeldShifts left out, by reason.
warningslist of ChargeWarning

ChargeLine

FieldTypeDescription
site_idinteger
site_namestring
worker_idinteger
worker_namestring
employee_idstring or null
rolestring or null
cost_codestring or null
postring or null
elementstring: regular, overtime, weekend, sunday, bank_holiday, night, guarantee
element_labelstring
hoursnumber
ratenumber
amountnumber
shiftsinteger
first_datedate
last_datedate
frozenboolean
no_chargeboolean
entry_ids sometimeslist of integerinclude_entries=true only.

ChargeSite

FieldTypeDescription
site_idinteger
site_namestring
billing_namestring or null
postring or null
sage_account_refstring or null
sage_departmentstring or null
nominal_codestring or null
hoursnumber
amountnumber
vatnumber
elementsobject of ChargeSiteElement

ChargeSiteElement

FieldTypeDescription
hoursnumber
amountnumber

ChargeTotals

FieldTypeDescription
hoursnumber
netnumber
vat_ratenumber
vatnumber
grossnumber
shiftsinteger
workersinteger
linesinteger

ChargeWeek

FieldTypeDescription
week_startdate
week_enddate
lockedboolean
locked_atlocal time or null
locked_at_utcUTC time or null

ChargeHeld

FieldTypeDescription
labelstring
countinteger
amountnumber

ChargeWarning

FieldTypeDescription
codestring: no_charge_rate, not_in_snapshot, zero_hours, not_closed, draft
levelstring: error, warning, info
messagestring
entry_idslist of integer

RateRule

How shifts are priced. A rate left blank is null (the multiplier, or the worker's own rate, applies).

FieldTypeDescription
idinteger
namestring
client_idinteger or nullnull = every client.
location_idinteger or nullnull = every site.
job_rolestring or null
cost_code_idinteger or null
valid_fromdate or null
valid_todate or null
priorityinteger
activeboolean
overtime_thresholdnumber or nullWeekly hours before overtime.
overtime_multipliernumber or null
overtime_tierslist of any or nullExtra overtime tiers, as set on the rule.
daily_overtime_thresholdnumber or null
daily_overtime_multipliernumber or null
weekend_multipliernumber or null
sunday_multipliernumber or null
bank_holiday_multipliernumber or null
guarantee_hoursnumber or null
paid_break_minutesinteger or null
auto_lunch_minutesinteger or null
auto_lunch_after_hoursnumber or null
pay sometimesRateRulePaypay:read only.
charge sometimesRateRuleChargecharges:read only.
night_bandslist of NightBand

RateRulePay

FieldTypeDescription
pay_ratenumber or null
overtime_pay_ratenumber or null
daily_overtime_pay_ratenumber or null
weekend_pay_ratenumber or null
sunday_pay_ratenumber or null
bank_holiday_pay_ratenumber or null
pre_parity_pay_ratenumber or null
post_parity_pay_ratenumber or null

RateRuleCharge

FieldTypeDescription
charge_ratenumber or null
overtime_charge_ratenumber or null
daily_overtime_charge_ratenumber or null
weekend_charge_ratenumber or null
sunday_charge_ratenumber or null
bank_holiday_charge_ratenumber or null
pre_parity_charge_ratenumber or null
post_parity_charge_ratenumber or null

NightBand

FieldTypeDescription
start_timetime HH:MM
end_timetime HH:MM
uplift_multipliernumber or null
charge_uplift_multiplier sometimesnumber or nullcharges:read only.
pay_rate sometimesnumber or nullpay:read only.
charge_rate sometimesnumber or nullcharges:read only.

RateRulesMeta

FieldTypeDescription
settingsRateSettings
currency sometimesstringThe list only.

RateSettings

Engine settings that change how rules combine.

FieldTypeDescription
premium_stackingstring
premium_day_basisstring
site_overtime_modestring
walk_in_policystringHow a clock with no booking is paid.
cancelled_policystringHow a clock on a cancelled booking is paid.
multi_shift_prioritystring
pay_week_start_dayinteger

Shift

A booking. A booking belongs to the client that owns its site, else the worker's home client.

FieldTypeDescription
idinteger
client_idinteger
worker_idinteger
worker_namestring
employee_idstring or null
department_idinteger or null
location_idinteger or null
location_namestring or null
demand_idinteger or nullThe requirement it fills.
shift_datedate
start_timetime HH:MM:SS
end_timetime HH:MM:SSBefore start_time = an overnight shift.
start_utcUTC time
end_utcUTC time
break_minutesinteger
job_rolestring or null
notesstring or null
statusstring: draft, published, acknowledged, completed, swapped, cancelled, client_cancelled, worker_cancelled, sick, awol, no_showAn older booking stored with a blank status is returned as cancelled.
cancel_reasonstring or null
cancelled_atlocal time or null
acknowledged_atlocal time or null
declined_atlocal time or null
confirmationstring: confirmed, declined, unconfirmed, n/aWhether the worker confirmed it.
created_atlocal time
created_at_utcUTC time
updated_atlocal time or null
updated_at_utcUTC time or null
warning_codes sometimeslist of stringCreate and change only: eligibility warnings that did not block it (for example outside_pattern, short_rest, over_hours, role_mismatch).
detached_demand_id sometimesintegerChange only: the requirement it no longer fills after a move.

ShiftCreate

POST /shifts.php body. Either the slot, or demand_id to book onto a requirement (its site, date, times, break and role are used).

FieldTypeDescription
worker_idintegerYour own worker (based at one of a site-limited key's sites).
demand_id sometimesinteger
location_id sometimesintegerRequired without demand_id.
shift_date sometimesdate
start_time sometimesstringHH:MM
end_time sometimesstringHH:MM
break_minutes sometimesinteger
job_role sometimesstring
status sometimesstring: draft, publishedDefault draft.
notes sometimesstring

ShiftUpdate

PUT /shifts.php body: any of these. A worked booking, or one in a locked pay week, can only have its notes changed (409 conflict, reason_code worked or week_locked).

FieldTypeDescription
worker_id sometimesinteger
location_id sometimesinteger
shift_date sometimesdate
start_time sometimesstring
end_time sometimesstring
break_minutes sometimesinteger
job_role sometimesstring or null
notes sometimesstring or null
status sometimesstring: draft, published

ShiftCancel

Cancel body. The status drives the cancelled-booking pay policy and the reliability score.

FieldTypeDescription
status sometimesstring: cancelled, client_cancelled, worker_cancelled, sick, awol, no_showDefault cancelled.
cancel_reason sometimesstring

Demand

A requirement: N people for a site and time. updated_at moves when the requirement changes and when a booking on it is added, cancelled, moved, re-timed or detached, even if status stays the same, so a change to filled or open is caught by updated_since.

FieldTypeDescription
idinteger
client_idinteger
location_idinteger
location_namestring or null
shift_datedate
start_timetime HH:MM:SS
end_timetime HH:MM:SS
start_utcUTC time
end_utcUTC time
break_minutesinteger
job_rolestring or null
headcountintegerPlaces needed.
requestedinteger or nullWhat the client first asked for.
filledintegerLive bookings.
openintegerheadcount - filled, never below 0.
statusstring: open, partial, filled, cancelled
series_idinteger or nullRepeating requirements share one.
sourcestring: admin, portal, auto, copy, pattern, api
notesstring or null
cancel_reasonstring or null
cancelled_atlocal time or null
created_atlocal time
created_at_utcUTC time
updated_atlocal time or nullLast change to the requirement or to a booking on it.
updated_at_utcUTC time or nullThe exact UTC of that change, right in the repeated hour when the clocks go back.
bookings sometimeslist of DemandBookingGET ?id= only: the live bookings.

DemandBooking

FieldTypeDescription
shift_idinteger
worker_idinteger
worker_namestring
employee_idstring or null
statusstring
confirmationstring: confirmed, declined, unconfirmed, n/a

DemandCreate

FieldTypeDescription
location_idinteger
shift_datedate
start_timestringHH:MM
end_timestringHH:MM
headcount sometimesinteger1 to 500 (default 1).
break_minutes sometimesinteger
job_role sometimesstring
notes sometimesstring

Offer

FieldTypeDescription
idinteger
demand_idinteger
worker_idinteger
worker_namestring
employee_idstring or null
statusstring: sent, accepted, declined, filled, expiredAn offer still marked sent after it expired reads expired.
viastring or nullsms, email, app ...
sent_atlocal time
sent_at_utcUTC time
responded_atlocal time or null
responded_at_utcUTC time or null
expires_atlocal time or null
expires_at_utcUTC time or null
shift_idinteger or nullThe booking made when it was accepted.
cover_for_shift_idinteger or null

AvailabilityRule

FieldTypeDescription
idinteger
worker_idinteger
rule_typestring: weekly, rolling, exception
dayslist of integer1 = Monday ... 7 = Sunday (weekly rules).
days_labelstring or null
start_timetime HH:MM or nullnull = all day.
end_timetime HH:MM or null
on_daysinteger or null
off_daysinteger or nullRolling rules.
effective_fromdate or null
effective_todate or null
activebooleanFalse for a retired rule (include_retired=true).
created_atlocal time or null
created_at_utcUTC time or null

AvailabilityRules

FieldTypeDescription
worker_idinteger
ruleslist of AvailabilityRuleThe worker's active rules.

AvailabilityCreate

POST /availability.php body.

FieldTypeDescription
worker_idinteger
typestring: weekly, rolling, exception
slots sometimeslist of AvailabilitySlotweekly: replaces the weekly pattern. An empty list clears it.
on_days sometimesintegerrolling: 1 to 60.
off_days sometimesintegerrolling: 0 to 60.
effective_from sometimesdateDefault today.
effective_to sometimesdate
start_time sometimesstringrolling or exception: HH:MM.
end_time sometimesstring
reason sometimesstringexception: kept for the agency, never returned.

AvailabilitySlot

FieldTypeDescription
daysany[1,2,3] or text such as "Mon-Fri".
start_time sometimesstringHH:MM
end_time sometimesstring

AvailabilityMatch

One worker in a "who is free" answer, best matches first. Codes, never reason text.

FieldTypeDescription
worker_idinteger
namestring
employee_idstring or null
location_idinteger or null
department_idinteger or null
job_rolestring or null
eligiblebooleanNo block codes.
availableboolean or nullInside the worker's pattern; null = unknown (no pattern).
availabilitylist of stringThe pattern windows that cover the slot.
role_matchboolean or nullnull when no job_role was asked for.
week_hoursnumberHours that pay week at every site: clocked on days before today (booked where nothing was clocked), booked from today.
max_hours_per_weeknumber or nullThe worker's weekly hours limit.
warning_codeslist of stringFor example outside_pattern, short_rest, over_hours, role_mismatch, absence_pending.
block_codeslist of stringOnly with include_unavailable=true: inactive, pending, dormant, suspended, dnr, absence, unavailable, overlap, hours_limit (over a hard weekly hours limit).

AvailabilityMeta

FieldTypeDescription
slotobjectThe slot that was checked.

Absence

A holiday or sickness request. The worker's reason and the reviewer's notes are never returned.

FieldTypeDescription
idinteger
worker_idinteger
worker_namestring
employee_idstring or null
location_idinteger or nullThe worker's home site.
typestring: holiday, sick, unpaid, compassionate, training, other
start_datedate
end_datedate
daysnumber
statusstring: pending, approved, rejected, cancelled
reviewed_atlocal time or null
reviewed_at_utcUTC time or null
created_atlocal time
created_at_utcUTC time

WebhookSubscription

FieldTypeDescription
idinteger
urlstring
eventslist of stringEvent names, or ["*"] for every event.
activeboolean
descriptionstring or null
api_key_idinteger or nullThe key that made it (null = made in the agency console). Only that key, or a key that replaced it by rotation (directly or through later rotations), can change, test, rotate the secret of or delete it (any other key gets 403 forbidden; it can still read it). It follows the key that made it: if that key is revoked, expires or loses the read scope an event needs, nothing more is sent.
secretstringThe signing secret: in full when created or rotated, otherwise its first 8 characters and "...".
created_atlocal time
created_at_utcUTC time
updated_atlocal time or null
updated_at_utcUTC time or null
recent_deliveries sometimeslist of WebhookDeliveryGET ?id= only: the 10 latest.

WebhookCreate

A new subscription is always active. To pause it, send PUT ?id= with active: false.

FieldTypeDescription
urlstringA public HTTPS address on port 443.
eventslist of stringNames from GET ?action=events, or ["*"]. Each needs the read scope for its data (see Webhooks).
description sometimesstringUp to 255 characters.

WebhookUpdate

FieldTypeDescription
url sometimesstring
events sometimeslist of string
active sometimesboolean
description sometimesstring or null

WebhookDelivery

FieldTypeDescription
idintegerThe delivery id (the X-TempClock-Delivery header). The same across retries.
eventstring
statusstring: delivered, pending, failed
attemptinteger
max_attemptsinteger
response_codeinteger or null0 = not reached (URL guard or network).
delivered_atlocal time or null
delivered_at_utcUTC time or null
next_retry_atlocal time or null
next_retry_at_utcUTC time or null
created_atlocal time
created_at_utcUTC time
response_body sometimesstring or nullDelivery log only: the first 1 KB of what the endpoint answered.
payload sometimesanyDelivery log only: the exact JSON sent.
payload_hidden sometimesstringInstead of payload, when this key could not subscribe to the event.

WebhookEvent

FieldTypeDescription
eventstring
groupstring
descriptionstring
legacyboolean

WebhookTestResult

FieldTypeDescription
subscription_idinteger
delivery_idinteger
deliveredboolean
response_codeinteger or nullWhat the endpoint answered; 0 = not reached.
errorstring or nullWhy it was not delivered.

WebhookSecret

FieldTypeDescription
idinteger
secretstring64 hex characters, shown once.

WebhookPayload

The JSON body POSTed to a subscriber. X-TempClock-Signature is sha256= followed by the hex HMAC-SHA256 of the exact body, keyed with the subscription secret.

FieldTypeDescription
eventstring
timestampUTC time
dataobject of anyEvent data, with location_id where there is one. client_id (the client that owns the site worked, else the worker's home client) is on every event except clock events from the kiosk and the worker app (source kiosk or kiosk_offline): in v1.3 those go to the worker's home client and carry no client_id, *_utc twins or risk_flags.
delivery_id sometimesinteger

Pagination

FieldTypeDescription
totalintegerRows matching the filters.
limitintegerRows per page (default 50).
offsetintegerRows skipped.
has_morebooleanTrue when offset + limit < total.

Changelog

Additive changes ship in v1 without notice; anything that removes or renames goes to /api/v2 (see Versioning). Each change is marked additive or action when an integration may need to do something.

v1.3.0, October 2026

Change
additiveNew endpoints: /me.php, /pay-weeks.php, /export-batches.php, /charges.php, /rate-rules.php, /exceptions.php, /amendments.php, /demands.php, /offers.php, /availability.php, /absences.php.
additiveBookings: PUT /shifts.php changes a booking and POST /shifts.php?id=&action=cancel cancels one or records sickness or a no-show; POST /demands.php creates a requirement and POST /shifts.php with demand_id books onto it.
additivePayroll: pay_elements (the Sage 50 file's lines), meta.weeks, adjustments=1 and a department_id filter on /payroll.php.
additiveTimesheets: payroll_status and payroll_excluded_reason, open_query, risk_flags, locked, exported_batch_id, original times, client_id, and updated_since, department_id and payroll_status filters.
additiveWorkers: department_id, cost_code_id, phone, status, dormant, start and end dates, updated_at, and updated_since, department_id, status and dormant filters. active=true and active=false now filter as expected.
additiveClock and booking times and most other record times have an ISO 8601 UTC *_utc twin (the times without one are listed under Dates and times). Every error has an error_code; every reply has X-Request-Id and X-API-Version.
additiveIdempotency-Key on POST and PUT. occurred_at, employee_id and scheduled_shift_id on /clock.php.
additiveWebhook events for approvals, client sign-off, amendments, pay week and period locks, exports, bookings, no-shows, time corrections and workers, sent from the consoles, the portal and the scheduled jobs as well as the API. Webhook delivery log, targeted test and secret rotation on /webhooks.php.
actionPay and charges have their own scopes. Existing keys that could read workers or timesheets were given pay:read and charges:read automatically, so they see what they saw before. A key that only holds payroll:read now needs charges:read for charge_total and margin on payroll lines. On a new key, Pay, Charges and Set rates start at No access.
actionKeys made before scopes existed are fixed to the v1.1 scope set and never gain new scopes. Give them the new scopes they need in Setup > API keys.
actionBrowser (CORS) access is off unless a key lists the origin. Server to server use is not affected.
actionTime records belong to the client that owns the site worked (the payroll rule) on /timesheets.php, /shifts.php and /on-site-now.php: a pooled worker's shift is listed for the site's client, not the worker's home client.
actionA key changes only the webhook subscriptions it made, and those made with a key it replaced by rotation: PUT, DELETE, action=test and action=rotate_secret on a subscription made in the agency console or with any other key answer 403 forbidden. It can still read them.
additiveWebhooks: approvals, corrections, deletes, queries, manual clocks, bookings and worker changes made in the agency console and the client console send the same events as the API and the portal. Booking changes workers make (taking or declining an offer, confirming) and cover do not send shift events yet. A lock on one site sends period.locked (scope site). clock.geofence_refused carries data.action from the kiosk and the worker app too, and a clock-out is checked against the fence as well as a clock-in. Where each event comes from lists every source and field.
additiveFixes: /clock.php answers no_open_clock_in and already_clocked_in before any site, survey or geofence check, so neither is recorded as a refusal. updated_since on /workers.php, /shifts.php and /demands.php is exact on the night the clocks go back, and a booking change moves its requirement's updated_at. /export-batches.php?id= lists the shifts each file held (payroll files too), unchanged by a later export. status=cancelled on /shifts.php includes older bookings stored with a blank status.
additiveFixes: /departments.php and /cost-codes.php answered 500; /on-site-now.php uses the 18-hour rule of the kiosk; two clock-ins at once can no longer open two shifts; a clock-out note no longer replaces the clock-in note; geofence_mode is documented as it is stored (disabled, warning, strict).

v1.2.1, October 2026

  • /payroll.php pays exactly the shifts the Sage 50 Payroll export pays: the same approval gate, site-client rule and totals. additive
  • Clock-outs in the repeated hour when the clocks go back keep the real time worked; clocks into a locked pay week are recorded and flagged for payroll. additive
  • Dormant workers cannot be booked. Workers created or changed with a key are written to the audit log under that key, with rate changes before and after. additive

v1.2, September 2026

  • /payroll.php; approval status and paid minutes on /timesheets.php; read and write scopes per resource. additive

v1.1, May 2026

  • GPS and geofencing on /clock.php, pre-clock-in surveys, /on-site-now.php, kiosk language, four clock webhook events. additive

v1.0, January 2026

  • First release: workers, timesheets, locations, clock, shifts, departments and cost codes, with bearer keys and a per-key rate limit.