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"
}| Key | Always | Meaning |
|---|---|---|
error | Yes | Always true. |
message | Yes | Plain English. It may be reworded at any time, so never match on it. |
status | Yes | The HTTP status, repeated. |
error_code | Yes | Stable code from the table below. |
required_scope | With insufficient_scope | The scope the request needs. |
parameter | With invalid_parameter | The query parameter or header at fault. |
field | With validation_failed (and some 404s) | The body field at fault, or null when no one field is. |
distance_meters | With outside_geofence | How far from the site the coordinates were. |
radius_meters | With outside_geofence | The site's geofence radius in metres. |
entry_id | With already_clocked_in | The open shift. Also on a clock conflict: the shift it clashes with. |
reason_code | On some conflicts | week_locked (the pay week is locked) or worked (the booking has been worked). |
block_codes, warning_codes | With not_eligible | Why 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_code | Status | Meaning | Extra keys | Real message |
|---|---|---|---|---|
missing_api_key | 401 | No Authorization: Bearer header was sent. | Missing or invalid Authorization header. Use: Authorization: Bearer <api_key> | |
invalid_api_key | 401 | The key is not recognised. | Invalid API key. | |
key_expired | 401 | The key has passed its expiry date. | This API key has expired. Please create a new key. | |
key_revoked | 401 | The key has been revoked. | This API key has been revoked. Create a new key. | |
api_access_disabled | 403 | API access is switched off for this account. | API access is not enabled for this account. Contact your TempClock administrator. | |
account_suspended | 403 | The account is suspended, cancelled or its trial has ended. | Not produced on the test copy | |
insufficient_scope | 403 | The key does not hold the scope this request needs. | required_scope | Insufficient permissions. This key does not have the 'payroll:read' scope. |
client_required | 400 | The request must name the client it is for. | Reserved for the planned agency-wide key; not sent in v1.3. | |
client_not_allowed | 403 | The key may not act for that client. | Reserved for the planned agency-wide key; not sent in v1.3. | |
location_not_allowed | 403 | The key is limited to other sites. | This key is not allowed to use that site. | |
rate_limited | 429 | Too many requests this minute for this key. Wait for Retry-After seconds. | Rate limit exceeded. Maximum 1 requests per minute. | |
forbidden | 403 | The 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_allowed | 405 | The HTTP method is not supported by this endpoint. | Method not allowed. Allowed: GET, POST, PUT | |
not_found | 404 | The record does not exist or belongs to another client. | Worker not found. | |
invalid_body | 400 | The request body is not valid JSON or is missing. | The request body must be a JSON object. | |
invalid_parameter | 400 | A query parameter or header is malformed. | parameter | date_from must be a date in YYYY-MM-DD format. |
validation_failed | 400 | A body field is missing or invalid. | field | name is required (1 to 100 characters). |
bad_request | 400 | The request is invalid (generic; a more specific code is used where one exists). | Not produced on the test copy | |
conflict | 409 | The request conflicts with the current state (generic; a more specific code is used where one exists). | Not produced on the test copy | |
duplicate_employee_id | 409 | Another worker of this client already has that employee ID. | That employee ID is not available. | |
not_eligible | 409 | The worker cannot take this shift. | Worker not eligible for this slot. | |
location_not_found | 404 | The site does not exist or belongs to another client. | Location not found. | |
department_not_found | 404 | The department does not exist or belongs to another client. | Not produced on the test copy | |
cost_code_not_found | 404 | The cost code does not exist or belongs to another client. | Not produced on the test copy | |
worker_pending | 409 | The worker has not been approved yet. | Not produced on the test copy | |
worker_inactive | 409 | The worker is not active. | This worker is not active. | |
already_clocked_in | 409 | The worker already has an open clock-in. | Worker already has an open clock-in. Clock out first. | |
no_open_clock_in | 409 | The worker has no open clock-in to close. | No open clock-in in the last 18 hours for this worker. | |
gps_required | 403 | The site needs GPS coordinates to clock. | GPS is required to clock at Birmingham Office. | |
outside_geofence | 403 | The coordinates are outside the site geofence. | distance_meters, radius_meters | You are not at Birmingham Office. 5004m away (max 200m). |
site_not_mapped | 403 | The 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_survey | 403 | A pre-clock-in survey answer blocks this clock-in. | Not produced on the test copy | |
idempotency_key_reused | 422 | The 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_progress | 409 | A request with this Idempotency-Key is still being processed. Retry shortly. | Not produced on the test copy | |
server_error | 500 | The 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.
| Endpoint | Methods |
|---|---|
| /me.php | GET |
| /workers.php | GET POST PUT |
| /locations.php | GET |
| /departments.php | GET |
| /cost-codes.php | GET |
| /timesheets.php | GET |
| /clock.php | POST |
| /on-site-now.php | GET |
| /exceptions.php | GET |
| /amendments.php | GET |
| /payroll.php | GET |
| /pay-weeks.php | GET |
| /export-batches.php | GET |
| /charges.php | GET |
| /rate-rules.php | GET |
| /shifts.php | GET POST PUT |
| /demands.php | GET POST |
| /offers.php | GET |
| /availability.php | GET POST DELETE |
| /absences.php | GET |
| /webhooks.php | GET 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_code | Where | What to do |
|---|---|---|
already_clocked_in | POST /clock.php action in | The 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_in | POST /clock.php action out | No open shift in the last 18 hours. An older one is a missing clock-out for the Exceptions page. |
worker_pending, worker_inactive | POST /clock.php | The worker is not approved yet, or has left. |
duplicate_employee_id | POST, PUT /workers.php | Another 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_eligible | POST, PUT /shifts.php | A 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: worked | PUT, cancel on /shifts.php | The 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.php | The pay week is locked. Unlock it in TempClock, or book into an open week. |
conflict | Other | A 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_progress | Any write with Idempotency-Key | The 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_code | When |
|---|---|
gps_required | The site's geofence is strict and the request has no latitude and longitude. |
outside_geofence | Strict 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_survey | A pre-clock-in survey answer blocks the clock-in. |
location_not_allowed | A 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
| Status | Retry? | How |
|---|---|---|
| 400, 404, 405, 422 | No | Fix the request first. |
| 401, 403 | No | Fix the key or its access (Setup > API keys). |
| 409 | Depends | Read error_code: idempotency_in_progress after Retry-After; the others need a decision. |
| 429 | Yes | After Retry-After seconds. See Rate limits. |
| 500, 502, 503, network errors | Yes | With 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"
}