Errors

Every error has the same shape and a stable error_code. Branch on the code, show the message to a person if you like, and quote the request id when you ask for help.

The error envelope

{
  "error": true,
  "message": "Insufficient permissions. This key does not have the 'payroll:read' scope.",
  "status": 403,
  "error_code": "insufficient_scope",
  "required_scope": "payroll:read"
}
KeyAlwaysMeaning
errorYesAlways true.
messageYesPlain English. It may be reworded at any time, so never match on it.
statusYesThe HTTP status, repeated.
error_codeYesStable code from the table below.
required_scopeWith insufficient_scopeThe scope the request needs.
parameterWith invalid_parameterThe query parameter or header at fault.
fieldWith validation_failed (and some 404s)The body field at fault, or null when no one field is.
distance_metersWith outside_geofenceHow far from the site the coordinates were.
radius_metersWith outside_geofenceThe site's geofence radius in metres.
entry_idWith already_clocked_inThe open shift. Also on a clock conflict: the shift it clashes with.
reason_codeOn some conflictsweek_locked (the pay week is locked) or worked (the booking has been worked).
block_codes, warning_codesWith not_eligibleWhy the worker cannot take a slot. Codes only, never the reason text.

Every reply, error or not, has an X-Request-Id header. The same id is in Setup > API logs, so support can find the exact request.

Every error code

Generated from the API's own catalogue. Real message is what the API answered on a test copy; some codes have several messages (the field or value at fault is named in them).

error_codeStatusMeaningExtra keysReal message
missing_api_key401No Authorization: Bearer header was sent.Missing or invalid Authorization header. Use: Authorization: Bearer <api_key>
invalid_api_key401The key is not recognised.Invalid API key.
key_expired401The key has passed its expiry date.This API key has expired. Please create a new key.
key_revoked401The key has been revoked.This API key has been revoked. Create a new key.
api_access_disabled403API access is switched off for this account.API access is not enabled for this account. Contact your TempClock administrator.
account_suspended403The account is suspended, cancelled or its trial has ended.Not produced on the test copy
insufficient_scope403The key does not hold the scope this request needs.required_scopeInsufficient permissions. This key does not have the 'payroll:read' scope.
client_required400The request must name the client it is for.Reserved for the planned agency-wide key; not sent in v1.3.
client_not_allowed403The key may not act for that client.Reserved for the planned agency-wide key; not sent in v1.3.
location_not_allowed403The key is limited to other sites.This key is not allowed to use that site.
rate_limited429Too many requests this minute for this key. Wait for Retry-After seconds.Rate limit exceeded. Maximum 1 requests per minute.
forbidden403The request is not allowed (generic; a more specific code is used where one exists).This subscription was made in the admin console, so it can only be changed there. Create your own subscription with this key.
method_not_allowed405The HTTP method is not supported by this endpoint.Method not allowed. Allowed: GET, POST, PUT
not_found404The record does not exist or belongs to another client.Worker not found.
invalid_body400The request body is not valid JSON or is missing.The request body must be a JSON object.
invalid_parameter400A query parameter or header is malformed.parameterdate_from must be a date in YYYY-MM-DD format.
validation_failed400A body field is missing or invalid.fieldname is required (1 to 100 characters).
bad_request400The request is invalid (generic; a more specific code is used where one exists).Not produced on the test copy
conflict409The request conflicts with the current state (generic; a more specific code is used where one exists).Not produced on the test copy
duplicate_employee_id409Another worker of this client already has that employee ID.That employee ID is not available.
not_eligible409The worker cannot take this shift.Worker not eligible for this slot.
location_not_found404The site does not exist or belongs to another client.Location not found.
department_not_found404The department does not exist or belongs to another client.Not produced on the test copy
cost_code_not_found404The cost code does not exist or belongs to another client.Not produced on the test copy
worker_pending409The worker has not been approved yet.Not produced on the test copy
worker_inactive409The worker is not active.This worker is not active.
already_clocked_in409The worker already has an open clock-in.Worker already has an open clock-in. Clock out first.
no_open_clock_in409The worker has no open clock-in to close.No open clock-in in the last 18 hours for this worker.
gps_required403The site needs GPS coordinates to clock.GPS is required to clock at Birmingham Office.
outside_geofence403The coordinates are outside the site geofence.distance_meters, radius_metersYou are not at Birmingham Office. 5004m away (max 200m).
site_not_mapped403The site is set to a strict geofence but has no map location, so no clock there can be checked. The agency must set the site's map pin.Not produced on the test copy
blocked_by_survey403A pre-clock-in survey answer blocks this clock-in.Not produced on the test copy
idempotency_key_reused422The Idempotency-Key was already used with a different request.This Idempotency-Key was already used for a different request. Use a new key.
idempotency_in_progress409A request with this Idempotency-Key is still being processed. Retry shortly.Not produced on the test copy
server_error500The server could not complete the request. It has been logged.Not produced on the test copy

The generic codes bad_request, forbidden and conflict are used where no more specific code fits. New codes may be added (an additive change): treat an unknown code by its HTTP status.

Methods per endpoint

A method an endpoint does not support answers 405 method_not_allowed with an Allow header listing the ones it does.

EndpointMethods
/me.phpGET
/workers.phpGET POST PUT
/locations.phpGET
/departments.phpGET
/cost-codes.phpGET
/timesheets.phpGET
/clock.phpPOST
/on-site-now.phpGET
/exceptions.phpGET
/amendments.phpGET
/payroll.phpGET
/pay-weeks.phpGET
/export-batches.phpGET
/charges.phpGET
/rate-rules.phpGET
/shifts.phpGET POST PUT
/demands.phpGET POST
/offers.phpGET
/availability.phpGET POST DELETE
/absences.phpGET
/webhooks.phpGET POST PUT DELETE
DELETE on an endpoint that does not support it 405

Key used: a key holding every scope.

Request

curl -s -X DELETE "$TC_BASE/workers.php" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 405
Allow: GET, POST, PUT

{
  "error": true,
  "message": "Method not allowed. Allowed: GET, POST, PUT",
  "status": 405,
  "error_code": "method_not_allowed"
}

409 conflicts

error_codeWhereWhat to do
already_clocked_inPOST /clock.php action inThe worker has an open shift (clocked in within 18 hours). entry_id names it. Clock out first, or treat it as done if it was your own retry.
no_open_clock_inPOST /clock.php action outNo open shift in the last 18 hours. An older one is a missing clock-out for the Exceptions page.
worker_pending, worker_inactivePOST /clock.phpThe worker is not approved yet, or has left.
duplicate_employee_idPOST, PUT /workers.phpAnother worker of your company already has that employee ID. Employee IDs are unique within your company, not across TempClock: another company may use the same number. The message never says who has it.
not_eligiblePOST, PUT /shifts.phpA hard block: dnr, absence, unavailable, overlap, inactive, pending, dormant, suspended. The API has no override: book it in TempClock with a reason.
conflict + reason_code: workedPUT, cancel on /shifts.phpThe booking has a clock against it: only its notes can change. Correct the timesheet in TempClock.
conflict + reason_code: week_locked/shifts.php, POST /demands.phpThe pay week is locked. Unlock it in TempClock, or book into an open week.
conflictOtherA booking already cancelled, a requirement that is cancelled, full or finished, an availability rule already removed, an anonymised worker, an occurred_at before the end of a recorded shift, or the 25-subscription limit.
idempotency_in_progressAny write with Idempotency-KeyThe first request with that key is still running. Wait for Retry-After seconds and send it again.
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": []
}
Clocking in a worker who is already clocked in 409

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":327,"action":"in"}'

Real reply (local test copy, fake demo data)

HTTP 409

{
  "error": true,
  "message": "Worker already has an open clock-in. Clock out first.",
  "status": 409,
  "error_code": "already_clocked_in",
  "entry_id": 40787
}
An employee ID already in use 409

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" \
  -d '{"name":"Casey Example","employee_id":"DOCS-77E88"}'

Real reply (local test copy, fake demo data)

HTTP 409

{
  "error": true,
  "message": "That employee ID is not available.",
  "status": 409,
  "error_code": "duplicate_employee_id"
}

Clock refusals (403)

error_codeWhen
gps_requiredThe site's geofence is strict and the request has no latitude and longitude.
outside_geofenceStrict geofence and the coordinates are outside the radius. distance_meters says how far. A site in warning mode accepts the clock and flags it instead (the reply has a warning).
blocked_by_surveyA pre-clock-in survey answer blocks the clock-in.
location_not_allowedA site-limited key and a site outside its list (on clock-out, the open shift's site). Limiting a key to some sites is not available yet, so no key you can make today gets it.

Which errors to retry

StatusRetry?How
400, 404, 405, 422NoFix the request first.
401, 403NoFix the key or its access (Setup > API keys).
409DependsRead error_code: idempotency_in_progress after Retry-After; the others need a decision.
429YesAfter Retry-After seconds. See Rate limits.
500, 502, 503, network errorsYesWith backoff (for example 1 s, 4 s, 15 s). Send writes with an Idempotency-Key so a retry can never act twice.

Real error replies

No Authorization header 401

Key used: no key.

Request

curl -s "$TC_BASE/workers.php"

Real reply (local test copy, fake demo data)

HTTP 401

{
  "error": true,
  "message": "Missing or invalid Authorization header. Use: Authorization: Bearer <api_key>",
  "status": 401,
  "error_code": "missing_api_key"
}
A key that does not exist 401

Key used: a key that does not exist.

Request

curl -s "$TC_BASE/workers.php" \
  -H "Authorization: Bearer tc_live_0000000000000000000000000000000000000000"

Real reply (local test copy, fake demo data)

HTTP 401

{
  "error": true,
  "message": "Invalid API key.",
  "status": 401,
  "error_code": "invalid_api_key"
}
A key past its expiry date 401

Key used: an expired key.

Request

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

Real reply (local test copy, fake demo data)

HTTP 401

{
  "error": true,
  "message": "This API key has expired. Please create a new key.",
  "status": 401,
  "error_code": "key_expired"
}
A revoked key 401

Key used: a revoked key.

Request

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

Real reply (local test copy, fake demo data)

HTTP 401

{
  "error": true,
  "message": "This API key has been revoked. Create a new key.",
  "status": 401,
  "error_code": "key_revoked"
}
A key that is still active on an account whose API access is off (rare: switching access off in the console deactivates every key, which then answers key_revoked) 403

Key used: a key left active on a client whose API access is off (set up directly for this example).

Request

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

Real reply (local test copy, fake demo data)

HTTP 403

{
  "error": true,
  "message": "API access is not enabled for this account. Contact your TempClock administrator.",
  "status": 403,
  "error_code": "api_access_disabled"
}
A workers:read key asking for payroll 403

Key used: a key with workers:read only.

Request

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

Real reply (local test copy, fake demo data)

HTTP 403

{
  "error": true,
  "message": "Insufficient permissions. This key does not have the 'payroll:read' scope.",
  "status": 403,
  "error_code": "insufficient_scope",
  "required_scope": "payroll:read"
}
A key limited to one site asking for another 403

Key used: a timesheets:read key limited to one site.

Request

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

Real reply (local test copy, fake demo data)

HTTP 403

{
  "error": true,
  "message": "This key is not allowed to use that site.",
  "status": 403,
  "error_code": "location_not_allowed"
}
Over the key's rate limit (this key allows 1 request a minute) 429

Key used: a key with a limit of 1 request a minute.

Request

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

Real reply (local test copy, fake demo data)

HTTP 429
Retry-After: 60

{
  "error": true,
  "message": "Rate limit exceeded. Maximum 1 requests per minute.",
  "status": 429,
  "error_code": "rate_limited"
}
Changing a webhook subscription this key did not make (here one made in the agency console) 403

Key used: a key holding every scope.

Request

curl -s -X PUT "$TC_BASE/webhooks.php?id=33" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"active":false}'

Real reply (local test copy, fake demo data)

HTTP 403

{
  "error": true,
  "message": "This subscription was made in the admin console, so it can only be changed there. Create your own subscription with this key.",
  "status": 403,
  "error_code": "forbidden"
}
A worker that does not exist (or belongs to another client) 404

Key used: a key holding every scope.

Request

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

Real reply (local test copy, fake demo data)

HTTP 404

{
  "error": true,
  "message": "Worker not found.",
  "status": 404,
  "error_code": "not_found"
}
A body that is not JSON 400

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 'action=in&worker_id=1'

Real reply (local test copy, fake demo data)

HTTP 400

{
  "error": true,
  "message": "The request body must be a JSON object.",
  "status": 400,
  "error_code": "invalid_body"
}
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"
}
A worker with no name 400

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" \
  -d '{"email":"no.name@example.test"}'

Real reply (local test copy, fake demo data)

HTTP 400

{
  "error": true,
  "message": "name is required (1 to 100 characters).",
  "status": 400,
  "error_code": "validation_failed",
  "field": "name"
}
Another client's site 404

Key used: a key holding every scope.

Request

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

Real reply (local test copy, fake demo data)

HTTP 404

{
  "error": true,
  "message": "Location not found.",
  "status": 404,
  "error_code": "location_not_found"
}
Clocking a worker who has left 409

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"}'

Real reply (local test copy, fake demo data)

HTTP 409

{
  "error": true,
  "message": "This worker is not active.",
  "status": 409,
  "error_code": "worker_inactive"
}
Clocking out a worker with no open shift 409

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":327,"action":"out"}'

Real reply (local test copy, fake demo data)

HTTP 409

{
  "error": true,
  "message": "No open clock-in in the last 18 hours for this worker.",
  "status": 409,
  "error_code": "no_open_clock_in"
}
Clocking in at a strict-geofence site with no coordinates 403

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}'

Real reply (local test copy, fake demo data)

HTTP 403

{
  "error": true,
  "message": "GPS is required to clock at Birmingham Office.",
  "status": 403,
  "error_code": "gps_required"
}
Clocking in outside a strict geofence 403

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":10}'

Real reply (local test copy, fake demo data)

HTTP 403

{
  "error": true,
  "message": "You are not at Birmingham Office. 5004m away (max 200m).",
  "status": 403,
  "error_code": "outside_geofence",
  "distance_meters": 5004,
  "radius_meters": 200
}
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"
}