Skip to content

Request an integration

Tell us which system you want to connect. We review every request weekly and publish a step-by-step guide or build the connection.

Idempotency

Safe retries in the TimeTracking API: the Idempotency-Key header, upsert by external ID, client-generated claim IDs, and which endpoints honour which mechanism.

Last updated: · Markdown version

Networks fail. A retried write must not create duplicate hours or people. TimeTracking offers three mechanisms.

Mechanism Scope Use for
Idempotency-Key header every authenticated POST/PUT/PATCH/DELETE route Any retry of a write
PUT /…/by-external/{source}/{ref} people, clients, placements Syncing master data from another system: naturally idempotent
Client-generated id on POST /claims claims Offline-first apps; dedupe at claim level

Idempotency-Key header

curl -s -X POST "$TT/claims" \
  -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -H "Idempotency-Key: bullhorn-timecard-881-2026-12-10" \
  -d '{"placementId":"96574dce-db96-4da1-82cf-de32021472d9","workDate":"2026-12-10","minutes":450,"source":"manual_office"}'

Rules:

  • The key is 8–200 characters, unique per org. Use a UUID or a deterministic key derived from your source record.
  • The first successful (2xx) response is stored for 24 hours. A retry with the same key, method and path returns that stored response with the header idempotent-replayed: true. The handler does not run again.
  • Failed responses (4xx/5xx) are not stored. You can fix the request and retry with the same key.
  • Reusing a key with a different method or path → 422 {"code":"invalid","message":"Idempotency-Key was already used for a different request"}.
  • A key of the wrong length → 422 {"code":"invalid","message":"Idempotency-Key must be 8–200 characters"}.
  • The body is not compared. Same key, same path, different body → the stored response is replayed. Derive keys from the content you send.

Captured replay:

HTTP/1.1 200 OK
idempotent-replayed: true
content-type: application/json

{"data":{"id":"498795f4-4027-4888-84a6-885952f0ab8b","minutes":450,"status":"draft", "…": "…"}}

Where it applies

Every authenticated write route honours the header, including POST /exports (preview and commit), /days/approve, /approval-links, /conflicts/{id}/resolve, /import, /periods/lock, /weeks/submit, the batch endpoints and the by-external upserts. Captured replay on POST /days/approve:

HTTP/1.1 200 OK
idempotent-replayed: true

{"data":{"skipped":1,"approved":0,"waitingForClient":0}}

The public client-approval routes (/client/approve/{token}, /code, /verify) take no credential and do not use the header.

Use a key on POST /approval-links in particular: every un-keyed call creates and emails a new link. Use one on export commits too: a replay returns the original {runId, …} and does not try to export again.

Upsert by external ID

PUT /people/by-external/{source}/{ref} (and /clients/…, /placements/…) creates the record the first time and updates it after that. The response says which happened:

{ "data": { "id": "69a47995-bc6a-4bc9-9b9b-070935b974bb", "first_name": "Eva", "last_name": "Docs", "external_source": "bullhorn", "external_ref": "12345", "created": true, "…": "…" } }

The same call again returns "created": false. source matches [a-z0-9_-]{1,40} (case-insensitive). ref is up to 120 characters; URL-encode it. The body is the same full object as POST (camelCase). Omitted optional fields are set to null.

Batch variant: POST /people/batch (and /clients/batch, /placements/batch). Rows with externalSource + externalRef upsert by external ID.

Client-generated claim IDs

POST /claims accepts an optional id (UUID v4). If a claim with that id already exists, the call returns the current day and inserts nothing. This works on all claim routes, including POST /claims/batch items.

{ "id": "3b1f8e2a-6c1d-4c8e-9f7a-2d5e0b9c4a11", "placementId": "…", "workDate": "2026-12-10", "minutes": 450, "source": "import" }
Write Do this
Master data from ATS/CRM PUT /…/by-external/{source}/{ref} or the batch endpoint, plus Idempotency-Key on the batch
Hours from another system POST /claims/batch with a deterministic id per row (e.g. UUIDv5 of source:recordId:date) and sourceRef
One-off writes from scripts or iPaaS Idempotency-Key: <uuid>; keep the same key across retries
Export commit / lock A human decides to commit or lock. Send an Idempotency-Key so a network retry of that one decision is safe

The CLI (tt api) does not send an Idempotency-Key. Use curl or your own HTTP client for retry-safe scripts.