Code examples
Copy-paste starting points in curl, PHP, Python and Node.js: test a key, page through a list, pull a pay week for payroll, sync workers, send clocks from another terminal and check a webhook signature.
Set-up
Every example reads the base URL and key from the environment. Keep keys out of source code.
export TC_BASE="https://tempclock.com/api/v1" # your TempClock address + /api/v1 export TC_KEY="tc_live_..." # Setup > API keys
Test a key
curl
curl -s "$TC_BASE/me.php" -H "Authorization: Bearer $TC_KEY"
PHP
<?php
$ch = curl_init(getenv('TC_BASE') . '/me.php');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('TC_KEY')],
]);
$me = json_decode(curl_exec($ch), true);
echo $me['data']['client_name'], ': ', implode(', ', $me['data']['effective_scopes']), "\n";A small client with retries
One function that sends the key, retries a 429 after Retry-After and a 5xx with backoff, and raises everything else with the error_code and request id.
Python
import os, time, uuid, requests
BASE, KEY = os.environ["TC_BASE"], os.environ["TC_KEY"]
class TempClockError(Exception):
def __init__(self, status, body, request_id):
self.status, self.code, self.request_id = status, body.get("error_code"), request_id
super().__init__(f"{status} {self.code}: {body.get('message')} (request {request_id})")
def tc(method, path, params=None, body=None, idempotency_key=None):
headers = {"Authorization": f"Bearer {KEY}"}
if method in ("POST", "PUT"):
headers["Idempotency-Key"] = idempotency_key or str(uuid.uuid4()) # the same key on every retry
for attempt in range(5):
r = requests.request(method, f"{BASE}/{path}", params=params, json=body, headers=headers, timeout=30)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "60"))); continue
if r.status_code >= 500:
time.sleep(2 ** attempt); continue
j = r.json()
if r.status_code >= 400:
raise TempClockError(r.status_code, j, r.headers.get("X-Request-Id"))
return j
raise TempClockError(r.status_code, r.json(), r.headers.get("X-Request-Id"))Page through a list
Python
def all_rows(path, **params):
offset = 0
while True:
page = tc("GET", path, params={**params, "limit": 100, "offset": offset})
yield from page["data"]
if not page["pagination"]["has_more"]:
return
offset += page["pagination"]["limit"]
for w in all_rows("workers.php", active="true"):
print(w["employee_id"], w["name"], w["department_id"])Pull a pay week for payroll
The payroll lines are the same shifts and totals as the Sage 50 Payroll file. The safe moment to pull is when the week is locked: listen for pay_week.locked, or check /pay-weeks.php. Key: payroll:read.
- Is the week final?
GET /pay-weeks.php?week_start=YYYY-MM-DD. Wait untilstatusislockedorexported. Until thenexceptions_opensays how many shifts still stop it closing. - Pull the lines.
GET /payroll.php?week=YYYY-MM-DD&limit=200&adjustments=1, paging withoffset. Importpay_elements: one line per Sage payment reference, rate, department and cost centre. - Pay late clocks.
adjustmentslists clocks that landed in an earlier locked week and are due in this one (state: due,adjust_week). - Reconcile.
batcheson the pay week lists the Sage files made for it;GET /export-batches.php?id=gives the shifts each one paid.
Python
def pull_pay_week(any_date):
week = tc("GET", "pay-weeks.php", params={"week_start": any_date})["data"][0]
if week["status"] not in ("locked", "exported"):
raise RuntimeError(f"Week of {week['week_start']} is {week['status']}: "
f"{week['exceptions_open']} shifts still to sort out")
lines, adjustments, offset = [], [], 0
while True:
page = tc("GET", "payroll.php", params={"week": week["week_start"], "limit": 200,
"offset": offset, "adjustments": 1})
lines += page["data"]
adjustments = page.get("adjustments", adjustments)
if not page["pagination"]["has_more"]:
break
offset += 200
for line in lines:
for el in line["pay_elements"]:
import_pay_line(line["employee_id"], el["sage_ref"], el["hours"], el["pay_rate"],
el["pay"], el["department"], el["cost_centre"])
for a in adjustments:
if a["state"] == "due" and a["adjust_week"] == week["week_start"]:
flag_late_clock(a["employee_id"], a["clock_in"], a["paid_minutes"])
return week["batches"] # the Sage files TempClock made for this weekIs the pay week final, and what was exported? 200
Key used: a key with payroll:read only (pay figures, no charges).
Request
curl -s "$TC_BASE/pay-weeks.php?week_start=2026-09-25" \ -H "Authorization: Bearer $TC_KEY"
Real reply (local test copy, fake demo data)
HTTP 200
{
"data": [
{
"week_start": "2026-09-21",
"week_end": "2026-09-27",
"status": "exported",
"ended": true,
"locked": true,
"locked_at": "2026-10-01 07:33:35",
"locked_at_utc": "2026-10-01T06:33:35Z",
"locked_by": null,
"lock_id": 19,
"frozen": false,
"shifts": 107,
"shifts_not_approved": 29,
"open_clock_outs": 0,
"exceptions_open": 0,
"late_adjustments_due": 0,
"late_adjustments_paid": 0,
"late_clocks_in_week": 1,
"batches": [
{
"id": 8,
"kind": "sage",
"format": "sage50",
"client_id": 21,
"period_start": "2026-09-21",
"period_end": "2026-09-27",
"entry_count": 2,
"line_count": 4,
"file_name": "sage50_payroll_2026-09-21.csv",
"file_sha256": "2393f9791223b6c74c664b8ad9549911439b7e9f10057d5c5ae9b9807b36f322",
"reason": null,
"created_at": "2026-10-02 07:33:35",
"created_at_utc": "2026-10-02T06:33:35Z",
"stamps_entries": true,
"total_pay": 241.5
}
]
}
],
"meta": {
"period_from": "2026-09-21",
"period_to": "2026-09-27",
"pay_week_start_day": 1,
"approval_rule": "agency_approved"
}
}With entry ids and late adjustments, charges included 200
Key used: a key holding every scope.
Request
curl -s "$TC_BASE/payroll.php?week=2026-09-25&include_entries=1&adjustments=1&limit=1" \ -H "Authorization: Bearer $TC_KEY"
Real reply (local test copy, fake demo data)
HTTP 200
{
"data": [
{
"worker_id": 351,
"employee_id": "AX025",
"worker_name": "Aisha Rahman",
"pay_week_start": "2026-09-21",
"pay_week_end": "2026-09-27",
"shifts": 1,
"paid_minutes": 168,
"paid_hours": 2.8,
"gross_pay": 37.8,
"charge_total": 52.92,
"margin": 15.12,
"base_pay_rate": 13.5,
"department": "WH-OPS",
"cost_centre": "WH-OPS",
"elements": {
"regular": {
"minutes": 168,
"hours": 2.8,
"pay": 37.8,
"pay_rate": 13.5,
"charge": 52.92,
"charge_rate": 18.9
},
"overtime": {
"minutes": 0,
"hours": 0,
"pay": 0,
"pay_rate": null,
"charge": 0,
"charge_rate": null
},
"weekend": {
"minutes": 0,
"hours": 0,
"pay": 0,
"pay_rate": null,
"charge": 0,
"charge_rate": null
},
"bank_holiday": {
"minutes": 0,
"hours": 0,
"pay": 0,
"pay_rate": null,
"charge": 0,
"charge_rate": null
},
"night": {
"minutes": 0,
"hours": 0,
"pay": 0,
"pay_rate": null,
"charge": 0,
"charge_rate": null
}
},
"pay_elements": [
{
"element": "regular",
"label": "Basic",
"sage_ref": "Basic",
"minutes": 168,
"hours": 2.8,
"pay_rate": 13.5,
"pay": 37.8,
"department": "WH-OPS",
"cost_centre": "WH-OPS",
"charge": 52.92,
"entry_ids": [
36685
]
}
],
"source": "live",
"flags": [],
"entry_ids": [
36685
... (64 more lines)Sync workers with updated_since
A nightly job that copies new and changed workers into another system. Store the time each run started and send it next time; de-duplicate on id. Key: workers:read (add pay:read only if the other system needs rates).
Node.js
// Node.js 18+ (global fetch)
const BASE = process.env.TC_BASE, KEY = process.env.TC_KEY;
async function tc(path, params = {}) {
const url = new URL(`${BASE}/${path}`);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
for (;;) {
const r = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
if (r.status === 429) { await new Promise(s => setTimeout(s, 1000 * (r.headers.get('Retry-After') || 60))); continue; }
const j = await r.json();
if (!r.ok) throw new Error(`${r.status} ${j.error_code}: ${j.message} (request ${r.headers.get('X-Request-Id')})`);
return j;
}
}
async function syncWorkers(lastRunStartedAt) { // e.g. '2026-10-05T23:00:00Z'
const startedAt = new Date(Date.now() - 60_000).toISOString(); // a minute early, from our own clock
let offset = 0, page;
do {
page = await tc('workers.php', { updated_since: lastRunStartedAt, limit: 100, offset });
for (const w of page.data) {
await upsertWorker(w.id, { // keyed on id: a repeat is harmless
employeeId: w.employee_id, name: w.name, active: w.active, status: w.status,
siteId: w.location_id, departmentId: w.department_id, costCodeId: w.cost_code_id,
});
}
offset += page.pagination.limit;
} while (page.pagination.has_more);
return startedAt; // store it for the next run
}Workers changed since a time (nightly sync) 200
Key used: a key holding every scope.
Request
curl -s "$TC_BASE/workers.php?updated_since=2026-01-01T00%3A00%3A00Z&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",
"hourly_rate": 12.7,
"charge_rate": 20.07,
"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
}
}Send clocks from another terminal
A turnstile, door reader or another time clock sends each badge read as a clock. Send the time it really happened in occurred_at (up to 15 minutes back) and an Idempotency-Key made from the read itself, so a retry after a network drop can never clock twice. Key: clock:write only.
Python
import hashlib
def send_badge_read(read): # read = {"badge": "AX014", "direction": "in", "at": "2026-10-06T05:58:41Z", "terminal": "GATE-2"}
key = hashlib.sha256(f"{read['terminal']}|{read['badge']}|{read['at']}".encode()).hexdigest()[:40]
try:
r = tc("POST", "clock.php", idempotency_key=key, body={
"employee_id": read["badge"],
"action": read["direction"], # "in" or "out"
"location_id": GATE_SITE_ID,
"occurred_at": read["at"],
})
return r["data"]["entry_id"]
except TempClockError as e:
if e.code in ("already_clocked_in", "no_open_clock_in"):
return None # a double read: nothing to do
if e.code in ("worker_pending", "worker_inactive", "not_found"):
alert_supervisor(read, e) # someone who should not be on site
return None
raiseClock 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"
}Clock out (the note is added after the clock-in note) 200
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":"out","notes":"Left by the main gate turnstile"}'Real reply (local test copy, fake demo data)
HTTP 200
{
"data": {
"entry_id": 40785,
"worker_id": 409,
"employee_id": "DOCS-77E88",
"worker_name": "Jordan Example",
"action": "clock_out",
"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,
"hours": 0.23,
"location_id": 37,
"scheduled_shift_id": null,
"risk_flags": [
"api_backdated"
],
"backdated": false
},
"message": "Jordan Example clocked out. Duration: 0h 14m"
}Book and cancel
PHP
<?php
function tc(string $method, string $path, ?array $body = null): array {
$ch = curl_init(getenv('TC_BASE') . '/' . $path);
$h = ['Authorization: Bearer ' . getenv('TC_KEY'), 'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16))];
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$j = json_decode((string)curl_exec($ch), true);
if (!empty($j['error'])) throw new RuntimeException($j['error_code'] . ': ' . $j['message']);
return $j;
}
$siteId = 37; $workerId = 377; // ids from your account (GET /locations.php, /workers.php)
// A requirement for 3 pickers, then one worker booked onto it.
$req = tc('POST', 'demands.php', ['location_id' => $siteId, 'shift_date' => '2026-10-28',
'start_time' => '06:00', 'end_time' => '14:00', 'headcount' => 3, 'job_role' => 'Picker']);
try {
$shift = tc('POST', 'shifts.php', ['worker_id' => $workerId, 'demand_id' => $req['data']['id'], 'status' => 'published']);
} catch (RuntimeException $e) {
// not_eligible: the worker cannot take it (block_codes says why, for example overlap or absence)
exit($e->getMessage() . "\n");
}
// The client cancels: the slot shows as open again and the cancelled-booking pay policy applies.
tc('POST', 'shifts.php?id=' . $shift['data']['id'] . '&action=cancel',
['status' => 'client_cancelled', 'cancel_reason' => 'Client reduced headcount']);Receive a webhook
Check the signature over the raw body in constant time, answer 200 quickly, and ignore a delivery id you have already handled. Full details and PHP and Python versions are on Webhooks.
Node.js
const crypto = require('crypto');
function verifyTempClock(rawBody, signatureHeader, secret) {
const want = Buffer.from('sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'));
const sent = Buffer.from(signatureHeader || '');
return want.length === sent.length && crypto.timingSafeEqual(want, sent);
}