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.

Errors

The TimeTracking API error envelope, every HTTP status and error code, real error messages captured from the API, and how a client should react to each.

Last updated: · Markdown version

Envelope

Every failure returns JSON in this shape:

{ "error": { "code": "invalid", "message": "Invalid input", "issues": [ … ] } }
  • code is stable and meant for machines. message is meant for people and may change. Never parse message.
  • issues is present only on validation errors. It is an array of zod issues, each with path (field), code and message.
  • Database errors are never shown raw. Unexpected failures return 500 {"code":"internal","message":"Internal error"}.

Status codes and codes

HTTP code Meaning Retry? What to do
401 unauthenticated Missing, invalid or revoked credential no Fix the key or token
403 forbidden Role or scope not allowed no Use a key with the right scope (write/admin) or an admin user token (see authentication)
404 not_found Unknown ID, or the record belongs to another org no Check the ID; resync your ID mapping
409 conflict The current state does not allow this no (re-read first) Read the record, then decide
409 invalid_transition · reason_required · forbidden Status machine refused the transition no See status lifecycle
422 invalid Validation failed, or the body is not valid JSON no Fix the request; see issues
423 locked Day is exported or locked no Record a correction in an open period
429 rate_limited More than 600 requests in the current minute for this key yes, after Retry-After See rate limits
500 internal Unexpected server error yes, with backoff and an Idempotency-Key Contact support if it persists

Real examples (captured)

401: no header

{ "error": { "code": "unauthenticated", "message": "Missing bearer token" } }

401: wrong or revoked key

{ "error": { "code": "unauthenticated", "message": "Invalid or revoked API key" } }

403: write with a read key

{ "error": { "code": "forbidden", "message": "API key is read-only" } }

403: admin-only endpoint with a write key (/webhooks, /periods/lock, /kiosk-devices; use an admin key or admin user)

{ "error": { "code": "forbidden", "message": "Admin role required" } }

403: creating API keys with any API key (even an admin key)

{ "error": { "code": "forbidden", "message": "Not available with an API key" } }

404

{ "error": { "code": "not_found", "message": "Placement not found" } }
{ "error": { "code": "not_found", "message": "Export mapping not found" } }
{ "error": { "code": "not_found", "message": "Not found" } }

409

{ "error": { "code": "conflict", "message": "Conflict already resolved" } }
{ "error": { "code": "conflict", "message": "1 day(s) in this period are not exported yet" } }
{ "error": { "code": "conflict", "message": "Client has placements; deactivate those instead" } }
{ "error": { "code": "conflict", "message": "These hours are already submitted; ask the office to change them" } }
{ "error": { "code": "conflict", "message": "Already exists" } }

422: validation (POST /claims with a bad UUID, a bad date and no hours)

{
  "error": {
    "code": "invalid",
    "message": "Invalid input",
    "issues": [
      { "origin": "string", "code": "invalid_format", "format": "uuid", "path": ["placementId"], "message": "Invalid UUID" },
      { "origin": "string", "code": "invalid_format", "format": "regex", "pattern": "/^\\d{4}-\\d{2}-\\d{2}$/", "path": ["workDate"], "message": "Invalid string: must match pattern /^\\d{4}-\\d{2}-\\d{2}$/" },
      { "code": "custom", "path": [], "message": "Provide either minutes or start and end time" }
    ]
  }
}

422: other cases

{ "error": { "code": "invalid", "message": "Body is not valid JSON" } }
{ "error": { "code": "invalid", "message": "Nothing approved to export in this period" } }
{ "error": { "code": "invalid", "message": "Invalid cursor" } }
{ "error": { "code": "invalid", "message": "Idempotency-Key was already used for a different request" } }
{ "error": { "code": "invalid", "message": "Person and client must exist (by id or external ref) before the placement" } }
{ "error": { "code": "invalid", "message": "PIN must be 6 digits" } }

423

{ "error": { "code": "locked", "message": "This day is exported or locked; book a correction in an open period" } }

Errors inside batch responses

Batch endpoints (POST /people/batch, /clients/batch, /placements/batch, /claims/batch) return 200 even when some rows fail. Each row reports its own error:

{ "data": { "ok": 1, "failed": 1, "results": [
  { "index": 0, "ok": true, "data": { "id": "02595da9-06ea-45d1-a5e8-927d854a2c46", "…": "…" } },
  { "index": 1, "ok": false, "error": { "code": "invalid", "message": "firstName: Too small: expected string to have >=1 characters; lastName: Invalid input: expected string, received undefined" } }
] } }

The batch itself fails with 422 only when items is missing, empty or has more than 500 entries.

Client handling pattern

switch (res.status) {
  case 429: await sleep(Number(res.headers.get("retry-after") ?? 60) * 1000); return retry();
  case 500: case 502: case 503: case 504: return retryWithBackoff(); // keep the same Idempotency-Key
  case 401: case 403: throw new ConfigError(json.error.message);   // alert a human
  case 409: case 423: return handleState(json.error);              // re-read, or correct in an open period
  case 422: throw new ValidationError(json.error.issues ?? json.error.message);
}

In the CLI, an API error makes tt exit with code 1 and prints error: <message> [<code> <status>]. The MCP server returns { "error": { "code", "message", "status" } }.