Get started
API basics
Everything every request has in common, in one place: where to send it, what comes back, and how to handle errors and retries.
Regions and addresses#
| Region | API address | Keys start with |
|---|---|---|
| Canada | https://ca-api.clocklap.com | tt_live_ca_, tt_test_ca_ |
| United States | https://us-api.clocklap.com | tt_live_us_, tt_test_us_ |
Every path starts with /v1. Every response says which region answered in X-ClockLap-Region. Building something many workspaces will use? Find a workspace's region with GET /v1/directory/resolve?workspace=<slug> (no key needed), which returns its api_url.
Requests and responses#
- Send JSON with
Content-Type: application/json; you get JSON back. Field names aresnake_case. - IDs are strings with a prefix that says what they are:
usr_person,loc_location,ent_time entry,evt_event. - Lists come back as
{ "data": [...], "has_more": true, "next_cursor": "…" }. - Request bodies can be up to 2 MB; requests time out after 120 seconds.
- Every response has an
X-Request-Id(req_…). Include it when you contact us.
Dates and times#
- Moments are ISO 8601 with an offset:
2026-10-26T08:02:00-04:00or…Z. Inputs without an offset are refused. - Days are
YYYY-MM-DDand times of dayHH:MM, in the site's own time zone. - Time entries include their site's
timezoneandlocal_date, so you never have to work out which day a night shift belongs to.
Pagination#
List endpoints take limit (usually 1 to 500, default 100) and cursor. Keep passing next_cursor back until has_more is false. Cursors are opaque: don't build or change them. (The events API pages with after_id instead.)
async function* all(path, params = {}) {
let cursor;
do {
const q = new URLSearchParams({ ...params, limit: '500', ...(cursor ? { cursor } : {}) });
const res = await fetch(`https://ca-api.clocklap.com${path}?${q}`, { headers: { Authorization: `Bearer ${process.env.CLOCKLAP_API_KEY}` } });
const page = await res.json();
yield* page.data;
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
}
for await (const person of all('/v1/users', { status: 'active' })) console.log(person.name);def all_items(path, params=None):
params = dict(params or {}, limit=500)
while True:
page = requests.get(f"https://ca-api.clocklap.com{path}", headers=HEADERS, params=params).json()
yield from page["data"]
if not page.get("has_more"):
break
params["cursor"] = page["next_cursor"]
for person in all_items("/v1/users", {"status": "active"}):
print(person["name"])Errors#
Errors use the standard problem details format (application/problem+json). Read code in your program and show detail to people: it says what to fix in plain words.
{
"type": "https://developer.clocklap.com/errors/validation_failed",
"title": "Invalid request",
"status": 400,
"code": "validation_failed",
"detail": "clock_in_at: Invalid ISO datetime",
"request_id": "req_4f1c2a9b7d3e5f60"
}The full list is on Errors.
Rate limits#
Each API key can make 600 requests a minute. Every response tells you where you stand:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the window |
RateLimit-Remaining | Requests left |
RateLimit-Reset | Seconds until the window resets |
Over the limit you get 429 rate_limited: wait for RateLimit-Reset seconds and try again. To read lots of data, use limit=500 and webhooks instead of polling.
Safe retries (idempotency)#
Networks fail. To retry a POST, PATCH, PUT or DELETE without doing it twice, send an Idempotency-Key header with a unique value (a UUID is ideal, up to 120 characters). ClockLap remembers the answer for 24 hours: a retry with the same key gets the same response, marked Idempotent-Replayed: true, and nothing happens twice.
curl -X POST https://ca-api.clocklap.com/v1/time-entries \
-H "Authorization: Bearer $CLOCKLAP_API_KEY" \
-H "Idempotency-Key: 6f1d9a2e-2b7c-4c55-9a51-0c8b8f1f7e21" \
-H "Content-Type: application/json" \
-d '{"user_id":"usr_…","location_id":"loc_…","clock_in_at":"2026-10-26T08:00:00-04:00","clock_out_at":"2026-10-26T16:00:00-04:00","reason":"Forgot to clock in"}'| If… | You get |
|---|---|
| the same key is sent again | the first response again, with Idempotent-Replayed: true |
| the first request is still running | 409 idempotency_key_busy: wait a moment and retry |
| the same key is used for a different request | 409 idempotency_key_reused: make a new key |
Versions and changes#
The API is /v1 and changes to it are additive: new fields and endpoints can appear, so ignore fields you don't know. Each response says the API version in ClockLap-Api-Version (now 2026-10-01). If something is ever retired, its responses carry Deprecation and Sunset headers well before, with a Link to its replacement.
OpenAPI#
The full description of the API (OpenAPI 3.1) is at /docs/json on each region, for example ca-api.clocklap.com/docs/json. Import it into Postman or Insomnia, or generate a client from it.