Authentication
Every request sends an API key as a bearer token. Keys are made by your own administrators in TempClock, one per system, each with only the access that system needs.
Sending the key
Send the key in the Authorization header of every request, over HTTPS:
Authorization: Bearer tc_live_4f0c9a2e7b1d58c3a6e9f07b3d2c1a54e8f6b9d0
A key is tc_live_ followed by 40 characters. Bearer can be any case; the key itself is case sensitive. Never put a key in a URL, a web page or a mobile app: anyone who can read it can use it until it is revoked.
Switching API access on
API access is off for a new client account. A TempClock administrator switches it on once, in the agency console under Setup > API > Client API Access. From then on the account's own administrators make and manage their keys themselves: nobody has to ask TempClock for a key.
Switching API access off deactivates every key of the account at once: they then answer 401 key_revoked. Switching access back on does not bring them back, so create new keys. Webhook subscriptions made with those keys stop receiving events, and each waiting delivery is recorded as refused in the delivery log.
Creating a key
- Open Setup > API keys in the client dashboard (account admins). In the agency console, use Setup > API > Generate API Key for Client.
- Name the key after the system that will use it, for example Payroll export or Site board, North depot. Use one key per system so you can revoke one without stopping the others.
- Under What this key can do, choose a level for each resource: No access, Read only or Read and write. Pay, charges, rates, on site now, availability, absences and clock start at No access. Payroll starts at Read only for people who may grant it: set it to No access unless the system needs payroll lines.
- Optionally open Expiry, rate limit, sites and browser origins to set an expiry date, a rate limit or browser origins (see below).
- Select Create key. The full key is shown once: copy it into your system's secret store straight away. TempClock keeps only a hash and the first 16 characters.
The step by step guide with screenshots is API keys and logs.
Testing a key
GET /me.php needs no scope. It answers which account and sites the key sees, its scopes (as stored, and as they really apply), its rate limit and its expiry, and never shows the key itself.
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",
"onsite:read",
"pay:read",
"payroll:read",
"rates:write",
"shifts:read",
"shifts:write",
"timesheets:read",
"webhooks:read",
"webhooks:write",
"workers:read",
... (12 more lines)Scopes
A scope is <resource>:read or <resource>:write. Write includes read, and payroll:read includes pay:read (payroll lines are pay). A request without the scope it needs gets 403 with error_code: insufficient_scope and the scope in required_scope. This table is generated from the same catalogue the key screens and the API use.
| Scope | Shown as | What it opens | Who may grant it | In the v1.1 set |
|---|---|---|---|---|
workers:read | Workers | Worker records. Seeing pay and charge rates needs the Pay and Charges scopes. Read: list and view workers. | None | Yes |
workers:write | Workers | Worker records. Seeing pay and charge rates needs the Pay and Charges scopes. Write: also create and update workers. Setting or clearing pay and charge rates (hourly_rate, charge_rate) also needs the Set rates scope, rates:write. Includes read. | None | Yes |
timesheets:read | Timesheets | Clock records with approval status and paid minutes, plus who is on site now. | None | Yes |
payroll:read | Payroll | Approved, priced hours per worker per pay week, split by pay element. Includes pay. Includes pay:read. | See pay rates and pay totals | Yes |
pay:read | Pay | Pay rates, pay amounts and pay elements wherever a resource shows them. | See pay rates and pay totals | Yes |
charges:read | Charges | Charge rates, charge amounts and billing fields (PO, Sage account, VAT). | See pay rates and pay totals | Yes |
rates:write | Set rates | Set or clear pay and charge rates on worker records. | Change pay and charge rates | No |
shifts:read | Shifts | Scheduled shifts and open demand. Read: list and view shifts, open demand and offers. | None | Yes |
shifts:write | Shifts | Scheduled shifts and open demand. Write: also book, change and cancel shifts and add open demand. Includes read. | None | Yes |
onsite:read | On site now | Who is on site right now (no pay data). | None | No |
locations:read | Locations | Sites and their geofence settings. | None | Yes |
departments:read | Departments | Department list. | None | Yes |
cost_codes:read | Cost codes | Cost code list. | None | Yes |
availability:read | Availability | Worker availability rules, and who is free for a time. Read: list availability rules and see who is free. | None | No |
availability:write | Availability | Worker availability rules, and who is free for a time. Write: also add and retire availability rules. Includes read. | None | No |
absences:read | Absences | Holiday and sickness requests. | None | No |
clock:write | Clock in/out | Clock workers in and out from another system. | None | Yes |
webhooks:read | Webhooks | Webhook subscriptions. Read: list subscriptions, the event list and the delivery log. | None | Yes |
webhooks:write | Webhooks | Webhook subscriptions. Write: also create, change, delete and test subscriptions and rotate the signing secret. Includes read. | None | Yes |
Who may grant it: a person can only give a key pay, charges or payroll access if they hold the See pay rates and pay totals permission themselves, and rate changes only with Change pay and charge rates. The key screens show those levels as locked for anyone without the permission, and the server refuses them whatever is posted. In the client dashboard, tenant managers and admins hold both.
Pay and charge figures appear only to keys that may see them: hourly_rate on a worker needs pay:read, charge_rate needs charges:read (the fields are left out otherwise), and setting either rate needs rates:write.
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
}
}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"
}Least privilege recipes
| Integration | Give the key | Leave at No access |
|---|---|---|
| Payroll import (Sage or another payroll system) | payroll:read (includes pay). Add webhooks:write to be told when a week is locked. | Everything else. Add charges:read only if it needs charge totals. |
| Invoicing or billing | charges:read, payroll:read for the pay week status | Workers write, rates, clock |
| ATS or CRM worker sync | workers:write, locations:read, departments:read, cost_codes:read | rates:write, pay, charges, payroll |
| Booking system | shifts:write, availability:read, absences:read, workers:read, locations:read | Pay, charges, payroll, rates |
| On-site board or fire register display | onsite:read only | Everything else (no pay, no worker records) |
| Turnstile or third-party clocking terminal | clock:write only, one key per terminal system | Everything else |
| BI or reporting tool | timesheets:read, workers:read, lookups | pay:read and charges:read unless it must show money |
What a key can see
- One client account. A key belongs to one client and only ever sees that client's data. Records of other clients answer
404, the same as a record that does not exist. - The site's client owns the shift. A time record, booking or payroll line belongs to the client that owns the site where it was worked, else the worker's home client. This is the rule payroll and the Sage export use, so a worker pooled from another client who works at your site appears in your timesheets and payroll, and your worker at another client's site does not.
- Every site of the account. A key sees every site of its client. Limiting a key to some sites is not available yet: the key screens do not offer it and the server refuses it. The reply
403 location_not_allowedis reserved for site-limited keys, so no key you can make today gets it. - Several client accounts. An agency-wide key that covers several client accounts at once is planned and not available in v1.3. Today, use one key per client account. In a single-client set-up one client key already covers every site.
- Who did it. Workers, bookings, requirements, availability rules and webhook subscriptions created or changed with a key are written to the audit log as
API key #<id> (client #<id>), never as a person. A clock sent through the API is recorded on the time entry with the clock methodapi.
Expiry, rotation and revoking
| Setting | What it does |
|---|---|
| Expiry date | The key stops working at the end of that day (401 key_expired). Leave it empty for no expiry. |
| Rate limit | Requests a minute for this key: 60 to 120, or up to 600 when set by a TempClock super admin. See Rate limits. |
| Sites this key can see | Every site of the account. Limiting a key to some sites is not available yet. |
| Browser origins | Only needed if a web page calls the API straight from the browser (CORS). One origin per line, for example https://reports.example.com. Leave it empty for server to server use: then no browser can call the API with the key. |
Rotate makes a new key with the same access and settings, and keeps the old key working for the overlap you choose (none, 1, 3, 7, 14 or 30 days; 7 by default), so you can swap the secret in without downtime. Once the overlap ends the old key answers 401 key_expired. Webhook subscriptions made with the old key stop when its overlap ends: create them again with the new key before then (each gets a new signing secret). Rotating a key that has pay access needs the pay permission, because it hands out a new secret. A key that has been rotated or has expired can no longer be edited.
Revoke stops a key at once (401 key_revoked). Revoke a key as soon as the system that used it is retired or the key may have leaked, then make a new one.
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"
}Older keys
Keys made before scopes existed are fixed to the v1.1 scope set: workers:read, workers:write, timesheets:read, payroll:read, shifts:read, shifts:write, locations:read, departments:read, cost_codes:read, clock:write, webhooks:read, webhooks:write, pay:read, charges:read. They keep exactly what they could do and never gain a scope added later. They show as Legacy key on the key list; use Edit access to give them an explicit, smaller set. When v1.3 split pay and charges into their own scopes, keys that could read workers or timesheets were given pay:read and charges:read so nothing stopped working; remove them from keys that do not need pay data. Each change the upgrade made is in the audit log as System (API v1.3 upgrade).
API logs and audit
Setup > API logs lists every request made with your keys, including refused ones: time, key, method, path, status, error code, response time and the request id. Filter Status Code to Any failure to see only problems, or pick one key. Every reply carries an X-Request-Id header with the same id: quote it when you ask for help.
Creating a key, changing its access or settings, rotating and revoking it are written to the audit log with the values before and after.
Refused requests
| Status | error_code | When |
|---|---|---|
| 401 | missing_api_key | No Authorization: Bearer header. |
| 401 | invalid_api_key | The key is not recognised (often a key cut short when it was copied). |
| 401 | key_revoked | The key was revoked, or API access was switched off for the account (every key is deactivated then, and switching access back on does not reactivate them: create new keys). |
| 401 | key_expired | The key passed its expiry date, or it was rotated and its overlap has ended. |
| 403 | api_access_disabled | API access is off for the account but the key was not deactivated (rare). |
| 403 | account_suspended | The account is suspended or cancelled, or its trial has ended. Every key stops working until the account is active again. |
| 403 | insufficient_scope | The key lacks the scope in required_scope. |
| 403 | location_not_allowed | A site-limited key named another site. Only a key limited to some sites gets it, and such keys are not available yet. |
| 429 | rate_limited | Over the key's limit, or 20 unknown keys from one address in 5 minutes. |
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"
}