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 .php form.
  • Send JSON bodies with Content-Type: application/json, in UTF-8. A body that is not a JSON object is 400 invalid_body.
  • Methods: GET reads, POST creates (and runs actions such as ?action=cancel), PUT changes the fields you send, DELETE removes. A method an endpoint does not support is 405 method_not_allowed with an Allow header.
  • One record: add ?id= to a list endpoint. A record of another client answers 404, 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 write

An 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

KindFormatNotes
DatesYYYY-MM-DDOn the clock-in date for time entries, on shift_date for bookings and requirements. date_from must be on or before date_to.
Pay weeksany date in the weekweek 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.
Booleanstrue / falseAlso 1 / 0 and yes / no, any case. Anything else is 400.
Idsa positive whole number0, negatives, decimals and text are 400.
Date-timesISO 8601See updated_since.
Siteslocation_idAnother 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 Z or 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.php and /amendments.php a 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.php and /demands.php compare the bound as an exact instant with the exact time of each change (updated_at_utc is exact in that hour too), so a bound sent with Z or 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 to filled and open is picked up even when status stays 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 gets 409 idempotency_in_progress with Retry-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:s with 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 *_utc in 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 an updated_since bound, 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).
  • Bookings have shift_date, start_time and end_time in local time, plus start_utc and end_utc. An end time before the start time is an overnight shift.
  • Always UTC: as_of on /on-site-now.php and the timestamp of a webhook.
  • Sending times: occurred_at on a clock and available_from / available_to take 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:read and charge figures only with charges:read.
  • total_minutes is the real time between clock-in and clock-out, right across a clock change. paid_minutes is 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

HeaderMeaning
X-Request-Id16 hex characters, also stored in API Logs. Quote it when asking for help.
X-API-VersionThe minor version serving the reply, for example 1.3.
X-RateLimit-Limit, X-RateLimit-RemainingOn every reply to a recognised key. See Rate limits.
Retry-AfterSeconds to wait, on 429 and on 409 idempotency_in_progress.
Idempotent-Replayedtrue 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_flags or an error's block_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/v1 keeps 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.