Data model
The TimeTracking entities (organisation, person, client, placement, claim, day, conflict, leave, expense, shift, export run, period) and how they relate.
Last updated: · Markdown version
Everything belongs to one organisation (org). An API key, webhook or user session never sees another org's data.
Organisation
├── Person ───────────┐ (employee / flex worker; employee_number, email, active)
├── Client ──┐ │ (approval_mode office|client|both, mail_domains, contacts)
│ └── Placement ◄───────┘ person × client, starts_on … ends_on, ref
│ ├── Claim* raw hours from one source (append-only)
│ ├── Day one resolved value per placement per date (timesheet_day)
│ │ └── Conflict when claims disagree (candidates → office picks one)
│ ├── Shift planned work (roster), draft → published
│ └── Expense km, travel_time, public_transport, parking, meal, other
├── Leave request person, leave type, starts_on … ends_on, minutes_per_day
├── Export mapping JSON recipe for a payroll file (presets + org-specific)
├── Export run an immutable, committed payroll file + pay lines
└── Period a locked date range (week, four_weeks, semi_month, month)
Entities
| Entity | API resource | Key fields (responses are snake_case) | Notes |
|---|---|---|---|
| Person | /people |
id, employee_number, first_name, last_name, email, phone, locale, active, user_id, external_source, external_ref |
employee_number is the payroll key and must be digits for the SD Worx preset. user_id is set once the person has app access. |
| Client | /clients |
id, name, external_ref, external_source, approval_mode, mail_domains, client_contacts[] |
approval_mode decides who must approve hours (see status lifecycle). mail_domains links inbound email to the client. |
| Placement | /placements |
id, person_id, client_id, ref, role_title, starts_on, ends_on, active, external_source, external_ref |
Hours are always recorded on a placement and a date. ref is your placement number and appears in exports as placementRef. |
| Claim | /claims |
id, placement_id, work_date, source, minutes, start_min, end_min, break_min, note, confidence, source_ref, created_at |
One raw input. Never edited; a newer claim from the same source replaces the older one. See sources and conflicts. |
| Day | /days, /org/week |
id, placement_id, person_id, work_date, minutes, start_min, end_min, break_min, status, resolution, has_conflict, claim_ids, approved_by_office_at, approved_by_client, approved_by_client_at, export_run_id, version, updated_at |
Derived from claims. minutes is null while the day is in conflict. version increases with every change. |
| Conflict | /conflicts |
id, dayId, workDate, personName, clientName, candidates[{claimId, source, minutes}] |
Resolved with POST /conflicts/{id}/resolve {claimId}. |
| Leave request | /leave |
id, person_id, leave_type_id, starts_on, ends_on, minutes_per_day, status (requested, approved, rejected, cancelled) |
Leave types per org: vacation, sick, special, unpaid by default (/leave/types). |
| Expense | /expenses |
id, person_id, placement_id, work_date, kind, km, minutes, amount_cents, status (submitted, approved, rejected, exported) |
km is priced with the placement's per_km component. Without one, amount_cents is 0. |
| Shift | /shifts |
id, client_id, placement_id (null = open shift), work_date, start_min, end_min, break_min, status (draft, published), requires_approval |
A plan only. Shifts never become hours by themselves. |
| Shift request | /shift-requests |
id, kind (claim, swap), shift_id, requester, target (swap), target_shift_id (exchange), status (pending_colleague, pending_planner, approved, rejected, declined, cancelled) |
Claims of open shifts and swaps between colleagues. Only an approved request changes the roster. |
| Export run | /exports/runs |
id, mapping_id, mapping_snapshot, starts_on, ends_on, row_count, content_hash, pay_lines, warnings |
Immutable and reproducible. The file is at /exports/runs/{id}/file. |
| Period | (POST /periods/lock) |
kind, starts_on, ends_on, locked_at |
See periods and locking. |
Identifiers
- TimeTracking IDs are UUIDs. Store them in your system.
- External IDs (v1.1) give every person, client and placement an optional
(external_source, external_ref)pair. Examples:("bullhorn", "12345"),("carerix", "PL-555"). Upsert by that pair withPUT /people/by-external/{source}/{ref}(also/clients/...and/placements/...). A placement can point at its person and client by their external refs (personExternalRef,clientExternalRef) from the same source. See the API reference. - Older natural keys that still work:
employee_number(people; the CSV import matches on it), placementref(the CSV import matches on it), clientname(the CSV import matches case-insensitively).
Units and time
- Durations are integer minutes. Money is integer cents. Dates are
YYYY-MM-DDin the org's timezone (defaultEurope/Amsterdam). start_minandend_minare minutes after midnight, from 0 to 1439. A shift that ends at or before its start crosses midnight. It belongs to the date it started on.- Weeks are ISO weeks (Monday–Sunday). Endpoints that take
weekaccept any date inside the week.
Request vs response naming
Request bodies use camelCase (placementId, workDate, startMin). Most responses return database rows in snake_case (placement_id, work_date, start_min). Aggregated views (/org/week, /conflicts, /client/approve/{token}, export preview) use camelCase. The API reference shows the real shape of every response.
Roles
| Role | Typical user | Can do |
|---|---|---|
owner, admin |
Agency management | Everything, including API keys, office users, webhooks, period lock, kiosk devices, export formats, GDPR erasure |
office, planner |
Intercedent / planner | Master data, hours, approvals, exports, rosters, leave and expense decisions |
accountant |
External accountant | Read only |
employee |
Flex worker | Own hours (app/kiosk), own leave, expenses, availability |
| client (no account) | Client contact | Approve, correct or reject via a magic link |
API keys act as accountant (read scope), office (write) or admin + office (admin). No key can create keys or change office roles. See authentication.