Conventions
The rules every endpoint follows: the shape of a reply, paging, filters, syncing changes, safe retries, dates and times, and how the API changes over time.
Requests
- Base URL:
https://<your TempClock address>/api/v1/<resource>.php, HTTPS only. The paths also answer without.php; the docs use the.phpform. - Send JSON bodies with
Content-Type: application/json, in UTF-8. A body that is not a JSON object is400 invalid_body. - Methods:
GETreads,POSTcreates (and runs actions such as?action=cancel),PUTchanges the fields you send,DELETEremoves. A method an endpoint does not support is405 method_not_allowedwith anAllowheader. - One record: add
?id=to a list endpoint. A record of another client answers404, the same as one that does not exist.
Replies
Every reply is JSON. A success has a data key: an object for one record, a list (with pagination) for a list. Some endpoints add meta (for example the pay weeks of a payroll period) and writes add a plain English message.
{ "data": { ... } } one record
{ "data": [ ... ], "pagination": { ... } } a list
{ "data": [ ... ], "pagination": { ... }, "meta": { ... } }
{ "data": { ... }, "message": "Worker created." } a writeAn error always has the same keys, plus any extras its code lists. Branch on error_code, never on message, which may be reworded. The full list is on Errors.
A date in the wrong format 400
Key used: a key holding every scope.
Request
curl -s "$TC_BASE/timesheets.php?date_from=01%2F10%2F2026" \ -H "Authorization: Bearer $TC_KEY"
Real reply (local test copy, fake demo data)
HTTP 400
{
"error": true,
"message": "date_from must be a date in YYYY-MM-DD format.",
"status": 400,
"error_code": "invalid_parameter",
"parameter": "date_from"
}Paging
Lists take limit (1 to 100, default 50; up to 200 on /payroll.php) and offset. A value outside the range is brought into it. Every list has a fixed order that ends with the record id, so walking the pages never skips or repeats a row, even when many rows tie (for example workers with the same name).
{
"pagination": {
"total": 25,
"limit": 2,
"offset": 0,
"has_more": true
}
}Keep requesting with offset = offset + limit while has_more is true. Rows created while you page can shift a newest-first list; for a complete copy use updated_since instead of re-reading everything.
Filters
| Kind | Format | Notes |
|---|---|---|
| Dates | YYYY-MM-DD | On the clock-in date for time entries, on shift_date for bookings and requirements. date_from must be on or before date_to. |
| Pay weeks | any date in the week | week or week_start takes any date inside the pay week (your pay week start day applies). A range is widened to whole pay weeks, at most 26 per request. Charges and exceptions take up to 93 days. |
| Booleans | true / false | Also 1 / 0 and yes / no, any case. Anything else is 400. |
| Ids | a positive whole number | 0, negatives, decimals and text are 400. |
| Date-times | ISO 8601 | See updated_since. |
| Sites | location_id | Another client's site is 404 location_not_found; a site outside a site-limited key is 403 location_not_allowed (site-limited keys are not available yet). |
A malformed filter is 400 invalid_parameter, and parameter names it. Unknown parameters are ignored.
Syncing changes with updated_since
updated_since returns only records created or changed since a time. It works on /workers.php, /timesheets.php, /shifts.php, /demands.php, /amendments.php. For time entries a clock-out, approval, client sign-off, correction or export all count as a change.
- Send ISO 8601 with
Zor an offset:2026-10-06T09:00:00Z,2026-10-06T10:00:00+01:00(encode the+as%2B; a+that arrives as a space is understood). A time with no offset is read as UK local time; a bare date means local midnight. - Keep the time you started each sync (from your own clock, a minute early), and send it next time. Treat the result as a feed and de-duplicate on
id. - On the night the clocks go back, local times from 01:00 to 01:59 happen twice. On
/timesheets.phpand/amendments.phpa bound inside that hour is moved back to its start, so a few rows can come back twice but none are missed./workers.php,/shifts.phpand/demands.phpcompare the bound as an exact instant with the exact time of each change (updated_at_utcis exact in that hour too), so a bound sent withZor an offset misses nothing and sends nothing twice. A local time inside the repeated hour is read as its first pass, so no change is missed but a few can come back twice. - On
/demands.php, a booking added to, cancelled on, moved off or re-timed on a requirement counts as a change to the requirement, so a change tofilledandopenis picked up even whenstatusstays the same.
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,
"clock_out_distance_meters": 0,
"created_at": "2026-10-02 07:33:36",
"updated_at": "2026-10-02 07:33:36",
"updated_at_utc": "2026-10-02T06:33:36Z"
},
{
"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,
... (55 more lines)Safe retries with Idempotency-Key
A network failure can leave you not knowing whether a write happened. Send an Idempotency-Key header on POST and PUT (workers, clock, shifts, demands, availability, webhooks) and you can retry safely:
- The key is 1 to 64 printable characters, unique per operation; a UUID is ideal. Keys belong to your API key and are kept for 24 hours.
- The same key with the same method, path and body gets the first answer again, with
Idempotent-Replayed: true, and nothing is done twice. - The same key with a different request is
422 idempotency_key_reused. While the first request is still running, a retry gets409 idempotency_in_progresswithRetry-After: 2. - A 5xx answer is not kept, so a retry runs again.
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" }
The same Idempotency-Key with a different body 422
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":"out"}'Real reply (local test copy, fake demo data)
HTTP 422
{
"error": true,
"message": "This Idempotency-Key was already used for a different request. Use a new key.",
"status": 422,
"error_code": "idempotency_key_reused"
}Without the header: GET is always safe to retry. A repeated PUT sends the same values again. A repeated POST can act twice: a clock-in sent twice gets 409 already_clocked_in, a second booking of the same worker at the same time gets a 409, a worker with the same employee ID gets 409 duplicate_employee_id, but a second requirement or webhook subscription would be created. Use the header on every POST and PUT.
Dates and times
- Record times (
clock_in,created_at...) are UK local time,Y-m-d H:i:swith no offset, as the consoles and the Sage files show them. - UTC twins. Clock times (
clock_in_utc,clock_out_utc), booking start and end (start_utc,end_utc) and most other record times also come as*_utcin ISO 8601 UTC (2026-10-05T07:00:00Z). Store and compare the UTC value where there is one: it is never ambiguous. In the repeated hour when the clocks go back, a local time is read as the second occurrence (GMT), except anupdated_sincebound, which never skips a change (see above). - Times with no UTC twin. These are UK local only: read them as Europe/London.
- Amendment:
changes.clock_in,changes.clock_out - Demand:
cancelled_at - Exception:
resolution.resolved_at - PayrollAdjustment:
detected_at,resolved_at - Shift:
cancelled_at,acknowledged_at,declined_at - TimeEntry:
approved_at,client_approved_at,edited_at,original_clock_in,original_clock_out,created_at - Webhooks (clock.geofence_refused and clock.blocked_by_survey):
attempted_at - Webhooks (shift events):
cancelled_at - Webhooks (timesheet.amended):
proposed_clock_in,proposed_clock_out - Clock webhooks from the kiosk and the worker app carry local times only (see Webhooks).
- Amendment:
- Bookings have
shift_date,start_timeandend_timein local time, plusstart_utcandend_utc. An end time before the start time is an overnight shift. - Always UTC:
as_ofon/on-site-now.phpand thetimestampof a webhook. - Sending times:
occurred_aton a clock andavailable_from/available_totake ISO 8601 with an offset (best) or local time.
Money and minutes
- Money is a number in pounds (GBP) to 2 decimal places. Pay figures appear only with
pay:readand charge figures only withcharges:read. total_minutesis the real time between clock-in and clock-out, right across a clock change.paid_minutesis what is paid, from the pricing engine (breaks, rounding to the booking and manual adjustments applied): use it for pay.- Payroll, the Sage 50 Payroll file and the payroll screen use the same engine and the same approval gate, so their totals agree.
Headers on every reply
| Header | Meaning |
|---|---|
X-Request-Id | 16 hex characters, also stored in API Logs. Quote it when asking for help. |
X-API-Version | The minor version serving the reply, for example 1.3. |
X-RateLimit-Limit, X-RateLimit-Remaining | On every reply to a recognised key. See Rate limits. |
Retry-After | Seconds to wait, on 429 and on 409 idempotency_in_progress. |
Idempotent-Replayed | true on a replayed answer. |
Browsers: cross-origin (CORS) calls are refused unless the key lists the page's origin under Browser origins. Then those headers are exposed to the page as well.
Versioning
- Additive changes ship in v1 without notice: new endpoints, new fields, new optional parameters, new webhook events and new values in lists documented as open (such as
risk_flagsor an error'sblock_codes). Ignore fields you do not know, and do not fail on a new value. - Removals and renames only go to a new major path,
/api/v2./api/v1keeps working for at least 6 months after v2 ships, and the owners of active keys are told by email. - The changelog marks each change additive, or says when an integration needs to act (for example a scope it must be given).
- The OpenAPI 3.0.3 file is the machine-readable contract. An automated test checks it against the code and calls every documented example again.