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.

API reference

Every TimeTracking REST API v1.1 endpoint with parameters, request fields, who may call it, and real captured request/response examples.

Last updated: · Markdown version

Generated from the live OpenAPI 3.1 spec (GET /api/v1/openapi.json, version 1.1.0, 64 paths, 21 webhook events) and enriched with real responses captured against the API on 2026-10-23 (demo organisation; IDs are real demo IDs, keys and secrets are masked, long lists are trimmed to two items). Field names in request bodies are camelCase; field names in responses are snake_case database columns unless noted.

export TT=https://app.timetracking.tech/api/v1
export KEY=tt_live_…   # see ../api/authentication.md

Conventions that apply to every endpoint: authentication, errors ({ "error": { "code", "message" } }, 401/403/404/409/422/423/429), rate limits, pagination, idempotency. Every endpoint can return the standard error statuses; only notable cases are shown.

Who can call it uses these terms: read key = API key with only the read scope (acts like an accountant), write key = API key with write (acts like an office user), admin key = API key with admin (implies write; acts like an admin, but can never create keys or change roles), office user / admin user / employee token = a signed-in user's access token. See authentication.

Contents

Hours

GET /org/week

Hours of all placements for one week, with KPIs and open conflicts.

  • operationId: getOrgWeek
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
week query string (date) no Any date in the ISO week; defaults to the current week (org timezone).

Example (captured from the API)

curl -s -X GET "$TT/org/week?week=2026-11-30" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "weekStart": "2026-11-30",
    "weekEnd": "2026-12-06",
    "rows": [
      {
        "placementId": "00000000-0000-4000-8000-0000000000e4",
        "personId": "00000000-0000-4000-8000-0000000000d4",
        "personName": "Sanne Janssen",
        "employeeNumber": "1004",
        "clientName": "Bakkerij Ruijters",
        "clientId": "00000000-0000-4000-8000-0000000000c3",
        "approvalMode": "office",
        "days": [
          null,
          {
            "dayId": "0ce9b17d-40ab-4542-937a-de57896a7358",
            "minutes": 480,
            "status": "submitted",
            "conflict": false,
            "clientApproved": false,
            "officeApproved": false
          }
        ],
        "totalMinutes": 930,
        "status": "submitted"
      },
      {
        "placementId": "00000000-0000-4000-8000-0000000000e2",
        "personId": "00000000-0000-4000-8000-0000000000d2",
        "personName": "Fatima El Amrani",
        "employeeNumber": "1002",
        "clientName": "Logistiek Parkstad",
        "clientId": "00000000-0000-4000-8000-0000000000c2",
        "approvalMode": "client",
        "days": [
          null,
          null
        ],
        "totalMinutes": 0,
        "status": "empty"
      }
    ],
    "conflicts": [],
    "kpis": {
      "placements": 4,
      "submitted": 1,
      "approved": 0,
      "conflicts": 0,
      "missing": 3,
      "totalMinutes": 930
    }
  }
}

Late status. Every row has late (boolean) and kpis.late counts people. A person is late when they have an active placement in the week, the hours deadline of the payroll period that contains the week has passed in the org timezone (default Monday 09:00 after the period; see GET /org/reminders), and none of their days in the week is submitted (drafts do not count). deadline is { "date": "2026-12-07", "minute": 540, "passed": false }.

GET /org/today

The office "Today" board: everyone with a published shift, kiosk clock-in, recorded hours or approved leave today (org timezone).

  • operationId: getOrgToday
  • Who can call it: read key, write key, office user

Each row has personId, name, clientName, shift ({start, end} in minutes of day, or null), clockIn (minutes of day of the first kiosk clock-in, or null), minutes recorded today, lateBy (minutes after the shift start) and state:

State Meaning
notIn Shift started more than 5 minutes ago, no clock-in and no hours
late Clocked in at a kiosk more than 5 minutes after the shift start, still in
in Clocked in at a kiosk, still in
done Clocked out, or hours for today are recorded (app, web, import, office)
starts Shift later today
leave Approved leave today

Rows are sorted with notIn and late first. Phone timers are not shown live: their hours appear when they are saved. No location or activity data is used.

curl -s "$TT/org/today" -H "Authorization: Bearer $KEY"

GET /days

Resolved hours over any date range.

  • operationId: listDays
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
from query string (date) yes First date (inclusive).
to query string (date) yes Last date (inclusive).
status query string no Comma list of statuses
person_id query string (uuid) no Filter
placement_id query string (uuid) no Filter
client_id query string (uuid) no Filter
updated_since query string (date-time) no Only rows changed at or after this instant (incremental sync).
limit query integer no Page size (paginated lists). min 1; max 500; default 100
cursor query string no Opaque cursor from next_cursor of the previous page.

Example (captured from the API)

curl -s -X GET "$TT/days?from=2026-11-30&to=2026-12-13&limit=3" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "items": [
      {
        "id": "816b451d-6921-42b9-af02-a1f079af77ce",
        "placement_id": "00000000-0000-4000-8000-0000000000e4",
        "person_id": "00000000-0000-4000-8000-0000000000d4",
        "work_date": "2026-12-03",
        "minutes": 480,
        "start_min": null,
        "end_min": null,
        "break_min": 0,
        "note": null,
        "office_note": null,
        "client_note": null,
        "status": "draft",
        "resolution": "auto_single_source",
        "has_conflict": false,
        "approved_by_office_at": null,
        "approved_by_client": null,
        "approved_by_client_at": null,
        "export_run_id": null,
        "version": 1,
        "updated_at": "2026-10-23T19:08:42.625245+00:00"
      },
      {
        "id": "0ce9b17d-40ab-4542-937a-de57896a7358",
        "placement_id": "00000000-0000-4000-8000-0000000000e4",
        "person_id": "00000000-0000-4000-8000-0000000000d4",
        "work_date": "2026-12-01",
        "minutes": 480,
        "start_min": 420,
        "end_min": 930,
        "break_min": 30,
        "note": "Matches the signed paper timesheet",
        "office_note": null,
        "client_note": null,
        "status": "exported",
        "resolution": "office_choice",
        "has_conflict": false,
        "approved_by_office_at": "2026-10-23T19:08:42.47+00:00",
        "approved_by_client": null,
        "approved_by_client_at": null,
        "export_run_id": "239a1898-fab5-486c-a2be-92d939be90cc",
        "version": 6,
        "updated_at": "2026-10-23T19:08:42.871612+00:00"
      }
    ],
    "next_cursor": "eyJ1IjoiMjAyNi0xMC0yM1QxOTowODo0Mi44NzE2MTIrMDA6MDAiLCJpIjoiODE3MzkzNDctMDMzMC00OWViLWE1NTktODc3Y2IxODg5ZDYzIn0"
  }
}

GET /claims

Raw claims over a date range.

  • operationId: listClaims
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
from query string (date) yes First date (inclusive).
to query string (date) yes Last date (inclusive).
placement_id query string (uuid) no Filter
source query app | kiosk | mail | import | predicted | manual_office | client_portal no Filter
updated_since query string (date-time) no Only rows changed at or after this instant (incremental sync).
limit query integer no Page size (paginated lists). min 1; max 500; default 100
cursor query string no Opaque cursor from next_cursor of the previous page.

Example (captured from the API)

curl -s -X GET "$TT/claims?from=2026-12-01&to=2026-12-13&placement_id=96574dce-db96-4da1-82cf-de32021472d9&limit=2" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "items": [
      {
        "id": "83b55c9f-e50e-4cdd-9b5f-8e2203614e73",
        "placement_id": "96574dce-db96-4da1-82cf-de32021472d9",
        "work_date": "2026-12-07",
        "source": "import",
        "minutes": 480,
        "start_min": 420,
        "end_min": 930,
        "break_min": 30,
        "note": null,
        "confidence": null,
        "source_ref": "bullhorn-timecard-881",
        "created_at": "2026-10-23T19:22:17.427293+00:00"
      },
      {
        "id": "ee03befd-2b7c-4fa6-a588-6cf1f1924955",
        "placement_id": "96574dce-db96-4da1-82cf-de32021472d9",
        "work_date": "2026-12-08",
        "source": "import",
        "minutes": 480,
        "start_min": null,
        "end_min": null,
        "break_min": 0,
        "note": null,
        "confidence": null,
        "source_ref": "bullhorn-timecard-881",
        "created_at": "2026-10-23T19:22:17.465052+00:00"
      }
    ],
    "next_cursor": "eyJ1IjoiMjAyNi0xMC0yM1QxOToyMjoxNy40NjUwNTIrMDA6MDAiLCJpIjoiZWUwM2JlZmQtMmI3Yy00ZmE2LWE1ODgtNmNmMWYxOTI0OTU1In0"
  }
}

POST /claims

Record hours (start/end/break or minutes).

  • operationId: createClaim
  • Who can call it: write key / office user (sources manual_office, mail, import, client_portal); employee token (own placements, app/kiosk)
  • Notes: Employees may use source app/kiosk for their own placements; office keys may also use mail, import, manual_office. The day is re-resolved immediately (conflict detection).
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
placementId string (uuid) yes
workDate string yes pattern ^\d{4}-\d{2}-\d{2}$
startMin integer no min 0; max 1439
endMin integer no min 0; max 1439
breakMin integer no min 0; max 720; default 0
minutes integer no min 0; max 1440
note string no max length 1000
source app | kiosk | mail | import | manual_office | client_portal no default "app"
confidence number no min 0; max 1
sourceRef string no max length 200
deviceTime string (date-time) no

Example (captured from the API)

curl -s -X POST "$TT/claims" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"placementId": "00000000-0000-4000-8000-0000000000e4", "workDate": "2026-12-01", "startMin": 420, "endMin": 930, "breakMin": 30, "source": "manual_office", "note": "Entered from paper timesheet"}'

Response 200:

{
  "data": {
    "id": "0ce9b17d-40ab-4542-937a-de57896a7358",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "placement_id": "00000000-0000-4000-8000-0000000000e4",
    "person_id": "00000000-0000-4000-8000-0000000000d4",
    "work_date": "2026-12-01",
    "minutes": 480,
    "start_min": 420,
    "end_min": 930,
    "break_min": 30,
    "note": "Entered from paper timesheet",
    "status": "draft",
    "resolution": "auto_single_source",
    "claim_ids": [
      "7d78dbdb-d70c-4960-8796-4cedfa3454e6"
    ],
    "has_conflict": false,
    "approved_by_office": null,
    "approved_by_office_at": null,
    "approved_by_client": null,
    "approved_by_client_at": null,
    "export_run_id": null,
    "version": 1,
    "updated_at": "2026-10-23T19:08:41.482007+00:00",
    "office_note": null,
    "client_note": null
  }
}

Example — Second source disagrees → conflict

curl -s -X POST "$TT/claims" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"placementId": "00000000-0000-4000-8000-0000000000e4", "workDate": "2026-12-01", "minutes": 450, "source": "mail", "sourceRef": "docs-capture"}'

Response 200:

{
  "data": {
    "id": "0ce9b17d-40ab-4542-937a-de57896a7358",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "placement_id": "00000000-0000-4000-8000-0000000000e4",
    "person_id": "00000000-0000-4000-8000-0000000000d4",
    "work_date": "2026-12-01",
    "minutes": null,
    "start_min": 420,
    "end_min": 930,
    "break_min": 30,
    "note": "Entered from paper timesheet",
    "status": "draft",
    "resolution": null,
    "claim_ids": [
      "7d78dbdb-d70c-4960-8796-4cedfa3454e6",
      "ddce5803-8f75-4ace-9d5c-e0bf9fae1d8a"
    ],
    "has_conflict": true,
    "approved_by_office": null,
    "approved_by_office_at": null,
    "approved_by_client": null,
    "approved_by_client_at": null,
    "export_run_id": null,
    "version": 2,
    "updated_at": "2026-10-23T19:08:41.746852+00:00",
    "office_note": null,
    "client_note": null
  }
}

Example — Exported day → refused before anything is stored

curl -s -X POST "$TT/claims" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"placementId": "00000000-0000-4000-8000-0000000000e4", "workDate": "2026-12-01", "minutes": 300, "source": "manual_office"}'

Response 423:

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

POST /claims/batch

Record up to 500 claims; per-row results.

  • operationId: batchClaims
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
items array of object yes
items[].id string (uuid) no
items[].placementId string (uuid) yes
items[].workDate string yes pattern ^\d{4}-\d{2}-\d{2}$
items[].startMin integer no min 0; max 1439
items[].endMin integer no min 0; max 1439
items[].breakMin integer no min 0; max 720; default 0
items[].minutes integer no min 0; max 1440
items[].note string no max length 1000
items[].source app | kiosk | mail | import | manual_office | client_portal no default "app"
items[].confidence number no min 0; max 1
items[].sourceRef string no max length 200
items[].deviceTime string (date-time) no

Example (captured from the API)

curl -s -X POST "$TT/claims/batch" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"items": [{"placementId": "96574dce-db96-4da1-82cf-de32021472d9", "workDate": "2026-12-07", "startMin": 420, "endMin": 930, "breakMin": 30, "source": "import", "sourceRef": "bullhorn-timecard-881"}, {"placementId": "96574dce-db96-4da1-82cf-de32021472d9", "workDate": "2026-12-08", "minutes": 480, "source": "import", "sourceRef": "bullhorn-timecard-881"}, {"placementId": "96574dce-db96-4da1-82cf-de32021472d9", "workDate": "2026-12-09"}]}'

Response 200:

{
  "data": {
    "ok": 2,
    "failed": 1,
    "results": [
      {
        "index": 0,
        "ok": true,
        "data": {
          "id": "f642c482-ca5b-4d57-bb2a-0b6a31997f83",
          "org_id": "00000000-0000-4000-8000-000000000001",
          "placement_id": "96574dce-db96-4da1-82cf-de32021472d9",
          "person_id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
          "work_date": "2026-12-07",
          "minutes": 480,
          "start_min": 420,
          "end_min": 930,
          "break_min": 30,
          "note": null,
          "status": "draft",
          "resolution": "auto_single_source",
          "claim_ids": [
            "83b55c9f-e50e-4cdd-9b5f-8e2203614e73"
          ],
          "has_conflict": false,
          "approved_by_office": null,
          "approved_by_office_at": null,
          "approved_by_client": null,
          "approved_by_client_at": null,
          "export_run_id": null,
          "version": 1,
          "updated_at": "2026-10-23T19:22:17.449068+00:00",
          "office_note": null,
          "client_note": null
        }
      },
      {
        "index": 1,
        "ok": true,
        "data": {
          "id": "482b4f99-74ec-472f-b6af-e92444fe2dce",
          "org_id": "00000000-0000-4000-8000-000000000001",
          "placement_id": "96574dce-db96-4da1-82cf-de32021472d9",
          "person_id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
          "work_date": "2026-12-08",
          "minutes": 480,
          "start_min": null,
          "end_min": null,
          "break_min": 0,
          "note": null,
          "status": "draft",
          "resolution": "auto_single_source",
          "claim_ids": [
            "ee03befd-2b7c-4fa6-a588-6cf1f1924955"
          ],
          "has_conflict": false,
          "approved_by_office": null,
          "approved_by_office_at": null,
          "approved_by_client": null,
          "approved_by_client_at": null,
          "export_run_id": null,
          "version": 1,
          "updated_at": "2026-10-23T19:22:17.495383+00:00",
          "office_note": null,
          "client_note": null
        }
      }
    ]
  }
}

POST /weeks/submit

Submit a person's draft days of a week.

  • operationId: submitWeek
  • Who can call it: write key / office user (with personId); employee token (own week)
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
weekStart string yes
personId string (uuid) no

Example (captured from the API)

curl -s -X POST "$TT/weeks/submit" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"weekStart": "2026-11-30", "personId": "00000000-0000-4000-8000-0000000000d4"}'

Response 200:

{
  "data": {
    "submitted": 2
  }
}

POST /days/approve

Office approval of submitted days.

  • operationId: approveDays
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
dayIds array of string (uuid) yes

Example (captured from the API)

curl -s -X POST "$TT/days/approve" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"dayIds": ["0ce9b17d-40ab-4542-937a-de57896a7358", "81739347-0330-49eb-a559-877cb1889d63"]}'

Response 200:

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

Example — Client-approval placement → waiting for client

curl -s -X POST "$TT/days/approve" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"dayIds": ["0c0dbf6f-9bc8-48b7-bfe1-cf8f6566a878"]}'

Response 200:

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

GET /conflicts

Open conflicts for a week.

  • operationId: listConflicts
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
week query string (date) no Any date in the ISO week; defaults to the current week (org timezone).

Example (captured from the API)

curl -s -X GET "$TT/conflicts?week=2026-11-30" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": [
    {
      "id": "c51f9c0d-da32-4edf-86a8-0ba2a86d93a7",
      "dayId": "0ce9b17d-40ab-4542-937a-de57896a7358",
      "workDate": "2026-12-01",
      "personName": "Sanne Janssen",
      "clientName": "Bakkerij Ruijters",
      "candidates": [
        {
          "source": "manual_office",
          "claimId": "7d78dbdb-d70c-4960-8796-4cedfa3454e6",
          "minutes": 480
        },
        {
          "source": "mail",
          "claimId": "ddce5803-8f75-4ace-9d5c-e0bf9fae1d8a",
          "minutes": 450
        }
      ]
    }
  ]
}

POST /conflicts/{id}/resolve

Pick the winning claim of a conflict.

  • operationId: resolveConflict
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
claimId string (uuid) yes
note string no

Example (captured from the API)

curl -s -X POST "$TT/conflicts/c51f9c0d-da32-4edf-86a8-0ba2a86d93a7/resolve" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"claimId": "7d78dbdb-d70c-4960-8796-4cedfa3454e6", "note": "Matches the signed paper timesheet"}'

Response 200:

{
  "data": {
    "resolved": true
  }
}

Approvals

POST /approval-links

Create a client approval magic link (emailed to the client contact).

  • operationId: createApprovalLink
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
clientId string (uuid) yes
startsOn string yes
endsOn string yes

Example (captured from the API)

curl -s -X POST "$TT/approval-links" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"clientId": "00000000-0000-4000-8000-0000000000c2", "startsOn": "2026-11-30", "endsOn": "2026-12-06"}'

Response 200:

{
  "data": {
    "url": "http://localhost:3001/approve/<token>",
    "expiresAt": "2026-11-06T19:09:09.202Z",
    "emailed": "ruud@logistiekparkstad.example"
  }
}

GET /client/approve/{token}

What the client sees behind a link (public; the token is the credential).

  • operationId: getClientApproval
  • Who can call it: public — the token in the URL is the credential (no Authorization header)
Parameter In Type Required Notes
token path string yes Magic-link token from the client approval URL.

Example (captured from the API)

curl -s -X GET "$TT/client/approve/XDcfG_swBYtm2BDrcnYJZh-9rXKDxEqXUAxVNuxB4Qs"

Response 200:

{
  "data": {
    "linkId": "b374151b-659a-4133-889e-1bd262647f48",
    "requiresCode": true,
    "contactEmailMasked": "r•••@logistiekparkstad.example",
    "orgName": "Jobhunting Uitzenden (demo)",
    "clientName": "Logistiek Parkstad",
    "startsOn": "2026-11-30",
    "endsOn": "2026-12-06",
    "completed": false,
    "rows": [
      {
        "dayId": "0c0dbf6f-9bc8-48b7-bfe1-cf8f6566a878",
        "personName": "Piotr Nowak",
        "roleTitle": "Heftruckchauffeur",
        "workDate": "2026-12-01",
        "minutes": 480,
        "startMin": 360,
        "endMin": 870,
        "breakMin": 30,
        "note": null,
        "status": "submitted",
        "clientApproved": false
      }
    ]
  }
}

POST /client/approve/{token}

Submit client decisions with signature (needs x-approval-proof when a code is required).

  • operationId: submitClientApproval
  • Who can call it: public — the token in the URL is the credential (no Authorization header)
Parameter In Type Required Notes
token path string yes Magic-link token from the client approval URL.
x-approval-proof header string no

Request body (JSON)

Field Type Required Constraints
signerName string yes min length 2; max length 120
signerRole string no max length 120
signature string yes min length 2; max length 120
decisions array of object yes min items 1
decisions[].dayId string (uuid) yes
decisions[].decision approved | corrected | rejected yes
decisions[].correctedMinutes integer no min 0; max 1440
decisions[].reason string no max length 500

Example (captured from the API)

curl -s -X POST "$TT/client/approve/XDcfG_swBYtm2BDrcnYJZh-9rXKDxEqXUAxVNuxB4Qs" \
  -H "x-approval-proof: <proof>" \
  -H "content-type: application/json" \
  -d '{"signerName": "Ruud Hendriks", "signerRole": "Operations manager", "signature": "R. Hendriks", "decisions": [{"dayId": "0c0dbf6f-9bc8-48b7-bfe1-cf8f6566a878", "decision": "approved"}]}'

Response 200:

{
  "data": {
    "linkId": "b374151b-659a-4133-889e-1bd262647f48",
    "requiresCode": true,
    "contactEmailMasked": "r•••@logistiekparkstad.example",
    "orgName": "Jobhunting Uitzenden (demo)",
    "clientName": "Logistiek Parkstad",
    "startsOn": "2026-11-30",
    "endsOn": "2026-12-06",
    "completed": true,
    "rows": [
      {
        "dayId": "0c0dbf6f-9bc8-48b7-bfe1-cf8f6566a878",
        "personName": "Piotr Nowak",
        "roleTitle": "Heftruckchauffeur",
        "workDate": "2026-12-01",
        "minutes": 480,
        "startMin": 360,
        "endMin": 870,
        "breakMin": 30,
        "note": null,
        "status": "approved",
        "clientApproved": true
      }
    ]
  }
}

Example — Without x-approval-proof when a code is required

curl -s -X POST "$TT/client/approve/XDcfG_swBYtm2BDrcnYJZh-9rXKDxEqXUAxVNuxB4Qs" \
  -H "content-type: application/json" \
  -d '{"signerName": "Ruud Hendriks", "signature": "R. Hendriks", "decisions": [{"dayId": "0c0dbf6f-9bc8-48b7-bfe1-cf8f6566a878", "decision": "approved"}]}'

Response 403:

{
  "error": {
    "code": "forbidden",
    "message": "Confirm the code sent by email first"
  }
}

POST /client/approve/{token}/code

Email a one-time code to the client contact.

  • operationId: sendClientCode
  • Who can call it: public — token is the credential
Parameter In Type Required Notes
token path string yes Magic-link token from the client approval URL.

Example (captured from the API)

curl -s -X POST "$TT/client/approve/XDcfG_swBYtm2BDrcnYJZh-9rXKDxEqXUAxVNuxB4Qs/code"

Response 200:

{
  "data": {
    "sentTo": "r•••@logistiekparkstad.example"
  }
}

POST /client/approve/{token}/verify

Exchange the code for a 2-hour proof.

  • operationId: verifyClientCode
  • Who can call it: public — token is the credential
Parameter In Type Required Notes
token path string yes Magic-link token from the client approval URL.

Request body (JSON)

Field Type Required Constraints
code string yes

Example (captured from the API)

curl -s -X POST "$TT/client/approve/XDcfG_swBYtm2BDrcnYJZh-9rXKDxEqXUAxVNuxB4Qs/verify" \
  -H "content-type: application/json" \
  -d '{"code": "180275"}'

Response 200:

{
  "data": {
    "proof": "1792789749942.317037da…"
  }
}

Exports

POST /exports

Preview or commit a payroll export (mapping id e.g. preset-sdworx).

  • operationId: runExport
  • Who can call it: write key, admin key, office user (read keys get 403: POST is blocked for read keys, also for preview)
  • Notes: commit is irreversible: included days become exported.
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
mappingId string yes
startsOn string yes
endsOn string yes
mode preview | commit yes

Example (captured from the API)

curl -s -X POST "$TT/exports" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"mappingId": "preset-generic", "startsOn": "2026-11-30", "endsOn": "2026-12-06", "mode": "preview"}'

Response 200:

{
  "data": {
    "content": "employee_number;employee_name;client;placement;date;hour_type;hours\r\n1004;Sanne Janssen;Bakkerij Ruijters;PL-2026-104;2026-12-01;normal;8.00\r\n1004;Sanne Janssen;Bakkerij Ruijters;PL-2026-104;2026-12-02;normal;7.50\r\n",
    "rows": [
      [
        "1004",
        "Sanne Janssen"
      ],
      [
        "1004",
        "Sanne Janssen"
      ]
    ],
    "issues": [],
    "ok": true,
    "dayCount": 2,
    "pendingCount": 1,
    "warnings": [
      {
        "key": "00000000-0000-4000-8000-0000000000e4",
        "date": "2026-12-02",
        "code": "break_too_short",
        "message": "Break 0 min, at least 30 min required",
        "person": "Sanne Janssen"
      }
    ],
    "packName": "NL basis (voorbeeld — afstemmen op CAO)"
  }
}

Example — Commit (irreversible)

curl -s -X POST "$TT/exports" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"mappingId": "preset-generic", "startsOn": "2026-11-30", "endsOn": "2026-12-06", "mode": "commit"}'

Response 200:

{
  "data": {
    "runId": "239a1898-fab5-486c-a2be-92d939be90cc",
    "rowCount": 2,
    "contentHash": "1f92f2a1b4c52a8bcc43c40740e3f29b83e475de5ebacce7807c51808a04f6eb",
    "content": "employee_number;employee_name;client;placement;date;hour_type;hours\r\n1004;Sanne Janssen;Bakkerij Ruijters;PL-2026-104;2026-12-01;normal;8.00\r\n1004;Sanne Janssen;Bakkerij Ruijters;PL-2026-104;2026-12-02;normal;7.50\r\n"
  }
}

GET /exports/runs

Committed export runs.

  • operationId: listExportRuns
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
updated_since query string (date-time) no Only rows changed at or after this instant (incremental sync).
limit query integer no Page size (paginated lists). min 1; max 500; default 100
cursor query string no Opaque cursor from next_cursor of the previous page.

Example (captured from the API)

curl -s -X GET "$TT/exports/runs?limit=3" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "items": [
      {
        "id": "239a1898-fab5-486c-a2be-92d939be90cc",
        "mapping_id": null,
        "starts_on": "2026-11-30",
        "ends_on": "2026-12-06",
        "mode": "full",
        "row_count": 2,
        "content_hash": "1f92f2a1b4c52a8bcc43c40740e3f29b83e475de5ebacce7807c51808a04f6eb",
        "created_by": "00000000-0000-4000-8000-0000000000a1",
        "created_at": "2026-10-23T19:08:42.871612+00:00"
      }
    ],
    "next_cursor": null
  }
}

GET /exports/runs/{id}

One export run with pay lines and warnings.

  • operationId: getExportRun
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X GET "$TT/exports/runs/239a1898-fab5-486c-a2be-92d939be90cc" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "id": "239a1898-fab5-486c-a2be-92d939be90cc",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "mapping_id": null,
    "mapping_snapshot": {
      "id": "preset-generic",
      "name": "Generiek CSV (per dag)",
      "format": "csv",
      "tables": {},
      "columns": [
        {
          "header": "employee_number",
          "source": "employeeNumber"
        },
        {
          "header": "employee_name",
          "source": "employeeName"
        }
      ],
      "encoding": "utf-8-bom",
      "delimiter": ";",
      "lineEnding": "crlf",
      "validation": [
        {
          "column": "employee_number",
          "required": true
        }
      ],
      "includeHeader": true
    },
    "starts_on": "2026-11-30",
    "ends_on": "2026-12-06",
    "mode": "full",
    "row_count": 2,
    "content_hash": "1f92f2a1b4c52a8bcc43c40740e3f29b83e475de5ebacce7807c51808a04f6eb",
    "pay_lines": [
      {
        "date": "2026-12-01",
        "minutes": 480,
        "hourType": "normal",
        "clientName": "Bakkerij Ruijters",
        "employeeName": "Sanne Janssen",
        "placementRef": "PL-2026-104",
        "componentType": "hours",
        "employeeNumber": "1004"
      },
      {
        "date": "2026-12-02",
        "minutes": 450,
        "hourType": "normal",
        "clientName": "Bakkerij Ruijters",
        "employeeName": "Sanne Janssen",
        "placementRef": "PL-2026-104",
        "componentType": "hours",
        "employeeNumber": "1004"
      }
    ],
    "created_by": "00000000-0000-4000-8000-0000000000a1",
    "created_at": "2026-10-23T19:08:42.871612+00:00",
    "warnings": [
      {
        "key": "00000000-0000-4000-8000-0000000000e4",
        "code": "break_too_short",
        "date": "2026-12-02",
        "person": "Sanne Janssen",
        "message": "Break 0 min, at least 30 min required"
      }
    ]
  }
}

GET /exports/runs/{id}/file

The committed payroll file exactly as stored: CSV, delimited TXT, fixed-width TXT (text/plain) or XML (application/xml), per the mapping's format. ?as=json or ?as=xlsx converts the rows.

  • operationId: getExportFile
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X GET "$TT/exports/runs/239a1898-fab5-486c-a2be-92d939be90cc/file" \
  -H "Authorization: Bearer $KEY"

Response 200 (text/csv; charset=utf-8):

employee_number;employee_name;client;placement;date;hour_type;hours
1004;Sanne Janssen;Bakkerij Ruijters;PL-2026-104;2026-12-01;normal;8.00
1004;Sanne Janssen;Bakkerij Ruijters;PL-2026-104;2026-12-02;normal;7.50

GET /people/{id}/timesheet

PDF timesheet ("urenstaat") of one worker: every day with hours in the range, client, clock times, break, hours and status, plus a total and signature lines. Returns application/pdf. Clients get the same PDF per worker behind their approval link (/approve/{token}/timesheet/{personId}) once they approved all of that worker's days.

  • operationId: getTimesheetPdf
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
id path string (uuid) yes
from query string (date) yes From
to query string (date) yes To (max 62 days after from)

POST /periods/lock

Lock a period after export (admin).

  • operationId: lockPeriod
  • Who can call it: admin key or admin user (write keys get 403 Admin role required)
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
startsOn string yes
endsOn string yes
kind week | four_weeks | semi_month | month yes

Example (captured from the API)

curl -s -X POST "$TT/periods/lock" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"startsOn": "2026-11-30", "endsOn": "2026-12-06", "kind": "week"}'

Response 409:

{
  "error": {
    "code": "conflict",
    "message": "2 day(s) in this period are not exported yet"
  }
}

Master data

POST /import

Import master data CSV (people, clients, placements).

  • operationId: importMasterData
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
csv string yes
dryRun boolean yes

Example (captured from the API)

curl -s -X POST "$TT/import" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"csv": "personeelsnummer;voornaam;achternaam;email;telefoon;opdrachtgever;plaatsing;functie;startdatum;einddatum\r\n9901;Docs;Voorbeeld;docs@example.com;+31612345678;VDL Montage;PL-DOCS-1;Monteur;01-11-2026;\r\n;Zonder;Nummer;;;;;;;\r\n", "dryRun": true}'

Response 200:

{
  "data": {
    "dryRun": true,
    "rows": [
      {
        "row": 2,
        "ok": true,
        "person": "Docs Voorbeeld",
        "client": "VDL Montage"
      },
      {
        "row": 3,
        "ok": false,
        "message": "personeelsnummer ontbreekt"
      }
    ],
    "people": {
      "created": 1,
      "updated": 0
    },
    "clients": {
      "created": 0
    },
    "placements": {
      "created": 1,
      "updated": 0
    },
    "unmappedHeaders": []
  }
}

POST /import/time-entries

Import a time-entry CSV exported from Toggl Track, Clockify, TimeChimp or Harvest (see Importing from other time trackers). Entries become claims with source import, summed per placement per day. Dry run by default; re-importing the same file creates no duplicates.

  • operationId: importTimeEntries
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
csv string yes max 5 MB
provider string no toggl, clockify, timechimp or harvest; detected from the header row when omitted
dryRun boolean no default true
personMap object no source person key (from unknownPeople[].key) → person id

Response 200 (shortened):

{
  "data": {
    "dryRun": true,
    "provider": "toggl",
    "providerName": "Toggl Track",
    "totals": { "rows": 5, "entries": 4, "days": 2, "minutes": 960, "newDays": 2, "imported": 0, "duplicates": 0, "locked": 0, "unmatched": 1, "errors": 1 },
    "days": [{ "personName": "Jan de Vries", "clientName": "VDL Montage", "date": "2027-03-06", "minutes": 480, "startMin": 420, "endMin": 930, "breakMin": 30, "rows": [2, 3], "status": "new" }],
    "unmatched": [{ "row": 5, "person": "P. Nowak <piotr.prive@example.com>", "date": "2027-03-07", "minutes": 240, "reason": "Medewerker niet gevonden (e-mail of naam)" }],
    "errors": [{ "row": 6, "message": "Ongeldige datum 'geen-datum'" }],
    "unknownPeople": [{ "key": "piotr.prive@example.com", "label": "P. Nowak <piotr.prive@example.com>", "rows": 1 }]
  }
}

GET /people

List people (full list, or paginated with limit/cursor/updated_since → { items, next_cursor }).

  • operationId: listPersons
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
updated_since query string (date-time) no Only rows changed at or after this instant (incremental sync).
limit query integer no Page size (paginated lists). min 1; max 500; default 100
cursor query string no Opaque cursor from next_cursor of the previous page.

Example (captured from the API)

curl -s -X GET "$TT/people?limit=2" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "items": [
      {
        "id": "00000000-0000-4000-8000-0000000000d1",
        "employee_number": "1001",
        "first_name": "Jan",
        "last_name": "de Vries",
        "email": "jan@demo.timetracking.tech",
        "phone": null,
        "locale": "nl",
        "active": true,
        "user_id": "00000000-0000-4000-8000-0000000000b1",
        "external_source": null,
        "external_ref": null,
        "created_at": "2026-10-23T18:38:58.634749+00:00",
        "updated_at": "2026-10-23T19:09:09.0923+00:00"
      },
      {
        "id": "00000000-0000-4000-8000-0000000000d2",
        "employee_number": "1002",
        "first_name": "Fatima",
        "last_name": "El Amrani",
        "email": "fatima@demo.timetracking.tech",
        "phone": null,
        "locale": "nl",
        "active": true,
        "user_id": "00000000-0000-4000-8000-0000000000b2",
        "external_source": null,
        "external_ref": null,
        "created_at": "2026-10-23T18:38:58.634749+00:00",
        "updated_at": "2026-10-23T19:09:09.0923+00:00"
      }
    ],
    "next_cursor": "eyJ1IjoiMjAyNi0xMC0yM1QxOTowOTowOS4wOTIzKzAwOjAwIiwiaSI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBkMiJ9"
  }
}

POST /people

Create a person (or update when id is given).

  • operationId: upsertPerson
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
employeeNumber string no max length 40
firstName string yes min length 1; max length 80
lastName string yes min length 1; max length 80
email string no max length 200
phone string no max length 30
locale string no max length 10
active boolean no default true

Example (captured from the API)

curl -s -X POST "$TT/people" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"employeeNumber": "9920", "firstName": "Iris", "lastName": "Docs"}'

Response 200:

{
  "data": {
    "id": "3afdcfed-9246-4f87-a0c4-9f0bd55d0408",
    "employee_number": "9920",
    "first_name": "Iris",
    "last_name": "Docs",
    "email": null,
    "phone": null,
    "locale": null,
    "active": true,
    "user_id": null,
    "external_source": null,
    "external_ref": null,
    "created_at": "2026-10-23T19:40:03.082812+00:00",
    "updated_at": "2026-10-23T19:40:03.082812+00:00"
  }
}

GET /people/{id}

Get one person.

  • operationId: getPerson
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X GET "$TT/people/69a47995-bc6a-4bc9-9b9b-070935b974bb" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
    "employee_number": "9910",
    "first_name": "Eva",
    "last_name": "Docs-Jansen",
    "email": "eva.docs@example.com",
    "phone": null,
    "locale": "nl",
    "active": true,
    "user_id": null,
    "external_source": "bullhorn",
    "external_ref": "12345",
    "created_at": "2026-10-23T19:22:15.92661+00:00",
    "updated_at": "2026-10-23T19:22:15.991953+00:00"
  }
}

PATCH /people/{id}

Update a person (send the full object).

  • operationId: updatePerson
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
employeeNumber string no max length 40
firstName string yes min length 1; max length 80
lastName string yes min length 1; max length 80
email string no max length 200
phone string no max length 30
locale string no max length 10
active boolean no default true

Example (captured from the API)

curl -s -X PATCH "$TT/people/3afdcfed-9246-4f87-a0c4-9f0bd55d0408" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"employeeNumber": "9920", "firstName": "Iris", "lastName": "Docs-Bakker"}'

Response 200:

{
  "data": {
    "id": "3afdcfed-9246-4f87-a0c4-9f0bd55d0408",
    "employee_number": "9920",
    "first_name": "Iris",
    "last_name": "Docs-Bakker",
    "email": null,
    "phone": null,
    "locale": null,
    "active": true,
    "user_id": null,
    "external_source": null,
    "external_ref": null,
    "created_at": "2026-10-23T19:40:03.082812+00:00",
    "updated_at": "2026-10-23T19:40:03.194666+00:00"
  }
}

DELETE /people/{id}

Deactivate a person.

  • operationId: deactivatePerson
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X DELETE "$TT/people/69a47995-bc6a-4bc9-9b9b-070935b974bb" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
    "employee_number": "9910",
    "first_name": "Eva",
    "last_name": "Docs-Jansen",
    "email": "eva.docs@example.com",
    "phone": "+31612345678",
    "locale": "nl",
    "active": false,
    "user_id": null,
    "external_source": "bullhorn",
    "external_ref": "12345",
    "created_at": "2026-10-23T19:22:15.92661+00:00",
    "updated_at": "2026-10-23T19:22:16.289696+00:00"
  }
}

PUT /people/by-external/{source}/{ref}

Idempotent upsert of a person by your system's id.

  • operationId: upsertPersonByExternal
  • Who can call it: write key / office user
Parameter In Type Required Notes
source path string yes Your system's name. pattern ^[a-z0-9_-]+$
ref path string yes The record's id in your system (URL-encoded).
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
employeeNumber string no max length 40
firstName string yes min length 1; max length 80
lastName string yes min length 1; max length 80
email string no max length 200
phone string no max length 30
locale string no max length 10
active boolean no default true

Example (captured from the API)

curl -s -X PUT "$TT/people/by-external/bullhorn/12345" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"employeeNumber": "9910", "firstName": "Eva", "lastName": "Docs", "email": "eva.docs@example.com", "locale": "nl"}'

Response 200:

{
  "data": {
    "id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "user_id": null,
    "employee_number": "9910",
    "first_name": "Eva",
    "last_name": "Docs",
    "email": "eva.docs@example.com",
    "phone": null,
    "locale": "nl",
    "active": true,
    "created_at": "2026-10-23T19:22:15.92661+00:00",
    "invited_at": null,
    "updated_at": "2026-10-23T19:22:15.92661+00:00",
    "external_source": "bullhorn",
    "external_ref": "12345",
    "created": true
  }
}

Example — Same call again → update (created: false)

curl -s -X PUT "$TT/people/by-external/bullhorn/12345" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"employeeNumber": "9910", "firstName": "Eva", "lastName": "Docs-Jansen", "email": "eva.docs@example.com", "locale": "nl"}'

Response 200:

{
  "data": {
    "id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "user_id": null,
    "employee_number": "9910",
    "first_name": "Eva",
    "last_name": "Docs-Jansen",
    "email": "eva.docs@example.com",
    "phone": null,
    "locale": "nl",
    "active": true,
    "created_at": "2026-10-23T19:22:15.92661+00:00",
    "invited_at": null,
    "updated_at": "2026-10-23T19:22:15.991953+00:00",
    "external_source": "bullhorn",
    "external_ref": "12345",
    "created": false
  }
}

POST /people/batch

Upsert up to 500 people; rows with externalSource+externalRef upsert by external id.

  • operationId: batchPersons
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
items array of object yes

Example (captured from the API)

curl -s -X POST "$TT/people/batch" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"items": [{"externalSource": "bullhorn", "externalRef": "12346", "employeeNumber": "9911", "firstName": "Noah", "lastName": "Docs"}, {"externalSource": "bullhorn", "externalRef": "12347", "firstName": ""}]}'

Response 200:

{
  "data": {
    "ok": 1,
    "failed": 1,
    "results": [
      {
        "index": 0,
        "ok": true,
        "data": {
          "id": "02595da9-06ea-45d1-a5e8-927d854a2c46",
          "org_id": "00000000-0000-4000-8000-000000000001",
          "user_id": null,
          "employee_number": "9911",
          "first_name": "Noah",
          "last_name": "Docs",
          "email": null,
          "phone": null,
          "locale": null,
          "active": true,
          "created_at": "2026-10-23T19:22:17.348383+00:00",
          "invited_at": null,
          "updated_at": "2026-10-23T19:22:17.348383+00:00",
          "external_source": "bullhorn",
          "external_ref": "12346",
          "created": true
        }
      },
      {
        "index": 1,
        "ok": false,
        "error": {
          "code": "invalid",
          "message": "firstName: Too small: expected string to have >=1 characters; lastName: Invalid input: expected string, received undefined"
        }
      }
    ]
  }
}

GET /clients

List clients (full list, or paginated with limit/cursor/updated_since → { items, next_cursor }).

  • operationId: listClients
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
updated_since query string (date-time) no Only rows changed at or after this instant (incremental sync).
limit query integer no Page size (paginated lists). min 1; max 500; default 100
cursor query string no Opaque cursor from next_cursor of the previous page.

Example (captured from the API)

curl -s -X GET "$TT/clients?limit=2" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "items": [
      {
        "id": "00000000-0000-4000-8000-0000000000c1",
        "name": "VDL Montage",
        "external_ref": null,
        "external_source": null,
        "approval_mode": "both",
        "mail_domains": [
          "vdl-montage.example"
        ],
        "created_at": "2026-10-23T18:38:58.634749+00:00",
        "updated_at": "2026-10-23T19:09:09.0923+00:00"
      },
      {
        "id": "00000000-0000-4000-8000-0000000000c2",
        "name": "Logistiek Parkstad",
        "external_ref": null,
        "external_source": null,
        "approval_mode": "client",
        "mail_domains": [
          "logistiekparkstad.example"
        ],
        "created_at": "2026-10-23T18:38:58.634749+00:00",
        "updated_at": "2026-10-23T19:09:09.0923+00:00"
      }
    ],
    "next_cursor": "eyJ1IjoiMjAyNi0xMC0yM1QxOTowOTowOS4wOTIzKzAwOjAwIiwiaSI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMiJ9"
  }
}

POST /clients

Create a client (or update when id is given).

  • operationId: upsertClient
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
name string yes min length 1; max length 160
externalRef string no max length 80
approvalMode office | client | both no default "office"
mailDomains string no

No captured example for this endpoint; see the request schema above and the live spec.

GET /clients/{id}

Get one client.

  • operationId: getClient
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X GET "$TT/clients/a3254709-d152-49b4-ad5e-51d50147910a" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "id": "a3254709-d152-49b4-ad5e-51d50147910a",
    "name": "Docs Demo Metaal BV",
    "external_ref": "CC-77",
    "external_source": "bullhorn",
    "approval_mode": "office",
    "mail_domains": [],
    "created_at": "2026-10-23T19:22:15.841586+00:00",
    "updated_at": "2026-10-23T19:22:15.847919+00:00"
  }
}

PATCH /clients/{id}

Update a client (send the full object).

  • operationId: updateClient
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
name string yes min length 1; max length 160
externalRef string no max length 80
approvalMode office | client | both no default "office"
mailDomains string no

No captured example for this endpoint; see the request schema above and the live spec.

DELETE /clients/{id}

Delete a client without placements.

  • operationId: deactivateClient
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X DELETE "$TT/clients/a3254709-d152-49b4-ad5e-51d50147910a" \
  -H "Authorization: Bearer $KEY"

Response 409:

{
  "error": {
    "code": "conflict",
    "message": "Client has placements; deactivate those instead"
  }
}

Example — Client without placements

curl -s -X DELETE "$TT/clients/af8223bd-de80-4773-8aae-27394604b69a" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "deleted": true
  }
}

PUT /clients/by-external/{source}/{ref}

Idempotent upsert of a client by your system's id.

  • operationId: upsertClientByExternal
  • Who can call it: write key / office user
Parameter In Type Required Notes
source path string yes Your system's name. pattern ^[a-z0-9_-]+$
ref path string yes The record's id in your system (URL-encoded).
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
name string yes min length 1; max length 160
externalRef string no max length 80
approvalMode office | client | both no default "office"
mailDomains string no

Example (captured from the API)

curl -s -X PUT "$TT/clients/by-external/bullhorn/CC-77" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"name": "Docs Demo Metaal BV", "approvalMode": "office"}'

Response 200:

{
  "data": {
    "id": "a3254709-d152-49b4-ad5e-51d50147910a",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "name": "Docs Demo Metaal BV",
    "external_ref": "CC-77",
    "approval_mode": "office",
    "mail_domains": [],
    "period_kind": null,
    "created_at": "2026-10-23T19:22:15.841586+00:00",
    "updated_at": "2026-10-23T19:22:15.841586+00:00",
    "external_source": "bullhorn",
    "created": true
  }
}

POST /clients/batch

Upsert up to 500 clients; rows with externalSource+externalRef upsert by external id.

  • operationId: batchClients
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
items array of object yes

No captured example for this endpoint; see the request schema above and the live spec.

GET /placements

List placements (full list, or paginated with limit/cursor/updated_since → { items, next_cursor }).

  • operationId: listPlacements
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
updated_since query string (date-time) no Only rows changed at or after this instant (incremental sync).
limit query integer no Page size (paginated lists). min 1; max 500; default 100
cursor query string no Opaque cursor from next_cursor of the previous page.

Example (captured from the API)

curl -s -X GET "$TT/placements?limit=2" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "items": [
      {
        "id": "00000000-0000-4000-8000-0000000000e1",
        "person_id": "00000000-0000-4000-8000-0000000000d1",
        "client_id": "00000000-0000-4000-8000-0000000000c1",
        "site_id": null,
        "ref": "PL-2026-101",
        "external_source": null,
        "external_ref": null,
        "role_title": "Monteur",
        "starts_on": "2026-09-01",
        "ends_on": null,
        "active": true,
        "created_at": "2026-10-23T18:38:58.634749+00:00",
        "updated_at": "2026-10-23T19:09:09.0923+00:00"
      },
      {
        "id": "00000000-0000-4000-8000-0000000000e2",
        "person_id": "00000000-0000-4000-8000-0000000000d2",
        "client_id": "00000000-0000-4000-8000-0000000000c2",
        "site_id": null,
        "ref": "PL-2026-102",
        "external_source": null,
        "external_ref": null,
        "role_title": "Orderpicker",
        "starts_on": "2026-09-01",
        "ends_on": null,
        "active": true,
        "created_at": "2026-10-23T18:38:58.634749+00:00",
        "updated_at": "2026-10-23T19:09:09.0923+00:00"
      }
    ],
    "next_cursor": "eyJ1IjoiMjAyNi0xMC0yM1QxOTowOTowOS4wOTIzKzAwOjAwIiwiaSI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBlMiJ9"
  }
}

POST /placements

Create a placement (or update when id is given).

  • operationId: upsertPlacement
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
personId string (uuid) yes
clientId string (uuid) yes
ref string no max length 80
roleTitle string no max length 120
startsOn string yes pattern ^\d{4}-\d{2}-\d{2}$
endsOn string or string no
active boolean no default true

No captured example for this endpoint; see the request schema above and the live spec.

GET /placements/{id}

Get one placement.

  • operationId: getPlacement
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X GET "$TT/placements/96574dce-db96-4da1-82cf-de32021472d9" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "id": "96574dce-db96-4da1-82cf-de32021472d9",
    "person_id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
    "client_id": "a3254709-d152-49b4-ad5e-51d50147910a",
    "site_id": null,
    "ref": "PL-555",
    "external_source": "bullhorn",
    "external_ref": "PL-555",
    "role_title": "Lasser",
    "starts_on": "2026-12-01",
    "ends_on": null,
    "active": true,
    "created_at": "2026-10-23T19:22:16.09915+00:00",
    "updated_at": "2026-10-23T19:22:16.105978+00:00"
  }
}

PATCH /placements/{id}

Update a placement (send the full object).

  • operationId: updatePlacement
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
personId string (uuid) yes
clientId string (uuid) yes
ref string no max length 80
roleTitle string no max length 120
startsOn string yes pattern ^\d{4}-\d{2}-\d{2}$
endsOn string or string no
active boolean no default true

No captured example for this endpoint; see the request schema above and the live spec.

DELETE /placements/{id}

Deactivate a placement.

  • operationId: deactivatePlacement
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X DELETE "$TT/placements/96574dce-db96-4da1-82cf-de32021472d9" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "id": "96574dce-db96-4da1-82cf-de32021472d9",
    "person_id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
    "client_id": "a3254709-d152-49b4-ad5e-51d50147910a",
    "site_id": null,
    "ref": "PL-555",
    "external_source": "bullhorn",
    "external_ref": "PL-555",
    "role_title": "Lasser",
    "starts_on": "2026-12-01",
    "ends_on": null,
    "active": false,
    "created_at": "2026-10-23T19:22:16.09915+00:00",
    "updated_at": "2026-10-23T19:22:16.105978+00:00"
  }
}

PUT /placements/by-external/{source}/{ref}

Idempotent upsert of a placement by your system's id.

  • operationId: upsertPlacementByExternal
  • Who can call it: write key / office user
Parameter In Type Required Notes
source path string yes Your system's name. pattern ^[a-z0-9_-]+$
ref path string yes The record's id in your system (URL-encoded).
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
personId string (uuid) yes
clientId string (uuid) yes
ref string no max length 80
roleTitle string no max length 120
startsOn string yes pattern ^\d{4}-\d{2}-\d{2}$
endsOn string or string no
active boolean no default true

Example (captured from the API)

curl -s -X PUT "$TT/placements/by-external/bullhorn/PL-555" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"personExternalRef": "12345", "clientExternalRef": "CC-77", "ref": "PL-555", "roleTitle": "Lasser", "startsOn": "2026-12-01"}'

Response 200:

{
  "data": {
    "id": "96574dce-db96-4da1-82cf-de32021472d9",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "person_id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
    "client_id": "a3254709-d152-49b4-ad5e-51d50147910a",
    "site_id": null,
    "ref": "PL-555",
    "role_title": "Lasser",
    "starts_on": "2026-12-01",
    "ends_on": null,
    "active": true,
    "created_at": "2026-10-23T19:22:16.09915+00:00",
    "updated_at": "2026-10-23T19:22:16.09915+00:00",
    "external_source": "bullhorn",
    "external_ref": "PL-555",
    "created": true
  }
}

POST /placements/batch

Upsert up to 500 placements; rows with externalSource+externalRef upsert by external id.

  • operationId: batchPlacements
  • Who can call it: write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
items array of object yes

No captured example for this endpoint; see the request schema above and the live spec.

POST /people/{id}/access

Give app access (email-code login).

  • operationId: grantAppAccess
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

No captured example for this endpoint; see the request schema above and the live spec.

DELETE /people/{id}/access

Revoke app access.

  • operationId: revokeAppAccess
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

No captured example for this endpoint; see the request schema above and the live spec.

POST /people/{id}/anonymize

GDPR erasure of a person (admin; owner protected).

  • operationId: anonymizePerson
  • Who can call it: admin key or admin user
Parameter In Type Required Notes
id path string (uuid) yes

No captured example for this endpoint; see the request schema above and the live spec.

Kiosk

POST /people/{id}/pin

Set a person's kiosk PIN (6 digits).

  • operationId: setKioskPin
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

Request body (JSON)

Field Type Required Constraints
pin string yes

Example (captured from the API)

curl -s -X POST "$TT/people/<id>/pin" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"pin": "1234"}'

Response 422:

{
  "error": {
    "code": "invalid",
    "message": "PIN must be 6 digits"
  }
}

GET /kiosk-devices

List kiosk tablets.

  • operationId: listKioskDevices
  • Who can call it: write key, admin key, office user

Example (captured from the API)

curl -s -X GET "$TT/kiosk-devices" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": []
}

POST /kiosk-devices

Create a kiosk tablet; returns its token once.

  • operationId: createKioskDevice
  • Who can call it: admin key or admin user

Request body (JSON)

Field Type Required Constraints
name string yes
clientId string (uuid) or null no

No captured example for this endpoint; see the request schema above and the live spec.

Rosters

GET /shifts

Roster week with planning issues.

  • operationId: getRoster
  • Who can call it: read key (GET), write key / office user
Parameter In Type Required Notes
week query string (date) no Any date in the ISO week; defaults to the current week (org timezone).

Example (captured from the API)

curl -s -X GET "$TT/shifts?week=2026-12-07" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "weekStart": "2026-12-07",
    "weekEnd": "2026-12-13",
    "shifts": [
      {
        "id": "52457e4d-25a3-4ad7-8129-f778f817a96d",
        "org_id": "00000000-0000-4000-8000-000000000001",
        "client_id": "00000000-0000-4000-8000-0000000000c3",
        "placement_id": "00000000-0000-4000-8000-0000000000e4",
        "site_id": null,
        "work_date": "2026-12-08",
        "start_min": 420,
        "end_min": 930,
        "break_min": 30,
        "role_title": "Productiemedewerker",
        "note": null,
        "status": "published",
        "published_at": "2026-10-23T19:09:36.863+00:00",
        "created_by": "00000000-0000-4000-8000-0000000000a1",
        "created_at": "2026-10-23T19:09:36.792606+00:00",
        "updated_at": "2026-10-23T19:09:36.792+00:00",
        "client": {
          "name": "Bakkerij Ruijters"
        },
        "placement": {
          "person": {
            "last_name": "Janssen",
            "first_name": "Sanne"
          },
          "person_id": "00000000-0000-4000-8000-0000000000d4"
        }
      }
    ],
    "placements": [
      {
        "id": "00000000-0000-4000-8000-0000000000e1",
        "starts_on": "2026-09-01",
        "ends_on": null,
        "role_title": "Monteur",
        "person": {
          "id": "00000000-0000-4000-8000-0000000000d1",
          "last_name": "de Vries",
          "first_name": "Jan"
        },
        "client": {
          "id": "00000000-0000-4000-8000-0000000000c1",
          "name": "VDL Montage"
        }
      },
      {
        "id": "00000000-0000-4000-8000-0000000000e2",
        "starts_on": "2026-09-01",
        "ends_on": null,
        "role_title": "Orderpicker",
        "person": {
          "id": "00000000-0000-4000-8000-0000000000d2",
          "last_name": "El Amrani",
          "first_name": "Fatima"
        },
        "client": {
          "id": "00000000-0000-4000-8000-0000000000c2",
          "name": "Logistiek Parkstad"
        }
      }
    ],
    "issues": []
  }
}

POST /shifts

Create or update a shift (draft).

  • operationId: upsertShift
  • Who can call it: read key (GET), write key / office user
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
id string (uuid) no
clientId string (uuid) yes
placementId string (uuid) or null no
siteId string (uuid) or null no
workDate string yes pattern ^\d{4}-\d{2}-\d{2}$
startMin integer yes min 0; max 1439
endMin integer yes min 0; max 1439
breakMin integer no min 0; max 720; default 0
roleTitle string no max length 120
note string no max length 500
requiresApproval boolean no open shifts only: claims wait for the planner instead of "first claim wins"

Example (captured from the API)

curl -s -X POST "$TT/shifts" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"clientId": "00000000-0000-4000-8000-0000000000c3", "placementId": "00000000-0000-4000-8000-0000000000e4", "workDate": "2026-12-08", "startMin": 420, "endMin": 930, "breakMin": 30, "roleTitle": "Productiemedewerker"}'

Response 200:

{
  "data": {
    "id": "52457e4d-25a3-4ad7-8129-f778f817a96d",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "client_id": "00000000-0000-4000-8000-0000000000c3",
    "placement_id": "00000000-0000-4000-8000-0000000000e4",
    "site_id": null,
    "work_date": "2026-12-08",
    "start_min": 420,
    "end_min": 930,
    "break_min": 30,
    "role_title": "Productiemedewerker",
    "note": null,
    "status": "draft",
    "published_at": null,
    "created_by": "00000000-0000-4000-8000-0000000000a1",
    "created_at": "2026-10-23T19:09:36.792606+00:00",
    "updated_at": "2026-10-23T19:09:36.792+00:00"
  }
}

DELETE /shifts/{id}

Delete a draft or cancel a published shift.

  • operationId: deleteShift
  • Who can call it: read key (GET), write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

No captured example for this endpoint; see the request schema above and the live spec.

POST /shifts/publish

Publish all draft shifts of a week.

  • operationId: publishShifts
  • Who can call it: read key (GET), write key / office user

Request body (JSON)

Field Type Required Constraints
weekStart string yes

Example (captured from the API)

curl -s -X POST "$TT/shifts/publish" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"weekStart": "2026-12-07"}'

Response 200:

{
  "data": {
    "published": 1
  }
}

POST /shifts/copy-week

Copy previous week's shifts as drafts.

  • operationId: copyShiftWeek
  • Who can call it: read key (GET), write key / office user

Request body (JSON)

Field Type Required Constraints
weekStart string yes

No captured example for this endpoint; see the request schema above and the live spec.

POST /shifts/{id}/claim

Signed-in employee: claim an open shift (a published shift without a placement). Eligible: an active placement at the shift's client covering the date, with the same role when both name one. Without planner approval the first claim wins (one conditional update; a second claim gets 409). With requiresApproval the claim becomes a request for the planner. Refused with 409 when it conflicts with leave, another shift, or the 11-hour rest rule.

  • operationId: claimOpenShift
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
note string no max length 500

Response 200:

{ "data": { "status": "assigned", "shiftId": "…", "placementId": "…" } }

or, when the planner approves: { "data": { "status": "pending", "shiftId": "…", "requestId": "…" } }.

POST /shifts/{id}/swap

Signed-in employee: ask a colleague to take over one of your published, upcoming shifts, optionally in exchange for one of the colleague's shifts. The colleague must have a matching placement and no leave/overlap/rest conflict. Creates a request with status pending_colleague.

  • operationId: requestShiftSwap
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
colleaguePersonId string (uuid) yes
colleagueShiftId string (uuid) no the colleague's shift you take in exchange
note string no max length 500

GET /shifts/{id}/swap-candidates

Signed-in employee: colleagues who could take over this shift (personId, name as first name + initial) and their upcoming shifts you could take in exchange.

  • operationId: getSwapCandidates
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
id path string (uuid) yes

GET /shift-requests

Open-shift claims and swap requests that are still open (pending_colleague, pending_planner), oldest first. Requests waiting for the planner include issues (leave, overlap, rest, outside placement) for the person who would get the shift. ?status=all adds requests from the last 30 days.

  • operationId: listShiftRequests
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
status query string no open (default) or all

POST /shift-requests/{id}/respond

The colleague accepts (→ pending_planner) or declines (→ declined) a swap request addressed to them.

  • operationId: respondShiftSwap
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
accept boolean yes

POST /shift-requests/{id}/decide

Planner decision on a request with status pending_planner. Approving a claim assigns the open shift and rejects the other open claims on it; approving a swap moves the shift(s) in one transaction. Conflicts are checked again; 409 when the roster changed or a rule is broken. Emits shift.request_decided.

  • operationId: decideShiftRequest
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
decision string yes approved or rejected
note string no max length 500

POST /shift-requests/{id}/cancel

The requester (or the office) withdraws an open request.

  • operationId: cancelShiftRequest
  • Who can call it: the requesting employee, or write key / office user
Parameter In Type Required Notes
id path string (uuid) yes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

GET /availability

Availability of the signed-in person (or personId for office).

  • operationId: getAvailability
  • Who can call it: employee token (own) or write key / office user with personId
Parameter In Type Required Notes
personId query string (uuid) no Person
from query string (date) no From
to query string (date) no To

No captured example for this endpoint; see the request schema above and the live spec.

POST /availability

Set availability for a day.

  • operationId: setAvailability
  • Who can call it: employee token (own) or write key / office user with personId

Request body (JSON)

Field Type Required Constraints
personId string (uuid) no
workDate string yes pattern ^\d{4}-\d{2}-\d{2}$
available boolean yes
note string no max length 300

No captured example for this endpoint; see the request schema above and the live spec.

Leave

GET /leave

List leave requests.

  • operationId: listLeave
  • Who can call it: read key (list), write key / office user (all people); employee token (own)
Parameter In Type Required Notes
personId query string (uuid) no Filter
from query string (date) no From
to query string (date) no To
status query string no requested

Example (captured from the API)

curl -s -X GET "$TT/leave?from=2026-12-01&to=2026-12-31" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": [
    {
      "id": "52666acf-c11c-47bc-a1e8-326776da1f2a",
      "org_id": "00000000-0000-4000-8000-000000000001",
      "person_id": "00000000-0000-4000-8000-0000000000d4",
      "leave_type_id": "ef33b5fc-c116-434c-a70c-4c4a1ee1bd1a",
      "starts_on": "2026-12-14",
      "ends_on": "2026-12-15",
      "minutes_per_day": 480,
      "status": "approved",
      "note": "Docs example",
      "decided_by": "00000000-0000-4000-8000-0000000000a1",
      "decided_at": "2026-10-23T19:09:10.242+00:00",
      "decision_note": null,
      "created_by": "00000000-0000-4000-8000-0000000000a1",
      "created_at": "2026-10-23T19:09:10.24124+00:00",
      "updated_at": "2026-10-23T19:09:10.24124+00:00",
      "external_source": null,
      "external_ref": null,
      "type": {
        "code": "vacation",
        "name": "Vakantie",
        "paid": true
      },
      "person": {
        "last_name": "Janssen",
        "first_name": "Sanne"
      }
    }
  ]
}

POST /leave

Request or register leave.

  • operationId: requestLeave
  • Who can call it: read key (list), write key / office user (all people); employee token (own)
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
personId string (uuid) no
leaveTypeId string (uuid) yes
startsOn string yes pattern ^\d{4}-\d{2}-\d{2}$
endsOn string yes pattern ^\d{4}-\d{2}-\d{2}$
minutesPerDay integer no min 0; max 1440; default 480
note string no max length 500

Example (captured from the API)

curl -s -X POST "$TT/leave" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"leaveTypeId": "ef33b5fc-c116-434c-a70c-4c4a1ee1bd1a", "startsOn": "2026-12-21", "endsOn": "2026-12-23", "minutesPerDay": 480, "note": "Christmas"}'

Response 200:

{
  "data": {
    "id": "c7aecb37-052b-48ef-a0df-7d8fbf52d63b",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "person_id": "00000000-0000-4000-8000-0000000000d1",
    "leave_type_id": "ef33b5fc-c116-434c-a70c-4c4a1ee1bd1a",
    "starts_on": "2026-12-21",
    "ends_on": "2026-12-23",
    "minutes_per_day": 480,
    "status": "requested",
    "note": "Christmas",
    "decided_by": null,
    "decided_at": null,
    "decision_note": null,
    "created_by": "00000000-0000-4000-8000-0000000000b1",
    "created_at": "2026-10-23T19:09:36.457189+00:00",
    "updated_at": "2026-10-23T19:09:36.457189+00:00",
    "external_source": null,
    "external_ref": null
  }
}

GET /leave/types

Leave types.

  • operationId: listLeaveTypes
  • Who can call it: read key (list), write key / office user (all people); employee token (own)

Example (captured from the API)

curl -s -X GET "$TT/leave/types" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": [
    {
      "id": "0666d17e-f288-4a92-bdb5-de4dc1ec4c83",
      "org_id": "00000000-0000-4000-8000-000000000001",
      "code": "special",
      "name": "Bijzonder verlof",
      "paid": true,
      "needs_approval": true,
      "export_code": "[CODE_BIJZONDER]"
    },
    {
      "id": "6ad1e945-9392-4c4c-a069-85a8d6af6205",
      "org_id": "00000000-0000-4000-8000-000000000001",
      "code": "unpaid",
      "name": "Onbetaald verlof",
      "paid": false,
      "needs_approval": true,
      "export_code": null
    }
  ]
}

GET /leave/balance

Leave balance per type (minutes).

  • operationId: getLeaveBalance
  • Who can call it: read key (list), write key / office user (all people); employee token (own)
Parameter In Type Required Notes
personId query string (uuid) no Person
year query integer no Year

Example (captured from the API)

curl -s -X GET "$TT/leave/balance?personId=00000000-0000-4000-8000-0000000000d4&year=2026" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": [
    {
      "leaveTypeId": "0666d17e-f288-4a92-bdb5-de4dc1ec4c83",
      "code": "special",
      "name": "Bijzonder verlof",
      "allowance": null,
      "taken": 0,
      "remaining": null
    },
    {
      "leaveTypeId": "6ad1e945-9392-4c4c-a069-85a8d6af6205",
      "code": "unpaid",
      "name": "Onbetaald verlof",
      "allowance": null,
      "taken": 0,
      "remaining": null
    }
  ]
}

POST /leave/{id}/decide

Approve or reject leave.

  • operationId: decideLeave
  • Who can call it: write key / office user (employees may cancel their own leave)
Parameter In Type Required Notes
id path string (uuid) yes

Request body (JSON)

Field Type Required Constraints
decision approved | rejected yes
note string no

Example (captured from the API)

curl -s -X POST "$TT/leave/c7aecb37-052b-48ef-a0df-7d8fbf52d63b/decide" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"decision": "approved", "note": "Enjoy"}'

Response 200:

{
  "data": {
    "decided": "approved"
  }
}

POST /leave/{id}/cancel

Cancel leave.

  • operationId: cancelLeave
  • Who can call it: write key / office user (employees may cancel their own leave)
Parameter In Type Required Notes
id path string (uuid) yes

No captured example for this endpoint; see the request schema above and the live spec.

Expenses

GET /expenses

List expenses.

  • operationId: listExpenses
  • Who can call it: read key (list), write key / office user (all people); employee token (own)
Parameter In Type Required Notes
from query string (date) no From
to query string (date) no To
personId query string (uuid) no Person
status query string no Status

Example (captured from the API)

curl -s -X GET "$TT/expenses?from=2026-12-01&to=2026-12-31" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": [
    {
      "id": "8917409f-9a1e-4a54-8356-073e6a93153d",
      "org_id": "00000000-0000-4000-8000-000000000001",
      "person_id": "00000000-0000-4000-8000-0000000000d4",
      "placement_id": null,
      "work_date": "2026-12-08",
      "kind": "parking",
      "km": null,
      "minutes": null,
      "amount_cents": 750,
      "description": null,
      "receipt_path": null,
      "status": "approved",
      "decided_by": null,
      "decision_note": null,
      "export_run_id": null,
      "created_by": "00000000-0000-4000-8000-0000000000a1",
      "created_at": "2026-10-23T19:09:10.391294+00:00",
      "updated_at": "2026-10-23T19:09:10.391294+00:00",
      "external_source": null,
      "external_ref": null,
      "person": {
        "last_name": "Janssen",
        "first_name": "Sanne"
      },
      "placement": null
    },
    {
      "id": "a925ddbe-8614-4e13-8df6-ca3219ed285d",
      "org_id": "00000000-0000-4000-8000-000000000001",
      "person_id": "00000000-0000-4000-8000-0000000000d4",
      "placement_id": "00000000-0000-4000-8000-0000000000e4",
      "work_date": "2026-12-08",
      "kind": "km",
      "km": 42,
      "minutes": null,
      "amount_cents": 0,
      "description": "Client visit",
      "receipt_path": null,
      "status": "approved",
      "decided_by": null,
      "decision_note": null,
      "export_run_id": null,
      "created_by": "00000000-0000-4000-8000-0000000000a1",
      "created_at": "2026-10-23T19:09:10.358841+00:00",
      "updated_at": "2026-10-23T19:09:10.358841+00:00",
      "external_source": null,
      "external_ref": null,
      "person": {
        "last_name": "Janssen",
        "first_name": "Sanne"
      },
      "placement": {
        "client": {
          "name": "Bakkerij Ruijters"
        }
      }
    }
  ]
}

POST /expenses

Record mileage, travel time or an expense.

  • operationId: createExpense
  • Who can call it: read key (list), write key / office user (all people); employee token (own)
Parameter In Type Required Notes
Idempotency-Key header string no Optional on writes: the first successful response is replayed for 24h for the same key. min length 8; max length 200

Request body (JSON)

Field Type Required Constraints
personId string (uuid) no
placementId string (uuid) no
workDate string yes pattern ^\d{4}-\d{2}-\d{2}$
kind km | travel_time | public_transport | parking | meal | other yes
km number no min 0; max 5000
minutes integer no min 0; max 1440
amountCents integer no min 0; max 10000000
description string no max length 500
receiptPath string no max length 300

Example (captured from the API)

curl -s -X POST "$TT/expenses" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"personId": "00000000-0000-4000-8000-0000000000d4", "workDate": "2026-12-08", "kind": "parking", "amountCents": 750}'

Response 200:

{
  "data": {
    "id": "8917409f-9a1e-4a54-8356-073e6a93153d",
    "org_id": "00000000-0000-4000-8000-000000000001",
    "person_id": "00000000-0000-4000-8000-0000000000d4",
    "placement_id": null,
    "work_date": "2026-12-08",
    "kind": "parking",
    "km": null,
    "minutes": null,
    "amount_cents": 750,
    "description": null,
    "receipt_path": null,
    "status": "approved",
    "decided_by": null,
    "decision_note": null,
    "export_run_id": null,
    "created_by": "00000000-0000-4000-8000-0000000000a1",
    "created_at": "2026-10-23T19:09:10.391294+00:00",
    "updated_at": "2026-10-23T19:09:10.391294+00:00",
    "external_source": null,
    "external_ref": null
  }
}

DELETE /expenses/{id}

Delete or withdraw an expense.

  • operationId: deleteExpense
  • Who can call it: read key (list), write key / office user (all people); employee token (own)
Parameter In Type Required Notes
id path string (uuid) yes

No captured example for this endpoint; see the request schema above and the live spec.

POST /expenses/decide

Approve or reject expenses.

  • operationId: decideExpenses
  • Who can call it: read key (list), write key / office user (all people); employee token (own)

Request body (JSON)

Field Type Required Constraints
ids array of string (uuid) yes
decision approved | rejected yes

Example (captured from the API)

curl -s -X POST "$TT/expenses/decide" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"ids": ["f63b3afc-61e0-4eaf-acfc-0f4bfc70db3f"], "decision": "approved"}'

Response 200:

{
  "data": {
    "updated": 1
  }
}

POST /expenses/receipt-upload

Signed URL to upload a receipt (employees).

  • operationId: createReceiptUpload
  • Who can call it: read key (list), write key / office user (all people); employee token (own)

Request body (JSON)

Field Type Required Constraints
fileName string yes

No captured example for this endpoint; see the request schema above and the live spec.

Inbox

GET /inbox

Inbound client timesheets: documents and rows to review.

  • operationId: getInbox
  • Who can call it: read key, write key, office user

Example (captured from the API)

curl -s -X GET "$TT/inbox" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "documents": [
      {
        "id": "17f5debc-557c-43f8-a53a-64751982df26",
        "sender": "planning@vdl-montage.example",
        "subject": "Urenstaat week 43",
        "status": "done",
        "summary": "2 dag(en) automatisch verwerkt",
        "received_at": "2026-10-26T08:15:00+00:00",
        "attachments": [
          {
            "name": "week43.csv",
            "path": "00000000-0000-4000-8000-000000000001/6ffe82f9-aac2-4e51-8fed-7e90d49aac98-week43.csv",
            "size": 135,
            "type": "text/csv"
          }
        ],
        "client": {
          "name": "VDL Montage"
        }
      },
      {
        "id": "aebb71dc-a16e-4167-8259-48278f5baedd",
        "sender": "ruud@logistiekparkstad.example",
        "subject": "correctie",
        "status": "needs_review",
        "summary": "0 dag(en) automatisch verwerkt · 1 ter controle",
        "received_at": "2026-10-23T18:39:17.165+00:00",
        "attachments": [],
        "client": {
          "name": "Logistiek Parkstad"
        }
      }
    ],
    "items": []
  }
}

POST /inbox/items/{id}/apply

Apply a reviewed row as client hours.

  • operationId: applyInboxItem
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

Request body (JSON)

Field Type Required Constraints
placementId string (uuid) yes
workDate string yes
minutes integer yes min -9007199254740991; max 9007199254740991

No captured example for this endpoint; see the request schema above and the live spec.

POST /inbox/items/{id}/ignore

Dismiss a row.

  • operationId: ignoreInboxItem
  • Who can call it: write key / office user
Parameter In Type Required Notes
id path string (uuid) yes

Request body (JSON)

Field Type Required Constraints
note string no

No captured example for this endpoint; see the request schema above and the live spec.

Employee

GET /me/week

Signed-in employee: placements and days of a week.

  • operationId: getMyWeek
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
week query string (date) no Any date in the ISO week; defaults to the current week (org timezone).

Example (captured from the API)

curl -s -X GET "$TT/me/week?week=2026-12-07" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "person": {
      "id": "00000000-0000-4000-8000-0000000000d1",
      "firstName": "Jan",
      "lastName": "de Vries",
      "locale": "nl"
    },
    "isOffice": false,
    "weekStart": "2026-12-07",
    "weekEnd": "2026-12-13",
    "placements": [
      {
        "id": "00000000-0000-4000-8000-0000000000e1",
        "ref": "PL-2026-101",
        "roleTitle": "Monteur",
        "clientName": "VDL Montage",
        "startsOn": "2026-09-01",
        "endsOn": null,
        "site": null
      }
    ],
    "days": []
  }
}

GET /me/shifts

Signed-in employee: published shifts.

  • operationId: getMyShifts
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
from query string (date) no From
to query string (date) no To

No captured example for this endpoint; see the request schema above and the live spec.

GET /me/open-shifts

Signed-in employee: open shifts you may claim, today up to 60 days ahead. Each item has placementId (the placement the claim would use), requiresApproval and requested (you already asked).

  • operationId: getMyOpenShifts
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
from query string (date) no From (not before today)
to query string (date) no To

GET /me/shift-requests

Signed-in employee: your claims and swap requests, and swap requests addressed to you (incoming: true), from the last 30 days.

  • operationId: getMyShiftRequests
  • Who can call it: employee user token only (an API key has no employee profile → 403)

GET /me/predictions

Signed-in employee: suggested hours for days without hours.

  • operationId: getMyPredictions
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
today query string (date) no Reference date

No captured example for this endpoint; see the request schema above and the live spec.

GET /me/rewards

Signed-in employee: points and on-time streak.

  • operationId: getMyRewards
  • Who can call it: employee user token only (an API key has no employee profile → 403)

No captured example for this endpoint; see the request schema above and the live spec.

GET /me/notifications

Signed-in employee: in-app notifications, newest first, and the unread count.

  • operationId: listMyNotifications
  • Who can call it: employee user token only (an API key has no employee profile → 403)
Parameter In Type Required Notes
limit query integer no 1–100, default 30.

Response 200:

{
  "data": {
    "items": [
      {
        "id": "6f0c…",
        "kind": "hours.late",
        "title": "Je uren van 26 okt – 1 nov zijn te laat",
        "body": "Hoi Jan, je hebt je uren van 26 okt – 1 nov nog niet ingediend. Dien ze nu in.",
        "data": { "periodStart": "2026-10-26", "periodEnd": "2026-11-01", "url": "/me?week=2026-10-26" },
        "readAt": null,
        "createdAt": "2026-11-02T08:00:12Z"
      }
    ],
    "unread": 1
  }
}

Title and body are written in the employee's language when the notification is created. Kinds: hours.late (hours past the deadline, sent automatically at the deadline).

POST /me/notifications/read

Signed-in employee: mark notifications as read. Without ids, every unread notification is marked.

  • operationId: markMyNotificationsRead
  • Who can call it: employee user token only
Field Type Required Constraints
ids array of uuid no max 100

Response 200: { "data": { "updated": 1 } }

POST /me/device-token

Register a push token.

  • operationId: registerDeviceToken
  • Who can call it: employee user token only (an API key has no employee profile → 403)

Request body (JSON)

Field Type Required Constraints
token string yes
platform ios | android | web yes

No captured example for this endpoint; see the request schema above and the live spec.

Settings

GET /org/reminders

Hours deadline and late-reminder policy, and whether the WhatsApp provider is configured.

  • operationId: getReminderSettings
  • Who can call it: write key, office user

Response 200:

{
  "data": {
    "timezone": "Europe/Amsterdam",
    "deadline": { "weekday": 1, "minute": 540 },
    "policy": { "enabled": true, "whatsappEnabled": true, "whatsappDelayMinutes": 90 },
    "whatsappConfigured": false
  }
}

deadline.weekday is the ISO weekday (1 = Monday) after the payroll period ends; minute is the local minute of day. Reminders: an in-app notification at the deadline, then a WhatsApp message whatsappDelayMinutes later if the hours are still missing and the person has a phone number. Never by email. No reminders between 21:00 and 07:00 local, at most 3 per person per day, one per channel per period, none for deadlines older than 7 days. Without a configured provider WhatsApp reminders are recorded as skipped (whatsappConfigured: false).

PUT /org/reminders

Set the hours deadline and reminder policy. Returns the new settings.

  • operationId: updateReminderSettings
  • Who can call it: admin user or admin key
Field Type Required Constraints
deadlineWeekday integer yes 1–7 (1 = Monday)
deadlineTime string yes HH:MM, local time
enabled boolean yes in-app reminders (master switch)
whatsappEnabled boolean yes WhatsApp follow-up
whatsappDelayMinutes integer yes 0–1440

Integrations

GET /api-keys

List API keys (admin users).

  • operationId: listApiKeys
  • Who can call it: admin user or admin key

No captured example for this endpoint; see the request schema above and the live spec.

POST /api-keys

Create an API key (admin users; keys cannot create keys).

  • operationId: createApiKey
  • Who can call it: admin user token only — no API key can create keys (403 Not available with an API key)

Request body (JSON)

Field Type Required Constraints
name string yes
scopes array of read | write yes

Example (captured from the API)

curl -s -X POST "$TT/api-keys" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"name": "docs-v12 admin", "scopes": ["read", "admin"]}'

Response 200:

{
  "data": {
    "key": "tt_live_…"
  }
}

DELETE /api-keys/{id}

Revoke an API key.

  • operationId: revokeApiKey
  • Who can call it: admin user or admin key
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X DELETE "$TT/api-keys/<id>" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "revoked": true
  }
}

GET /webhooks

List webhooks.

  • operationId: listWebhooks
  • Who can call it: admin key or admin user (write keys get 403)

Example (captured from the API)

curl -s -X GET "$TT/webhooks" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": [
    {
      "id": "2c799e37-907b-4d22-a297-9f4a31e7c0f4",
      "url": "https://example.com/tt-docs-capture",
      "events": [
        "timesheet.submitted",
        "timesheet.approved"
      ],
      "active": true,
      "created_at": "2026-10-23T19:08:40.943918+00:00"
    }
  ]
}

POST /webhooks

Create a webhook; returns the signing secret once.

  • operationId: createWebhook
  • Who can call it: admin key or admin user (write keys get 403)

Request body (JSON)

Field Type Required Constraints
url string (uri) yes
events array of timesheet.submitted | timesheet.approved | timesheet.client_approved | conflict.created | export.completed | period.locked | leave.requested | leave.decided | expense.submitted | shift.published | person.created | person.updated | placement.created | placement.updated | claim.created | expense.decided | leave.cancelled | webhook.test | client.created | client.updated | conflict.resolved yes min items 1

Example (captured from the API)

curl -s -X POST "$TT/webhooks" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"url": "https://example.com/tt-docs-capture", "events": ["timesheet.submitted", "timesheet.approved", "timesheet.client_approved", "conflict.created", "export.completed", "period.locked", "leave.requested", "leave.decided", "expense.submitted", "shift.published"]}'

Response 200:

{
  "data": {
    "id": "2c799e37-907b-4d22-a297-9f4a31e7c0f4",
    "secret": "whsec_…"
  }
}

DELETE /webhooks/{id}

Delete a webhook.

  • operationId: deleteWebhook
  • Who can call it: admin key or admin user (write keys get 403)
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X DELETE "$TT/webhooks/<id>" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "deleted": true
  }
}

GET /webhooks/{id}/deliveries

Recent deliveries with status codes.

  • operationId: listWebhookDeliveries
  • Who can call it: admin key or admin user (write keys get 403)
Parameter In Type Required Notes
id path string (uuid) yes
limit query integer no Max 200

Example (captured from the API)

curl -s -X GET "$TT/webhooks/682094f0-0ab9-4667-980d-808bb575fe5d/deliveries?limit=50" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": [
    {
      "id": 21,
      "event": "webhook.test",
      "status": "pending",
      "attempts": 0,
      "last_status_code": null,
      "last_error": null,
      "next_attempt_at": "2026-10-23T19:22:20.469944+00:00",
      "created_at": "2026-10-23T19:22:20.469944+00:00",
      "payload": {
        "id": "evt_78eae536b3c2ecb793b5ca22",
        "data": {
          "message": "Test event from TimeTracking"
        },
        "type": "webhook.test",
        "created": "2026-10-23T19:22:20.468Z"
      }
    },
    {
      "id": 20,
      "event": "person.updated",
      "status": "pending",
      "attempts": 0,
      "last_status_code": null,
      "last_error": null,
      "next_attempt_at": "2026-10-23T19:22:20.127386+00:00",
      "created_at": "2026-10-23T19:22:20.127386+00:00",
      "payload": {
        "id": "evt_be726cdb624908aa0d34a2ab",
        "data": {
          "active": false,
          "personId": "69a47995-bc6a-4bc9-9b9b-070935b974bb"
        },
        "type": "person.updated",
        "created": "2026-10-23T19:22:20.124Z"
      }
    }
  ]
}

POST /webhooks/{id}/test

Queue a webhook.test event.

  • operationId: testWebhook
  • Who can call it: admin key or admin user (write keys get 403)
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X POST "$TT/webhooks/682094f0-0ab9-4667-980d-808bb575fe5d/test" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "queued": true
  }
}

POST /webhooks/{id}/rotate-secret

Issue a new signing secret (returned once).

  • operationId: rotateWebhookSecret
  • Who can call it: admin key or admin user (write keys get 403)
Parameter In Type Required Notes
id path string (uuid) yes

Example (captured from the API)

curl -s -X POST "$TT/webhooks/682094f0-0ab9-4667-980d-808bb575fe5d/rotate-secret" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "secret": "whsec_…"
  }
}

POST /webhook-deliveries/{id}/replay

Re-queue a delivery.

  • operationId: replayWebhookDelivery
  • Who can call it: admin key or admin user (write keys get 403)
Parameter In Type Required Notes
id path integer yes

Example (captured from the API)

curl -s -X POST "$TT/webhook-deliveries/21/replay" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "requeued": true
  }
}

Reports

GET /reports/hours

Hours per person, client or placement over a date range (max 366 days). Counts resolved days with minutes > 0; minutesByStatus splits the total over the statuses, conflictDays counts days still in conflict. The same numbers are on the office page Rapportages (/reports), which also shows roster vs worked, open/late submissions, expenses and anomalies, and downloads them as CSV or XLSX.

  • operationId: getHoursReport
  • Who can call it: read key, write key, office user
Parameter In Type Required Notes
group_by query string (person | client | placement) no Default person. Anything else → 422.
from query string (date) yes First date (inclusive).
to query string (date) yes Last date (inclusive); at most 366 days after from.
client_id query string (uuid) no Filter.
person_id query string (uuid) no Filter.
placement_id query string (uuid) no Filter.
status query string no draft, submitted, approved, exported, locked or conflict.

Example

curl -s "$TT/reports/hours?group_by=placement&from=2026-09-14&to=2026-09-18" \
  -H "Authorization: Bearer $KEY"

Response 200:

{
  "data": {
    "from": "2026-09-14",
    "to": "2026-09-18",
    "group_by": "placement",
    "total_minutes": 2940,
    "rows": [
      {
        "id": "5f0c…",
        "name": "Rapport Tester",
        "sub": "Rapport A",
        "personId": "1b7e…",
        "clientId": "c2d4…",
        "placementId": "5f0c…",
        "days": 4,
        "minutes": 1740,
        "minutesByStatus": { "draft": 1740, "submitted": 0, "approved": 0, "exported": 0, "locked": 0 },
        "conflictDays": 0
      }
    ]
  }
}

Account

GET /export/account

Full account export (open data): a ZIP with every table of your organisation as data/<table>.csv and data/<table>.json (people, clients, placements, time_claims, timesheet_days, approvals, leave, expenses, shifts, export_runs, audit_log, …), data/pay_lines.* (pay lines flattened from the export runs), schema.json (tables, columns, keys) and README.md explaining the files and conventions. The response is streamed (Content-Type: application/zip, not the JSON envelope; errors still use the JSON envelope).

Secrets never leave: columns and JSON keys kiosk_pin_hash, key_hash, token_hash, email_code_hash, secret, token, password, body_text, body_html are removed everywhere, including inside audit_log.before/after; caches, rate counters and device push tokens are not exported. Each download is written to the audit log (account.export). Owners and admins can download the same ZIP in the web app under Instellingen → Volledige account-export.

  • operationId: exportAccount
  • Who can call it: admin key or owner/admin user (read and write keys get 403)

Example

curl -s -o account.zip "$TT/export/account" \
  -H "Authorization: Bearer $ADMIN_KEY"
unzip -l account.zip | head