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.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /me.php | The key making the request | any key |
Workers Worker records.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /workers.php | List workers, or one worker | workers:read |
| POST | /workers.php | Create a worker | workers:write |
| PUT | /workers.php | Change a worker | workers:write |
Lookups Sites, departments and cost codes.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /locations.php | List sites, or one site | locations:read |
| GET | /departments.php | List departments, or one department | departments:read |
| GET | /cost-codes.php | List cost codes, or one cost code | cost_codes:read |
Time Clocks, time entries, who is on site, exceptions and client amendments.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /timesheets.php | List time entries, or one | timesheets:read |
| POST | /clock.php | Clock a worker in or out | clock:write |
| GET | /on-site-now.php | Who is on site right now | onsite:read or timesheets:read |
| GET | /exceptions.php | Exceptions (the Exceptions page checks) | timesheets:read |
| GET | /amendments.php | Client amendment requests | timesheets:read |
Payroll Pay weeks, payroll lines, export batches, charges and rate rules.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /payroll.php | Payroll lines per worker per pay week | payroll:read |
| GET | /pay-weeks.php | Is the pay week final, and what was exported? | payroll:read |
| GET | /export-batches.php | Export batches (Sage, Xero, payroll files) | payroll:read |
| GET | /charges.php | Charge schedule (what the agency bills) | charges:read |
| GET | /rate-rules.php | Rate rules (read only) | pay:read or charges:read |
Bookings Shifts, requirements, offers, availability and absences.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /shifts.php | List bookings, or one booking | shifts:read |
| POST | /shifts.php | Book a worker, or cancel a booking | shifts:write |
| PUT | /shifts.php | Change a booking | shifts:write |
| GET | /demands.php | List requirements, or one with its bookings | shifts:read |
| POST | /demands.php | Create a requirement | shifts:write |
| GET | /offers.php | Shift offers sent to workers (read only) | shifts:read |
| GET | /availability.php | Availability rules, or who is free for a slot | availability:read |
| POST | /availability.php | Add an availability rule | availability:write |
| DELETE | /availability.php | Retire an availability rule | availability:write |
| GET | /absences.php | Holiday and sickness requests | absences:read |
Webhooks Subscriptions and deliveries.
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /webhooks.php | Subscriptions, one subscription, its delivery log, or the event list | webhooks:read |
| POST | /webhooks.php | Create a subscription, send a test, or rotate its secret | webhooks:write |
| PUT | /webhooks.php | Change a subscription | webhooks:write |
| DELETE | /webhooks.php | Delete a subscription | webhooks: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
| Name | Type | Description |
|---|---|---|
id | integer | One worker by id (the reply's data is then an object, not a list). |
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
department_id | integer | The worker's department. |
status | string: active, pending | pending = registered themselves and not approved yet. |
active | true or false (or 1, 0) | true or false (also 1 or 0). |
dormant | true or false (or 1, 0) | true = only dormant workers. |
updated_since | string | Only 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. |
search | string | Name, employee ID or email contains. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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.
| Field | Type | Description |
|---|---|---|
name | string | Required, 1 to 100 characters. |
employee_id sometimes | string | 1 to 32 letters, numbers, dashes or underscores. Generated when left out. 409 duplicate_employee_id when taken. |
email sometimes | string or null | |
phone sometimes | string or null | |
job_role sometimes | string or null | |
location_id sometimes | integer or null | Home site. Required for a site-limited key. |
department_id sometimes | integer or null | Your own or a global department. |
cost_code_id sometimes | integer or null | |
start_date sometimes | date or null | |
end_date sometimes | date or null | |
preferred_language sometimes | string: en, pl, ro, lt or null | |
hourly_rate sometimes | number or null | Needs rates:write (403 insufficient_scope otherwise). 0 to 9999.99. |
charge_rate sometimes | number or null | Needs 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
| Name | Type | Description |
|---|---|---|
id required | integer | The 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).
| Field | Type | Description |
|---|---|---|
name sometimes | string | |
employee_id sometimes | string | |
email sometimes | string or null | |
phone sometimes | string or null | |
job_role sometimes | string or null | |
location_id sometimes | integer or null | |
department_id sometimes | integer or null | |
cost_code_id sometimes | integer or null | |
start_date sometimes | date or null | |
end_date sometimes | date or null | |
leave_reason sometimes | string or null | |
preferred_language sometimes | string: en, pl, ro, lt or null | |
hourly_rate sometimes | number or null | Needs rates:write. |
charge_rate sometimes | number or null | Needs rates:write. |
active sometimes | boolean |
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
| Name | Type | Description |
|---|---|---|
id | integer | One site by id (the reply's data is then an object, not a list). |
active | true or false (or 1, 0) | true or false (also 1 or 0). |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
id | integer | One department by id (the reply's data is then an object, not a list). |
active | true or false (or 1, 0) | true or false (also 1 or 0). |
location_id | integer | Departments of one site. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
id | integer | One cost code by id (the reply's data is then an object, not a list). |
active | true or false (or 1, 0) | true or false (also 1 or 0). |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
id | integer | One time entry by id (the reply's data is then an object, not a list). |
date_from | date | On the clock-in date, YYYY-MM-DD. |
date_to | date | On the clock-in date, YYYY-MM-DD. |
updated_since | string | Only 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_id | integer | One worker. |
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
department_id | integer | The worker's department. |
entry_filter | string: shifts, active, breaks, all | shifts (default) = completed shifts; active = open shifts clocked in within 18 hours (on site now); breaks; all. |
approval_status | string: pending, approved, disputed | Agency approval. |
client_approval | string: pending, approved, rejected | Client sign-off. |
payroll_status | string: payable, on_hold, excluded | Completed shifts only, at most 2,000 matching shifts per request (narrow with dates). |
include_pricing | true or false (or 1, 0) | Add the pricing block: pay figures need pay:read, charge figures charges:read (403 insufficient_scope with neither). |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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.
| Field | Type | Description |
|---|---|---|
worker_id sometimes | integer | The worker, or |
employee_id sometimes | string | their employee ID. |
action | string: in, out | |
location_id sometimes | integer | Clock-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 sometimes | integer | Clock-in: the booking this clock is for. Default: the booking that covers the clock time. |
occurred_at sometimes | string | When 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 sometimes | string | At most 255 characters. A clock-out note is added after the clock-in note. |
latitude sometimes | number | |
longitude sometimes | number | Both 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 sometimes | number | Metres. |
survey_id sometimes | integer | |
survey_answers sometimes | list of SurveyAnswer | A 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
| Name | Type | Description |
|---|---|---|
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
include_workers | true 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
| Name | Type | Description |
|---|---|---|
from | date | Default 7 days ago. |
to | date | Default today; at most 93 days after from. |
type | string | One category or one type. |
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
worker_id | integer | One worker. |
include_resolved | true or false (or 1, 0) | Also the ones dismissed or settled (default false). |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
id | integer | One request by id (the reply's data is then an object, not a list). |
status | string: pending, accepted, declined, superseded, withdrawn | Default: all. |
entry_id | integer | One time entry. |
worker_id | integer | One worker. |
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
from | date | On the date the request was made. |
to | date | On the date the request was made. |
updated_since | string | Only 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. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
week | date | Any date inside the pay week wanted. |
date_from | date | With date_to: a range, widened to whole pay weeks (at most 26). |
date_to | date | See date_from. |
worker_id | integer | One worker. |
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
department_id | integer | The worker's department. |
include_entries | true or false (or 1, 0) | 1 = list the time entry ids behind each line and pay element. |
adjustments | true or false (or 1, 0) | 1 = add adjustments[]: clocks that landed in a locked week, paid as an adjustment in a later week. |
limit | integer | Lines per page, 1 to 200 (default 50). |
offset | integer | Lines 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
| Name | Type | Description |
|---|---|---|
week_start | date | Any date inside the pay week wanted. |
from | date | With to: a range, widened to whole pay weeks (at most 26). |
to | date | See 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
| Name | Type | Description |
|---|---|---|
id | integer | One batch by id (the reply's data is then an object, not a list). |
from | date | Batches whose period overlaps from and to. |
to | date | See from. |
kind | string: sage, xero, payroll | One kind of file. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
week_start | date | Any date inside the pay week wanted. |
from | date | With to: any period up to 93 days. |
to | date | See from. |
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
vat | true or false (or 1, 0) | true = add VAT per site. |
vat_rate | number | VAT percentage, 0 to 100 (default 20). |
include_entries | true 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
| Name | Type | Description |
|---|---|---|
id | integer | One rule by id (the reply's data is then an object, not a list). |
location_id | integer | Rules for one site plus the client-wide ones. |
active | true or false (or 1, 0) | Default: active rules only. |
on | date | Rules valid on that date. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
id | integer | One booking by id (the reply's data is then an object, not a list). |
date_from | date | On shift_date. |
date_to | date | On shift_date. |
worker_id | integer | One worker. |
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
department_id | integer | The worker's department. |
demand_id | integer | Bookings of one requirement. |
status | string | One status (see Shift.status). status=cancelled also returns older bookings stored with a blank status, which are shown as cancelled. |
updated_since | string | Only 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. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
id | integer | The booking to cancel (with action=cancel). |
action | string: cancel | cancel = 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).
| Field | Type | Description |
|---|---|---|
worker_id | integer | Your own worker (based at one of a site-limited key's sites). |
demand_id sometimes | integer | |
location_id sometimes | integer | Required without demand_id. |
shift_date sometimes | date | |
start_time sometimes | string | HH:MM |
end_time sometimes | string | HH:MM |
break_minutes sometimes | integer | |
job_role sometimes | string | |
status sometimes | string: draft, published | Default draft. |
notes sometimes | string |
JSON body: ShiftCancel
Cancel body. The status drives the cancelled-booking pay policy and the reliability score.
| Field | Type | Description |
|---|---|---|
status sometimes | string: cancelled, client_cancelled, worker_cancelled, sick, awol, no_show | Default cancelled. |
cancel_reason sometimes | string |
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
| Name | Type | Description |
|---|---|---|
id required | integer | The 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).
| Field | Type | Description |
|---|---|---|
worker_id sometimes | integer | |
location_id sometimes | integer | |
shift_date sometimes | date | |
start_time sometimes | string | |
end_time sometimes | string | |
break_minutes sometimes | integer | |
job_role sometimes | string or null | |
notes sometimes | string or null | |
status sometimes | string: 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
| Name | Type | Description |
|---|---|---|
id | integer | One requirement by id (the reply's data is then an object, not a list). |
date_from | date | On shift_date. |
date_to | date | On shift_date. |
location_id | integer | One site. Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed. |
status | string: open, partial, filled, cancelled | One status. |
unfilled | true or false (or 1, 0) | true = open or partial. |
series_id | integer | One repeating series. |
job_role | string | One role. |
updated_since | string | Only 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. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Field | Type | Description |
|---|---|---|
location_id | integer | |
shift_date | date | |
start_time | string | HH:MM |
end_time | string | HH:MM |
headcount sometimes | integer | 1 to 500 (default 1). |
break_minutes sometimes | integer | |
job_role sometimes | string | |
notes sometimes | string |
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
| Name | Type | Description |
|---|---|---|
id | integer | One offer by id (the reply's data is then an object, not a list). |
demand_id | integer | Offers for one requirement. |
shift_id | integer | Cover offers for one booking. |
worker_id | integer | One worker. |
status | string: sent, accepted, declined, filled, expired | One status. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
worker_id | integer | One worker's rules. |
rule_type | string: weekly, rolling, exception | One kind of rule. |
include_retired | true or false (or 1, 0) | Also retired rules. |
available_from | string | Who is free: the slot start (ISO 8601 with an offset or Z, or local time). |
available_to | string | The 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_id | integer | Who is free: the slot's site (its bans and rest rule apply). |
job_role | string | Who is free: the role wanted. |
department_id | integer | Who is free: one department. |
include_unavailable | true or false (or 1, 0) | Who is free: also list workers who cannot take it, with block_codes. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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.
| Field | Type | Description |
|---|---|---|
worker_id | integer | |
type | string: weekly, rolling, exception | |
slots sometimes | list of AvailabilitySlot | weekly: replaces the weekly pattern. An empty list clears it. |
on_days sometimes | integer | rolling: 1 to 60. |
off_days sometimes | integer | rolling: 0 to 60. |
effective_from sometimes | date | Default today. |
effective_to sometimes | date | |
start_time sometimes | string | rolling or exception: HH:MM. |
end_time sometimes | string | |
reason sometimes | string | exception: 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
| Name | Type | Description |
|---|---|---|
id required | integer | The 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
| Name | Type | Description |
|---|---|---|
id | integer | One request by id (the reply's data is then an object, not a list). |
worker_id | integer | One worker. |
type | string: holiday, sick, unpaid, compassionate, training, other | One type. |
status | string: pending, approved, rejected, cancelled | One status. |
from | date | Requests that overlap from and to. |
to | date | See from. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
id | integer | One subscription. |
action | string: deliveries, events | deliveries (with id) or events. |
active | true or false (or 1, 0) | List: active or paused subscriptions. |
status | string: delivered, pending, failed | Delivery log: one status. |
event | string | Delivery log: one event. |
limit | integer | Rows per page, 1 to 100 (default 50). |
offset | integer | Rows 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
| Name | Type | Description |
|---|---|---|
id | integer | The subscription (with an action). |
action | string: test, rotate_secret | test or rotate_secret. |
JSON body: WebhookCreate
A new subscription is always active. To pause it, send PUT ?id= with active: false.
| Field | Type | Description |
|---|---|---|
url | string | A public HTTPS address on port 443. |
events | list of string | Names from GET ?action=events, or ["*"]. Each needs the read scope for its data (see Webhooks). |
description sometimes | string | Up 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
| Name | Type | Description |
|---|---|---|
id required | integer | The subscription. |
JSON body: WebhookUpdate
| Field | Type | Description |
|---|---|---|
url sometimes | string | |
events sometimes | list of string | |
active sometimes | boolean | |
description sometimes | string 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
| Name | Type | Description |
|---|---|---|
id required | integer | The 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.
| Field | Type | Description |
|---|---|---|
api_key_id | integer | The key id, as shown in the audit log ("API key #N"). |
key_name | string | |
key_prefix | string | The first 16 characters (12 for keys made before v1.3), as shown on the key list. |
kind | string: client | The key kind. Only 'client' in v1.3 (an agency-wide key is planned). |
client_id | integer | The client account the key belongs to. |
client_ids | list of integer | Every client the key may see. Always [client_id] in v1.3. |
client_name | string or null | |
site_limited | boolean | True when the key is limited to some sites. |
location_ids | list of integer or null | The sites of a site-limited key, else null (every site of the client). |
locations | list of MeLocation | The sites the key can see. |
scopes | list of string | The scopes stored on the key (empty for a legacy key). |
effective_scopes | list of string | Every scope the key really has, after the legacy, write-includes-read and payroll-includes-pay rules. |
legacy | boolean | True for a key made before scopes existed: fixed to the v1.1 scope set. |
rate_limit_per_minute | integer | |
allowed_origins | list of string | Browser origins allowed to call the API with this key (CORS). Empty = server to server only. |
expires_at | local time or null | Expiry, or null for none. |
expires_at_utc | UTC time or null | expires_at in UTC. |
created_at | local time or null | |
created_at_utc | UTC time or null | |
api_version | string | The API minor version serving the request (also the X-API-Version header). |
MeLocation
| Field | Type | Description |
|---|---|---|
id | integer | |
name | string | |
active | boolean |
Location
A site.
| Field | Type | Description |
|---|---|---|
id | integer | |
name | string | |
address | string or null | |
city | string or null | |
postcode | string or null | |
capacity | integer or null | Most people allowed on site at once (fire register), or null. |
operates_24x7 | boolean | |
active | boolean | |
latitude | number or null | WGS84. |
longitude | number or null | WGS84. |
geofence_radius | integer | Metres (default 200). |
geofence_mode | string: disabled, warning, strict | disabled = no check; warning = a clock outside the radius is allowed and flagged; strict = refused outside the radius. |
kiosk_language | string: en, pl, ro, lt or null | The kiosk language, or null for English. |
gps_accuracy_threshold | integer or null | Metres. A worse GPS reading is flagged low_gps_confidence. |
created_at | local time | |
created_at_utc | UTC time | |
billing_name sometimes | string or null | Only with charges:read. |
billing_contact sometimes | string or null | Only with charges:read. |
billing_email sometimes | string or null | Only with charges:read. |
po_reference sometimes | string or null | Only with charges:read. |
sage_account_ref sometimes | string or null | Only with charges:read. |
sage_department sometimes | string or null | Only with charges:read. |
nominal_code sometimes | string or null | Only with charges:read. |
Department
| Field | Type | Description |
|---|---|---|
id | integer | |
name | string | |
description | string or null | |
location_id | integer or null | The site it belongs to, or null (every site). |
location_name | string or null | |
global | boolean | True for a department shared by every client. |
active | boolean | |
created_at | local time | |
created_at_utc | UTC time |
CostCode
A cost code: the Sage cost centre / analysis key. Send its id as cost_code_id on a worker.
| Field | Type | Description |
|---|---|---|
id | integer | |
code | string | |
description | string or null | |
global | boolean | True for a cost code shared by every client. |
active | boolean | |
created_at | local time | |
created_at_utc | UTC time |
Worker
| Field | Type | Description |
|---|---|---|
id | integer | |
name | string | |
employee_id | string | The badge number and payroll reference. Unique within your company (another company on TempClock may use the same number). |
location_id | integer or null | Home site. |
location_name | string or null | |
department_id | integer or null | |
department_name | string or null | |
cost_code_id | integer or null | |
cost_code | string or null | |
email | string or null | |
phone | string or null | |
job_role | string or null | |
hourly_rate sometimes | number or null | Pay rate. Only with pay:read; null = rate rules apply. |
charge_rate sometimes | number or null | Charge rate. Only with charges:read; null = rate rules apply. |
max_hours_per_week | number or null | |
status | string: active, pending | pending = registered themselves and not approved yet (cannot clock or be booked). |
active | boolean | False for a leaver. |
dormant | boolean | Not worked for a while; a clock sets it back to active. |
dormant_since | local time or null | |
dormant_since_utc | UTC time or null | |
start_date | date or null | |
end_date | date or null | |
leave_reason | string or null | |
preferred_language | string: en, pl, ro, lt or null | |
has_face | boolean | Face enrolled for kiosk clocking. |
has_pin | boolean | A kiosk PIN is set. |
created_at | local time | |
created_at_utc | UTC time | |
updated_at | local time or null | Last change to the record (use with updated_since). |
updated_at_utc | UTC time or null | The 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.
| Field | Type | Description |
|---|---|---|
name | string | Required, 1 to 100 characters. |
employee_id sometimes | string | 1 to 32 letters, numbers, dashes or underscores. Generated when left out. 409 duplicate_employee_id when taken. |
email sometimes | string or null | |
phone sometimes | string or null | |
job_role sometimes | string or null | |
location_id sometimes | integer or null | Home site. Required for a site-limited key. |
department_id sometimes | integer or null | Your own or a global department. |
cost_code_id sometimes | integer or null | |
start_date sometimes | date or null | |
end_date sometimes | date or null | |
preferred_language sometimes | string: en, pl, ro, lt or null | |
hourly_rate sometimes | number or null | Needs rates:write (403 insufficient_scope otherwise). 0 to 9999.99. |
charge_rate sometimes | number or null | Needs 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).
| Field | Type | Description |
|---|---|---|
name sometimes | string | |
employee_id sometimes | string | |
email sometimes | string or null | |
phone sometimes | string or null | |
job_role sometimes | string or null | |
location_id sometimes | integer or null | |
department_id sometimes | integer or null | |
cost_code_id sometimes | integer or null | |
start_date sometimes | date or null | |
end_date sometimes | date or null | |
leave_reason sometimes | string or null | |
preferred_language sometimes | string: en, pl, ro, lt or null | |
hourly_rate sometimes | number or null | Needs rates:write. |
charge_rate sometimes | number or null | Needs rates:write. |
active sometimes | boolean |
TimeEntry
A time entry (a shift or a break). Times are Europe/London local time; every *_utc twin is ISO 8601 UTC.
| Field | Type | Description |
|---|---|---|
id | integer | |
worker_id | integer | |
worker_name | string | |
employee_id | string or null | |
department_id | integer or null | The worker's department. |
location_id | integer or null | |
location_name | string or null | |
client_id | integer or null | The client that owns the shift: the site's client, else the worker's home client. |
entry_type | string: shift, break | |
clock_in | local time | |
clock_out | local time or null | null while the shift is open. |
clock_in_utc | UTC time | |
clock_out_utc | UTC time or null | |
total_minutes | integer or null | Real elapsed time, right across a clock change. |
hours | number or null | total_minutes / 60. |
paid_minutes | integer or null | From the pricing engine (breaks, rounding to the booking and manual adjustments applied). Completed shifts only. |
paid_hours | number or null | |
unpaid_break_minutes | integer or null | |
clock_method | string: face, pin, manual, api, mobile, backfill | |
clock_out_method | string or null | How the shift was closed (for example face, pin, manual, api, auto). |
clock_source | string or null | Where the clock came from (for example kiosk, app, api). |
kiosk_id | string or null | |
notes | string or null | |
match_score | number or null | Face match score of a face clock. |
cost_code_id | integer or null | |
scheduled_shift_id | integer or null | The booking this clock is for. |
approval_status | string: pending, approved, disputed | Agency approval. |
approved | boolean | |
approved_at | local time or null | |
approved_by | integer or null | Admin user id. |
client_approval | string: pending, approved, rejected | Client sign-off in the portal. |
client_approved | boolean or null | null = not signed off yet. |
client_approved_at | local time or null | |
client_approved_by | integer or null | |
client_rejected_reason | string or null | |
queried | boolean | A query was ever raised. |
open_query | boolean | A query is open: payroll holds the shift. |
payroll_status | string: payable, on_hold, excluded or null | Completed shifts: what the Sage 50 payroll gate does with it. null for a break. |
payroll_excluded_reason | string: pending, client_pending, disputed, adjustment_pending, rejected, open_clock_out, walk_in, cancelled, no_pay_rate, paid_adjustment or null | Why a shift is on_hold or excluded; null when payable. |
locked | boolean | Inside a locked pay period: it can no longer be edited. |
exported_batch_id | integer or null | The 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_flags | list of string | For example api_backdated, locked_week, dormant_worker, offsite, low_gps_confidence. |
manual_break_minutes | integer or null | |
pay_adjustment_minutes | integer or null | |
edited | boolean | |
edit_count | integer | |
edited_at | local time or null | |
original_clock_in | local time or null | The times before the first correction. |
original_clock_out | local time or null | |
within_geofence | integer or null | 1 inside, 0 outside, null no GPS or no geofence. |
distance_meters | integer or null | |
gps_accuracy | integer or null | |
low_gps_confidence | boolean or null | |
clock_out_within_geofence | integer or null | |
clock_out_distance_meters | integer or null | |
created_at | local time | |
updated_at | local time or null | Last change (clock-out, approval, sign-off, correction, export). |
updated_at_utc | UTC time or null | |
pricing sometimes | TimeEntryPricing | Only with include_pricing=true, on completed shifts. |
TimeEntryPricing
The pricing engine result for one shift. Pay figures need pay:read, charge figures charges:read.
| Field | Type | Description |
|---|---|---|
pay_amount sometimes | number | pay:read only. |
charge_amount sometimes | number | charges:read only. |
pay_week_start | date or null | |
source | string: live, locked | locked = the frozen figures of a closed pay week. |
elements | object of PricingElement | By element: regular, overtime, weekend, sunday, bank_holiday, night, guarantee. |
flags | list of string | For example walk_in_no_pay, no_pay_rate. |
PricingElement
| Field | Type | Description |
|---|---|---|
minutes | number | |
pay sometimes | number | pay:read only. |
charge sometimes | number | charges:read only. |
ClockResult
| Field | Type | Description |
|---|---|---|
entry_id | integer | |
worker_id | integer | |
employee_id | string or null | |
worker_name | string | |
action | string: clock_in, clock_out | |
clock_in | local time | |
clock_out sometimes | local time | Clock-out only. |
clock_in_utc | UTC time | |
clock_out_utc sometimes | UTC time | Clock-out only. |
total_minutes sometimes | integer | Clock-out only. |
hours sometimes | number | Clock-out only. |
location_id | integer or null | |
scheduled_shift_id | integer or null | The booking the clock was linked to. |
risk_flags | list of string | |
backdated | boolean | occurred_at was more than 60 seconds in the past: the entry is flagged api_backdated for review. |
within_geofence sometimes | integer | 1 inside, 0 outside: when coordinates were sent and the site has a geofence. |
distance_meters sometimes | integer | Metres from the site. |
low_gps_confidence sometimes | boolean | gps_accuracy is worse than the site threshold. |
warning sometimes | string | A warning-mode geofence breach. |
ClockRequest
POST /clock.php body.
| Field | Type | Description |
|---|---|---|
worker_id sometimes | integer | The worker, or |
employee_id sometimes | string | their employee ID. |
action | string: in, out | |
location_id sometimes | integer | Clock-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 sometimes | integer | Clock-in: the booking this clock is for. Default: the booking that covers the clock time. |
occurred_at sometimes | string | When 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 sometimes | string | At most 255 characters. A clock-out note is added after the clock-in note. |
latitude sometimes | number | |
longitude sometimes | number | Both 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 sometimes | number | Metres. |
survey_id sometimes | integer | |
survey_answers sometimes | list of SurveyAnswer | A blocking answer is 403 blocked_by_survey. |
SurveyAnswer
| Field | Type | Description |
|---|---|---|
question_id | integer | |
answer_value | string |
OnSiteNow
| Field | Type | Description |
|---|---|---|
as_of | UTC time | |
totals | OnSiteTotals | |
locations | list of OnSiteLocation | |
unassigned | list of OnSiteWorker | Workers clocked in with no site. |
OnSiteTotals
| Field | Type | Description |
|---|---|---|
clocked_in | integer | |
on_site | integer | |
off_site | integer | |
geofence_unknown | integer | No GPS, or geofence off: counted as on site. |
low_gps_confidence | integer |
OnSiteLocation
| Field | Type | Description |
|---|---|---|
location_id | integer | |
location_name | string | |
clocked_in | integer | |
on_site | integer | |
off_site | integer | |
geofence_unknown | integer | |
workers sometimes | list of OnSiteWorker | Left out with include_workers=false. |
OnSiteWorker
| Field | Type | Description |
|---|---|---|
worker_id | integer | |
name | string | |
employee_id | string or null | |
entry_id | integer | |
clock_in | local time | |
clock_in_utc | UTC time | |
minutes_on_shift | integer | |
within_geofence | integer or null | 1 inside, 0 outside, null unknown. |
distance_meters | integer or null | |
gps_accuracy | integer or null | |
low_gps_confidence | boolean |
Exception
One check from the Exceptions page.
| Field | Type | Description |
|---|---|---|
category | string: late, no_show, early, missing_clockout, excessive, missed_break, dispute, not_approved, flagged, locked_week, offline_conflict | |
type | string: late_start, no_show, early_finish, missing_clockout, long_shift, overtime, missed_break, client_dispute, not_approved, flagged_clock, locked_week, offline_conflict | |
entity_type | string | What the exception is about (for example shift, entry, worker_week). |
entity_id | integer | |
period_key | string or null | The week, for weekly checks. |
worker_id | integer or null | |
worker_name | string or null | |
employee_id | string or null | |
location_name | string or null | |
date | date or null | |
booked | string or null | The booked times, for example 08:00-16:00. |
entry_id | integer or null | |
shift_id | integer or null | |
clock_in | local time or null | |
clock_out | local time or null | |
clock_in_utc | UTC time or null | |
clock_out_utc | UTC time or null | |
minutes | integer or null | |
detail | string | Plain English, no pay figures. |
label | string or null | |
resolution | object or null | Once dismissed or settled (include_resolved=true). |
ExceptionResolution
| Field | Type | Description |
|---|---|---|
resolution | string or null | |
notes | string or null | |
resolved_at | local time or null |
Amendment
A change a client asked for on a shift from the portal, and what the agency decided.
| Field | Type | Description |
|---|---|---|
id | integer | |
entry_id | integer | |
worker_id | integer | |
worker_name | string | |
employee_id | string or null | |
location_id | integer or null | |
location_name | string or null | |
status | string: pending, accepted, declined, superseded, withdrawn | |
reason | string or null | |
proposed | AmendmentTimes | null values: no change asked for. |
current | AmendmentTimes | The shift as it is now. |
changes | object | Only the values that differ from the shift now. The times are UK local, with no UTC twin (proposed has them). |
proposed_by | string | portal for a client user. |
created_at | local time | |
created_at_utc | UTC time | |
resolved_at | local time or null | |
resolved_at_utc | UTC time or null | |
resolved_by | string or null | |
resolution_note | string or null |
AmendmentTimes
| Field | Type | Description |
|---|---|---|
clock_in | local time or null | |
clock_out | local time or null | |
break_minutes | integer or null | |
clock_in_utc | UTC time or null | |
clock_out_utc | UTC time or null |
PayWeek
Where one pay week stands for this client.
| Field | Type | Description |
|---|---|---|
week_start | date | |
week_end | date | |
status | string: open, checking, ready_to_lock, locked, exported | open = not ended; checking = ended, shifts still to sort out; ready_to_lock; locked; exported = locked and a Sage file made. |
ended | boolean | |
locked | boolean | |
locked_at | local time or null | |
locked_at_utc | UTC time or null | |
locked_by | string or null | Name of the person who locked it. |
lock_id | integer or null | |
frozen | boolean | Frozen figures are stored: GET /payroll.php returns them. |
shifts | integer | Completed shifts in the week at this client's sites. |
shifts_not_approved | integer | Completed shifts the payroll gate leaves out for an approval reason (not approved, waiting for client sign-off, disputed or queried, rejected). |
open_clock_outs | integer | |
exceptions_open | integer | Shifts that stop the week being locked, each counted once (0 once locked). |
late_adjustments_due | integer | Clocks that landed in an earlier locked week, due to be paid as an adjustment in this week. |
late_adjustments_paid | integer | |
late_clocks_in_week | integer | Clocks recorded in this week after it was locked. |
batches | list of ExportBatch | Export files made for this client that cover the week. |
PayWeeksMeta
| Field | Type | Description |
|---|---|---|
period_from | date | |
period_to | date | |
pay_week_start_day | integer | 1 = Monday ... 7 = Sunday. |
approval_rule | string: agency_approved, agency_and_client_approved | agency_and_client_approved when the client must also sign off hours before payroll. |
ExportBatch
A payroll or accounts file made for this client.
| Field | Type | Description |
|---|---|---|
id | integer | |
kind | string: sage, xero, payroll | |
format | string or null | For example sage50. |
client_id | integer or null | |
period_start | date | |
period_end | date | |
entry_count | integer | |
line_count | integer or null | |
file_name | string or null | |
file_sha256 | string or null | SHA-256 of the exact file kept on the server. |
reason | string or null | Given when a period was exported again. |
created_at | local time or null | |
created_at_utc | UTC time or null | |
stamps_entries | boolean | True 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 sometimes | number | pay:read, and not for a site-limited key. |
total_charge sometimes | number | charges:read, and not for a site-limited key. |
ExportBatchDetail
An export batch with the shifts it held. Every field of ExportBatch, plus:
| Field | Type | Description |
|---|---|---|
entry_ids | list of integer | The 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_ok | boolean | The stored file is present and matches its sha256. |
PayrollLine
Approved, priced hours for one worker in one pay week.
| Field | Type | Description |
|---|---|---|
worker_id | integer | |
employee_id | string or null | |
worker_name | string | |
pay_week_start | date | |
pay_week_end | date | |
shifts | integer | |
paid_minutes | integer | |
paid_hours | number | |
gross_pay | number | |
charge_total sometimes | number | charges:read only. |
margin sometimes | number | charges:read only. |
base_pay_rate | number or null | |
department | string or null | The Sage department (analysis value) of the line. |
cost_centre | string or null | |
elements | object of PayrollBucket | The five legacy buckets (regular, overtime, weekend, bank_holiday, night; Sunday is in weekend and guarantee in regular), kept for existing integrations. |
pay_elements | list of PayElement | The Sage 50 Payroll file's lines for this worker and week. |
source | string: live, locked | locked = the frozen figures of a closed week. |
flags | list of string | |
entry_ids sometimes | list of integer | include_entries=1 only. |
PayrollBucket
| Field | Type | Description |
|---|---|---|
minutes | number | |
hours | number | |
pay | number | |
pay_rate | number or null | |
charge sometimes | number | charges:read only. |
charge_rate sometimes | number or null | charges:read only. |
PayElement
One line of the Sage 50 Payroll file.
| Field | Type | Description |
|---|---|---|
element | string: regular, overtime, weekend, sunday, bank_holiday, night, guarantee | |
label | string | |
sage_ref | string | Your Sage payment reference for the element. |
minutes | number | |
hours | number | |
pay_rate | number or null | |
pay | number | |
department | string or null | |
cost_centre | string or null | |
charge sometimes | number | charges:read only. |
entry_ids sometimes | list of integer | include_entries=1 only. |
PayrollAdjustment
A clock that landed in a locked week, paid as an adjustment in a later week.
| Field | Type | Description |
|---|---|---|
entry_id | integer | |
worker_id | integer | |
worker_name | string | |
employee_id | string | |
location_id | integer or null | |
location_name | string or null | |
clock_in | local time | |
clock_out | local time or null | |
clock_in_utc | UTC time | |
clock_out_utc | UTC time or null | |
week_start | date | The locked week it belongs to. |
adjust_week | date | The week it is (or was) paid in. |
state | string: due, week_unlocked, paid_adjustment, paid_own_week | |
source | string or null | kiosk, api, manual ... |
detected_at | local time or null | |
resolved_at | local time or null | |
resolved_week | date or null | |
paid_minutes | integer or null | |
pay sometimes | number or null | pay:read only. |
charge sometimes | number or null | charges:read only. |
PayrollMeta
| Field | Type | Description |
|---|---|---|
period_from | date | |
period_to | date | |
pay_week_start_day | integer | |
approval_rule | string: agency_approved, agency_and_client_approved | agency_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_approved | integer | Completed 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. |
currency | string | |
weeks | list of PayrollWeek | |
sage_refs | object of string | Your Sage payment reference per element (empty = folded into another element, as in the Sage file). |
PayrollWeek
| Field | Type | Description |
|---|---|---|
week_start | date | |
week_end | date | |
status | string: open, checking, ready_to_lock, locked, exported | |
locked | boolean | |
locked_at | local time or null | |
locked_at_utc | UTC time or null | |
shifts_not_approved | integer | |
exceptions_open | integer | |
late_adjustments_due | integer |
Charges
The client charge schedule: what the agency bills for labour, per site, worker, role and pay element.
| Field | Type | Description |
|---|---|---|
reference | string | |
client_id | integer | |
from | date | |
to | date | |
frozen | boolean | Every week in the period is locked: the frozen charge lines are returned. |
partly_frozen | boolean | |
require_client_approval | boolean | |
currency | string | |
lines | list of ChargeLine | |
sites | list of ChargeSite | |
totals | ChargeTotals | |
weeks | list of ChargeWeek | |
held | object of ChargeHeld | Shifts left out, by reason. |
warnings | list of ChargeWarning |
ChargeLine
| Field | Type | Description |
|---|---|---|
site_id | integer | |
site_name | string | |
worker_id | integer | |
worker_name | string | |
employee_id | string or null | |
role | string or null | |
cost_code | string or null | |
po | string or null | |
element | string: regular, overtime, weekend, sunday, bank_holiday, night, guarantee | |
element_label | string | |
hours | number | |
rate | number | |
amount | number | |
shifts | integer | |
first_date | date | |
last_date | date | |
frozen | boolean | |
no_charge | boolean | |
entry_ids sometimes | list of integer | include_entries=true only. |
ChargeSite
| Field | Type | Description |
|---|---|---|
site_id | integer | |
site_name | string | |
billing_name | string or null | |
po | string or null | |
sage_account_ref | string or null | |
sage_department | string or null | |
nominal_code | string or null | |
hours | number | |
amount | number | |
vat | number | |
elements | object of ChargeSiteElement |
ChargeSiteElement
| Field | Type | Description |
|---|---|---|
hours | number | |
amount | number |
ChargeTotals
| Field | Type | Description |
|---|---|---|
hours | number | |
net | number | |
vat_rate | number | |
vat | number | |
gross | number | |
shifts | integer | |
workers | integer | |
lines | integer |
ChargeWeek
| Field | Type | Description |
|---|---|---|
week_start | date | |
week_end | date | |
locked | boolean | |
locked_at | local time or null | |
locked_at_utc | UTC time or null |
ChargeHeld
| Field | Type | Description |
|---|---|---|
label | string | |
count | integer | |
amount | number |
ChargeWarning
| Field | Type | Description |
|---|---|---|
code | string: no_charge_rate, not_in_snapshot, zero_hours, not_closed, draft | |
level | string: error, warning, info | |
message | string | |
entry_ids | list of integer |
RateRule
How shifts are priced. A rate left blank is null (the multiplier, or the worker's own rate, applies).
| Field | Type | Description |
|---|---|---|
id | integer | |
name | string | |
client_id | integer or null | null = every client. |
location_id | integer or null | null = every site. |
job_role | string or null | |
cost_code_id | integer or null | |
valid_from | date or null | |
valid_to | date or null | |
priority | integer | |
active | boolean | |
overtime_threshold | number or null | Weekly hours before overtime. |
overtime_multiplier | number or null | |
overtime_tiers | list of any or null | Extra overtime tiers, as set on the rule. |
daily_overtime_threshold | number or null | |
daily_overtime_multiplier | number or null | |
weekend_multiplier | number or null | |
sunday_multiplier | number or null | |
bank_holiday_multiplier | number or null | |
guarantee_hours | number or null | |
paid_break_minutes | integer or null | |
auto_lunch_minutes | integer or null | |
auto_lunch_after_hours | number or null | |
pay sometimes | RateRulePay | pay:read only. |
charge sometimes | RateRuleCharge | charges:read only. |
night_bands | list of NightBand |
RateRulePay
| Field | Type | Description |
|---|---|---|
pay_rate | number or null | |
overtime_pay_rate | number or null | |
daily_overtime_pay_rate | number or null | |
weekend_pay_rate | number or null | |
sunday_pay_rate | number or null | |
bank_holiday_pay_rate | number or null | |
pre_parity_pay_rate | number or null | |
post_parity_pay_rate | number or null |
RateRuleCharge
| Field | Type | Description |
|---|---|---|
charge_rate | number or null | |
overtime_charge_rate | number or null | |
daily_overtime_charge_rate | number or null | |
weekend_charge_rate | number or null | |
sunday_charge_rate | number or null | |
bank_holiday_charge_rate | number or null | |
pre_parity_charge_rate | number or null | |
post_parity_charge_rate | number or null |
NightBand
| Field | Type | Description |
|---|---|---|
start_time | time HH:MM | |
end_time | time HH:MM | |
uplift_multiplier | number or null | |
charge_uplift_multiplier sometimes | number or null | charges:read only. |
pay_rate sometimes | number or null | pay:read only. |
charge_rate sometimes | number or null | charges:read only. |
RateRulesMeta
| Field | Type | Description |
|---|---|---|
settings | RateSettings | |
currency sometimes | string | The list only. |
RateSettings
Engine settings that change how rules combine.
| Field | Type | Description |
|---|---|---|
premium_stacking | string | |
premium_day_basis | string | |
site_overtime_mode | string | |
walk_in_policy | string | How a clock with no booking is paid. |
cancelled_policy | string | How a clock on a cancelled booking is paid. |
multi_shift_priority | string | |
pay_week_start_day | integer |
Shift
A booking. A booking belongs to the client that owns its site, else the worker's home client.
| Field | Type | Description |
|---|---|---|
id | integer | |
client_id | integer | |
worker_id | integer | |
worker_name | string | |
employee_id | string or null | |
department_id | integer or null | |
location_id | integer or null | |
location_name | string or null | |
demand_id | integer or null | The requirement it fills. |
shift_date | date | |
start_time | time HH:MM:SS | |
end_time | time HH:MM:SS | Before start_time = an overnight shift. |
start_utc | UTC time | |
end_utc | UTC time | |
break_minutes | integer | |
job_role | string or null | |
notes | string or null | |
status | string: draft, published, acknowledged, completed, swapped, cancelled, client_cancelled, worker_cancelled, sick, awol, no_show | An older booking stored with a blank status is returned as cancelled. |
cancel_reason | string or null | |
cancelled_at | local time or null | |
acknowledged_at | local time or null | |
declined_at | local time or null | |
confirmation | string: confirmed, declined, unconfirmed, n/a | Whether the worker confirmed it. |
created_at | local time | |
created_at_utc | UTC time | |
updated_at | local time or null | |
updated_at_utc | UTC time or null | |
warning_codes sometimes | list of string | Create and change only: eligibility warnings that did not block it (for example outside_pattern, short_rest, over_hours, role_mismatch). |
detached_demand_id sometimes | integer | Change 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).
| Field | Type | Description |
|---|---|---|
worker_id | integer | Your own worker (based at one of a site-limited key's sites). |
demand_id sometimes | integer | |
location_id sometimes | integer | Required without demand_id. |
shift_date sometimes | date | |
start_time sometimes | string | HH:MM |
end_time sometimes | string | HH:MM |
break_minutes sometimes | integer | |
job_role sometimes | string | |
status sometimes | string: draft, published | Default draft. |
notes sometimes | string |
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).
| Field | Type | Description |
|---|---|---|
worker_id sometimes | integer | |
location_id sometimes | integer | |
shift_date sometimes | date | |
start_time sometimes | string | |
end_time sometimes | string | |
break_minutes sometimes | integer | |
job_role sometimes | string or null | |
notes sometimes | string or null | |
status sometimes | string: draft, published |
ShiftCancel
Cancel body. The status drives the cancelled-booking pay policy and the reliability score.
| Field | Type | Description |
|---|---|---|
status sometimes | string: cancelled, client_cancelled, worker_cancelled, sick, awol, no_show | Default cancelled. |
cancel_reason sometimes | string |
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.
| Field | Type | Description |
|---|---|---|
id | integer | |
client_id | integer | |
location_id | integer | |
location_name | string or null | |
shift_date | date | |
start_time | time HH:MM:SS | |
end_time | time HH:MM:SS | |
start_utc | UTC time | |
end_utc | UTC time | |
break_minutes | integer | |
job_role | string or null | |
headcount | integer | Places needed. |
requested | integer or null | What the client first asked for. |
filled | integer | Live bookings. |
open | integer | headcount - filled, never below 0. |
status | string: open, partial, filled, cancelled | |
series_id | integer or null | Repeating requirements share one. |
source | string: admin, portal, auto, copy, pattern, api | |
notes | string or null | |
cancel_reason | string or null | |
cancelled_at | local time or null | |
created_at | local time | |
created_at_utc | UTC time | |
updated_at | local time or null | Last change to the requirement or to a booking on it. |
updated_at_utc | UTC time or null | The exact UTC of that change, right in the repeated hour when the clocks go back. |
bookings sometimes | list of DemandBooking | GET ?id= only: the live bookings. |
DemandBooking
| Field | Type | Description |
|---|---|---|
shift_id | integer | |
worker_id | integer | |
worker_name | string | |
employee_id | string or null | |
status | string | |
confirmation | string: confirmed, declined, unconfirmed, n/a |
DemandCreate
| Field | Type | Description |
|---|---|---|
location_id | integer | |
shift_date | date | |
start_time | string | HH:MM |
end_time | string | HH:MM |
headcount sometimes | integer | 1 to 500 (default 1). |
break_minutes sometimes | integer | |
job_role sometimes | string | |
notes sometimes | string |
Offer
| Field | Type | Description |
|---|---|---|
id | integer | |
demand_id | integer | |
worker_id | integer | |
worker_name | string | |
employee_id | string or null | |
status | string: sent, accepted, declined, filled, expired | An offer still marked sent after it expired reads expired. |
via | string or null | sms, email, app ... |
sent_at | local time | |
sent_at_utc | UTC time | |
responded_at | local time or null | |
responded_at_utc | UTC time or null | |
expires_at | local time or null | |
expires_at_utc | UTC time or null | |
shift_id | integer or null | The booking made when it was accepted. |
cover_for_shift_id | integer or null |
AvailabilityRule
| Field | Type | Description |
|---|---|---|
id | integer | |
worker_id | integer | |
rule_type | string: weekly, rolling, exception | |
days | list of integer | 1 = Monday ... 7 = Sunday (weekly rules). |
days_label | string or null | |
start_time | time HH:MM or null | null = all day. |
end_time | time HH:MM or null | |
on_days | integer or null | |
off_days | integer or null | Rolling rules. |
effective_from | date or null | |
effective_to | date or null | |
active | boolean | False for a retired rule (include_retired=true). |
created_at | local time or null | |
created_at_utc | UTC time or null |
AvailabilityRules
| Field | Type | Description |
|---|---|---|
worker_id | integer | |
rules | list of AvailabilityRule | The worker's active rules. |
AvailabilityCreate
POST /availability.php body.
| Field | Type | Description |
|---|---|---|
worker_id | integer | |
type | string: weekly, rolling, exception | |
slots sometimes | list of AvailabilitySlot | weekly: replaces the weekly pattern. An empty list clears it. |
on_days sometimes | integer | rolling: 1 to 60. |
off_days sometimes | integer | rolling: 0 to 60. |
effective_from sometimes | date | Default today. |
effective_to sometimes | date | |
start_time sometimes | string | rolling or exception: HH:MM. |
end_time sometimes | string | |
reason sometimes | string | exception: kept for the agency, never returned. |
AvailabilitySlot
| Field | Type | Description |
|---|---|---|
days | any | [1,2,3] or text such as "Mon-Fri". |
start_time sometimes | string | HH:MM |
end_time sometimes | string |
AvailabilityMatch
One worker in a "who is free" answer, best matches first. Codes, never reason text.
| Field | Type | Description |
|---|---|---|
worker_id | integer | |
name | string | |
employee_id | string or null | |
location_id | integer or null | |
department_id | integer or null | |
job_role | string or null | |
eligible | boolean | No block codes. |
available | boolean or null | Inside the worker's pattern; null = unknown (no pattern). |
availability | list of string | The pattern windows that cover the slot. |
role_match | boolean or null | null when no job_role was asked for. |
week_hours | number | Hours that pay week at every site: clocked on days before today (booked where nothing was clocked), booked from today. |
max_hours_per_week | number or null | The worker's weekly hours limit. |
warning_codes | list of string | For example outside_pattern, short_rest, over_hours, role_mismatch, absence_pending. |
block_codes | list of string | Only with include_unavailable=true: inactive, pending, dormant, suspended, dnr, absence, unavailable, overlap, hours_limit (over a hard weekly hours limit). |
AvailabilityMeta
| Field | Type | Description |
|---|---|---|
slot | object | The slot that was checked. |
Absence
A holiday or sickness request. The worker's reason and the reviewer's notes are never returned.
| Field | Type | Description |
|---|---|---|
id | integer | |
worker_id | integer | |
worker_name | string | |
employee_id | string or null | |
location_id | integer or null | The worker's home site. |
type | string: holiday, sick, unpaid, compassionate, training, other | |
start_date | date | |
end_date | date | |
days | number | |
status | string: pending, approved, rejected, cancelled | |
reviewed_at | local time or null | |
reviewed_at_utc | UTC time or null | |
created_at | local time | |
created_at_utc | UTC time |
WebhookSubscription
| Field | Type | Description |
|---|---|---|
id | integer | |
url | string | |
events | list of string | Event names, or ["*"] for every event. |
active | boolean | |
description | string or null | |
api_key_id | integer or null | The 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. |
secret | string | The signing secret: in full when created or rotated, otherwise its first 8 characters and "...". |
created_at | local time | |
created_at_utc | UTC time | |
updated_at | local time or null | |
updated_at_utc | UTC time or null | |
recent_deliveries sometimes | list of WebhookDelivery | GET ?id= only: the 10 latest. |
WebhookCreate
A new subscription is always active. To pause it, send PUT ?id= with active: false.
| Field | Type | Description |
|---|---|---|
url | string | A public HTTPS address on port 443. |
events | list of string | Names from GET ?action=events, or ["*"]. Each needs the read scope for its data (see Webhooks). |
description sometimes | string | Up to 255 characters. |
WebhookUpdate
| Field | Type | Description |
|---|---|---|
url sometimes | string | |
events sometimes | list of string | |
active sometimes | boolean | |
description sometimes | string or null |
WebhookDelivery
| Field | Type | Description |
|---|---|---|
id | integer | The delivery id (the X-TempClock-Delivery header). The same across retries. |
event | string | |
status | string: delivered, pending, failed | |
attempt | integer | |
max_attempts | integer | |
response_code | integer or null | 0 = not reached (URL guard or network). |
delivered_at | local time or null | |
delivered_at_utc | UTC time or null | |
next_retry_at | local time or null | |
next_retry_at_utc | UTC time or null | |
created_at | local time | |
created_at_utc | UTC time | |
response_body sometimes | string or null | Delivery log only: the first 1 KB of what the endpoint answered. |
payload sometimes | any | Delivery log only: the exact JSON sent. |
payload_hidden sometimes | string | Instead of payload, when this key could not subscribe to the event. |
WebhookEvent
| Field | Type | Description |
|---|---|---|
event | string | |
group | string | |
description | string | |
legacy | boolean |
WebhookTestResult
| Field | Type | Description |
|---|---|---|
subscription_id | integer | |
delivery_id | integer | |
delivered | boolean | |
response_code | integer or null | What the endpoint answered; 0 = not reached. |
error | string or null | Why it was not delivered. |
WebhookSecret
| Field | Type | Description |
|---|---|---|
id | integer | |
secret | string | 64 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.
| Field | Type | Description |
|---|---|---|
event | string | |
timestamp | UTC time | |
data | object of any | Event 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 sometimes | integer |
Pagination
| Field | Type | Description |
|---|---|---|
total | integer | Rows matching the filters. |
limit | integer | Rows per page (default 50). |
offset | integer | Rows skipped. |
has_more | boolean | True 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 | |
|---|---|
| additive | New 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. |
| additive | Bookings: 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. |
| additive | Payroll: pay_elements (the Sage 50 file's lines), meta.weeks, adjustments=1 and a department_id filter on /payroll.php. |
| additive | Timesheets: 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. |
| additive | Workers: 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. |
| additive | Clock 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. |
| additive | Idempotency-Key on POST and PUT. occurred_at, employee_id and scheduled_shift_id on /clock.php. |
| additive | Webhook 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. |
| action | Pay 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. |
| action | Keys 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. |
| action | Browser (CORS) access is off unless a key lists the origin. Server to server use is not affected. |
| action | Time 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. |
| action | A 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. |
| additive | Webhooks: 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. |
| additive | Fixes: /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. |
| additive | Fixes: /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.phppays 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.