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.

CLI (tt)

Reference for the tt command line: install, authentication, every command and option, output formats and exit codes.

Last updated: · Markdown version

tt is the command line for the TimeTracking REST API (@timetracking/cli). It covers the weekly close (overview, conflicts, approval, export), master data, leave, expenses, rosters and the inbox, plus a raw tt api escape hatch for every other endpoint.

Everything tt does goes through the public API at https://app.timetracking.tech/api/v1. It has no extra permissions: what an API key cannot do over HTTP, tt cannot do either.

Install

npm i -g @timetracking/cli      # installs the `tt` binary
npx @timetracking/cli week      # or run without installing
tt --version

Requires Node.js 18 or later (uses the built-in fetch).

Authenticate

Create an API key as an admin in the dashboard under "Regels & koppelingen" (Rules & integrations) → "API-sleutels". Tick "Mag ook schrijven" (may also write) for a key with the write scope; leave it unticked for a read-only key. A key with the admin scope (needed for tt lock) can only be created over the API by an admin user: POST /api-keys {"name":"…","scopes":["read","admin"]} with the admin's own access token; no key can create keys. See Authentication.

tt login --key tt_live_...                 # verifies the key with GET /org/week, then saves it
tt login --key tt_live_... --url http://localhost:3001 --skip-verify
tt whoami                                  # shows URL, masked key, where each came from, current week
tt logout                                  # removes the stored key (URL is kept)

Configuration file: $XDG_CONFIG_HOME/timetracking/config.json, default ~/.config/timetracking/config.json, written with mode 0600:

{ "apiKey": "tt_live_...", "url": "https://app.timetracking.tech" }

Precedence, highest first:

  1. Flags --key KEY and --url URL (on any command)
  2. Environment variables TT_API_KEY and TT_API_URL
  3. The config file
  4. Default URL https://app.timetracking.tech (there is no default key)

--url / TT_API_URL is the app origin, not the /api/v1 path.

What a key can do

Key scope Behaves like Works Fails with 403
read accountant (read-only) all list/read commands: tt week, tt conflicts, tt approve --dry-run, tt people list, tt placements list, tt clients list, tt leave list, tt expenses list, tt shifts, tt inbox, tt whoami; via tt api every GET every write, and tt export (even preview: it is a POST, answered API key is read-only)
read + write office user everything above plus writes, preview/commit export tt lock (Admin role required)
admin (implies write) admin + office everything, including tt lock and admin endpoints via tt api (webhooks, kiosk devices, listing/revoking keys) creating API keys or changing office roles (Not available with an API key)

tt lock needs an admin-scope key. With a write key, lock in the dashboard ("Export" → "Periode vergrendelen") as an admin. See Periods and locking.

Global options

Option Meaning
--json Print raw JSON (the unwrapped data of the API response) instead of tables
--key KEY API key; overrides TT_API_KEY and the config file
--url URL API origin; overrides TT_API_URL; default https://app.timetracking.tech
-h, --help Help for a command: tt <command> --help, tt help <command>
--version, -v Print version

Input conventions:

  • Dates are YYYY-MM-DD. --week takes any day in the ISO week (Mon–Sun); today and last-week are also accepted.
  • Durations accept 7.5, 7,5, 7:30, 7u30 and 450m. Output durations are h:mm.
  • Clock times are HH:MM.

Exit codes

Code Meaning
0 OK
1 API error (HTTP error from the server, network error). Message format: error: <message> [<code> <status>], followed by one line per validation issue ( path: message)
2 Usage error (unknown command or option, missing argument, unparseable value, missing API key)

Example API error on stderr:

error: This day is exported or locked; book a correction in an open period [locked 423]

Commands

tt login

Store an API key (and API URL) in the config file.

tt login --key tt_live_… [--url https://app.timetracking.tech] [--skip-verify]
  • Keys must start with tt_; otherwise exit 2.
  • Without --skip-verify the key is tested with GET /org/week before saving.

tt logout

Remove the stored API key. tt logout.

tt whoami

Validate the key and show the connection: URL and its source (flag, env, config, default), masked key and its source, current week, number of placements and open conflicts. Calls GET /org/week.

tt whoami --json

tt week

Hours per placement for one week, one column per day, with status.

tt week [--week DATE] [--client NAME] [--json]
  • --week, -w: any day in the ISO week. Default: current week.
  • --client: only rows whose client name contains NAME (case-insensitive, filtered client-side).
  • Cells are h:mm; ! marks a day with an open conflict.
  • API: GET /org/week. --json prints the full response (weekStart, weekEnd, rows[], conflicts[], kpis).

tt hours add

Record hours for a placement as an office entry (claim source manual_office).

tt hours add --placement ID --date DATE (--hours 7.5 | --start 07:00 --end 15:30 [--break 30]) [--note TEXT]
Option Meaning
--placement, -p Placement UUID (from tt placements list)
--date, -d Work date
--hours Net worked duration (7.5, 7,5, 7:30, 7u30, 450m). Not combinable with --start/--end or --break
--start, --end Clock times; both required together. Overnight shifts: end earlier than start
--break Break in minutes (30) or as a duration (0:30); only with --start/--end
--note Free text, max 1000 characters

More than 24 hours in a day is rejected locally. The claim is matched against other sources for the same placement and day; a disagreement creates a conflict (the output says CONFLICT — see tt conflicts). Exported or locked days return 423 locked and nothing is stored. API: POST /claims. See Sources and conflicts.

tt hours add -p 00000000-0000-4000-8000-0000000000e4 -d 2026-12-01 --start 07:00 --end 15:30 --break 30
# Recorded 8:00 on 2026-12-01 → day status draft

tt approve

Approve all submitted, conflict-free days of a week.

tt approve --week DATE [--client NAME] [--dry-run]
  • Reads the week with GET /org/week, collects submitted days without a conflict, and posts them to POST /days/approve.
  • Days of clients with approval mode client or both stay waiting until the client has approved too (waitingForClient).
  • --dry-run prints which days would be approved and changes nothing (works with a read-only key).

Output: Approved 2 day(s) · 0 waiting for client · 0 skipped.

tt conflicts

List open conflicts between hour sources for a week.

tt conflicts [--week DATE] [--json]

One row per candidate: conflict id, date, person, client, claim id, source, hours. Data comes from conflicts in GET /org/week.

tt conflicts resolve

Resolve a conflict by picking the winning claim.

tt conflicts resolve CONFLICT_ID --claim CLAIM_ID [--note TEXT]

API: POST /conflicts/{id}/resolve. Resolving an already resolved conflict returns 409 conflict.

tt export

Preview (default) or commit a payroll export.

tt export --week DATE [--mapping preset-sdworx] [--commit] [-o file.csv]
tt export --from DATE --to DATE ...
Option Meaning
--week, -w Any day in the ISO week (Mon–Sun)
--from, --to Explicit inclusive range; both required together
--mapping, -m Export mapping id. Default preset-sdworx. Other built-in: preset-generic. See Presets
--commit Commit instead of preview. Irreversible: included days move to exported
--output, -o Output file for --commit. Default tt-export-START_END.csv; -o - writes the file content to stdout

Without --commit, nothing changes. The preview shows rows, issues (block a commit), warnings (rule-pack compliance hints such as break_too_short), the number of approved days included and pendingCount (draft/submitted days that are NOT included).

With --commit, the file content is written and the command prints the run id, row count and SHA-256 content hash. The same range cannot be committed twice within one minute (409); a second commit with nothing newly approved returns 422 Nothing approved to export in this period. API: POST /exports.

tt export --week 2026-11-30 --mapping preset-generic
tt export --week 2026-11-30 --mapping preset-generic --commit -o week49.csv

tt lock

Lock a period after export (admin; irreversible).

tt lock --week DATE --yes [--kind week]
tt lock --from DATE --to DATE --kind month --yes
  • Kinds: week, four_weeks, semi_month, month. Default week.
  • --yes is required; without it the command exits 2.
  • API: POST /periods/lock.
  • Needs an admin-scope key; a write key gets Admin role required [forbidden 403].

tt people list

tt people list [--all] [--json]

Active people by default; --all includes inactive people. Columns: employee number, name, email, app access (yes, invited, no), active placements, id. API: GET /people.

tt people add

Create a person, or update one with --id.

tt people add --first NAME --last NAME [--email E] [--number EMPLOYEE_NO] [--phone P] [--locale nl] [--id ID]

API: POST /people. Update replaces the whole record: optional fields you omit (email, phone, locale, employee number) are cleared, and active resets to true. Pass every field you want to keep. Read the current values with tt people list --json first.

To create or update a person by your own system's id, use the external-ID upsert through tt api (see v1.1 endpoints via tt api). There is no tt command for it yet.

tt placements list

tt placements list [--client NAME] [--all] [--json]

Columns: id, ref, person, client, role, from, until. API: GET /placements.

tt clients list

tt clients list [--json]

Columns: id, name, approval mode (office, client, both), number of active placements. API: GET /clients.

tt import

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

tt import FILE.csv [--commit]

Without --commit this is a dry run that reports what would change. Format and header aliases: CSV import. API: POST /import.

tt leave list, tt leave approve, tt leave reject

tt leave list [--status requested|approved|rejected|cancelled] [--person ID] [--from DATE] [--to DATE] [--json]
tt leave approve ID [--note TEXT]
tt leave reject ID [--note TEXT]

APIs: GET /leave, POST /leave/{id}/decide. Needs a write-scope key (office role) even for list.

tt expenses list, tt expenses approve, tt expenses reject

tt expenses list [--status submitted|approved|rejected] [--person ID] [--from DATE] [--to DATE] [--json]
tt expenses approve ID [ID…]
tt expenses reject ID [ID…]

APIs: GET /expenses (default range: last 60 days), POST /expenses/decide. Needs a write-scope key.

tt shifts

Roster (planned shifts) for a week, with planning issues (leave, unavailable, overlap, rest, outside_placement).

tt shifts [--week DATE] [--json]

API: GET /shifts?week=. Needs a write-scope key.

tt inbox

Mail/upload items that need review (status needs_review or week_conflict).

tt inbox [--json]

API: GET /inbox. Needs a write-scope key. Apply or ignore items with tt api (below) or in the dashboard. See Inbound email.

tt api

Raw API call for any endpoint.

tt api METHOD PATH [JSON|@file.json|-]
  • METHOD: GET, POST, PUT, PATCH, DELETE.
  • PATH is relative to /api/v1, including the query string.
  • Body: inline JSON, @file.json, or - for stdin.
  • Prints the unwrapped data as JSON.
tt api GET "/org/week?week=2026-10-19"
tt api POST /approval-links '{"clientId":"00000000-0000-4000-8000-0000000000c2","startsOn":"2026-11-30","endsOn":"2026-12-06"}'
tt api POST /inbox/items/ITEM_ID/apply '{"placementId":"…","workDate":"2026-10-20","minutes":480}'
cat claim.json | tt api POST /claims -

v1.1 endpoints via tt api

API v1.1 added endpoints that have no dedicated tt command (the CLI itself is unchanged). Call them with tt api. Details: API reference, Pagination, Idempotency.

# Upsert a person by external ID (idempotent; response has "created": true|false)
tt api PUT /people/by-external/bullhorn/12345 '{"employeeNumber":"9910","firstName":"Eva","lastName":"Docs","email":"eva.docs@example.com"}'

# Upsert a client by external ID
tt api PUT /clients/by-external/bullhorn/C-77 '{"name":"Bakkerij Brood & Co","approvalMode":"office"}'

# Placement linked by the same source's external refs
tt api PUT /placements/by-external/bullhorn/P-501 '{"personExternalRef":"12345","clientExternalRef":"C-77","ref":"BH-P-501","startsOn":"2026-12-01"}'

# Read one record, deactivate a person (DELETE = active:false)
tt api GET /people/69a47995-bc6a-4bc9-9b9b-070935b974bb
tt api DELETE /people/69a47995-bc6a-4bc9-9b9b-070935b974bb

# Cursor pagination and change feeds (read-only key is enough)
tt api GET "/people?limit=100&updated_since=2026-10-23T00:00:00Z"
tt api GET "/days?from=2026-11-30&to=2026-12-13&status=approved,exported&limit=500"
tt api GET "/claims?from=2026-11-30&to=2026-12-06&source=mail"

# Batch (1–500 items; one bad row never aborts the rest)
tt api POST /claims/batch @claims.json

# Export runs and the exact committed file
tt api GET /exports/runs
tt api GET /exports/runs/239a1898-fab5-486c-a2be-92d939be90cc

tt api prints the unwrapped data as JSON, so a {items, next_cursor} page comes back as is; loop on next_cursor in your script. GET /exports/runs/{id}/file returns CSV text, not JSON; fetch it with curl instead. tt api sends no Idempotency-Key header (the API honours it on every authenticated write); use curl or your own client when you need one.

Admin endpoints (webhooks incl. deliveries, test, replay, rotate secret; period lock; kiosk devices; listing and revoking API keys) need an admin-scope key. Creating keys is never possible with a key.

tt api GET /webhooks/WEBHOOK_ID/deliveries          # admin key
tt api POST /webhook-deliveries/123/replay          # admin key

Scripting patterns

# Fail a CI job when the week still has open conflicts
n=$(tt conflicts --week last-week --json | jq length)
[ "$n" -eq 0 ] || { echo "$n open conflicts"; exit 1; }

# Preview as JSON and stop on validation issues
tt export --week last-week --mapping preset-generic --json | jq -e '.ok' > /dev/null

# Placement id by reference
tt placements list --json | jq -r '.[] | select(.ref=="PL-2026-104") | .id'