Planbition planning and hours
Combine Planbition workforce planning with TimeTracking: import Planbition hours as claims for matching, or send approved TimeTracking hours to Planbition.
Last updated: · Markdown version
1. Summary
| What syncs | Option 1: Planbition worked/planned hours → TimeTracking claims. Option 2: TimeTracking approved hours → Planbition |
| Direction | Either, per customer; pick one source of truth for rosters |
| Mechanism | Script/iPaaS: Planbition export (file or API) → POST /claims with source: "import"; or TimeTracking export file → Planbition import |
| Trigger | Daily or weekly |
Status: Recipe — works today with a script or iPaaS. No native Planbition connector.
2. Mechanism
Planbition is workforce planning software used by staffing agencies and their clients (rosters, availability, hours). Assumption — Planbition can export planned/realised hours per employee per day (file or API) and can import hours; the exact interface depends on your Planbition contract. Verify with your Planbition consultant.
Option 1 — Planbition hours as a source. Each realised Planbition shift becomes a TimeTracking claim with source: "import" and sourceRef = the Planbition shift id. TimeTracking matches it against app, kiosk and client hours per placement and day; agreeing sources auto-match (priority client_portal > manual_office > mail > import > kiosk > app), disagreeing sources create a conflict for the office. See Sources and conflicts.
Option 2 — TimeTracking hours to Planbition. A custom export mapping writes per-day lines (no groupBy) that Planbition imports as realised hours.
Rosters: TimeTracking has its own roster (/shifts). Do not plan in both systems. If Planbition is the planner, leave TimeTracking shifts unused.
3. Prerequisites
- TimeTracking API key with
writescope. - A shared employee key: Planbition employee number = TimeTracking
employee_number. - A way to resolve the TimeTracking
placementIdfor a Planbition shift (person + client + date). Build a lookup fromGET /placements. - Planbition export/import access.
4. Entity mapping
| Planbition object / field | TimeTracking | Notes / external ID |
|---|---|---|
| Employee number | people.employee_number |
Only stable key today. |
| Customer / department | clients.name or clients.external_ref |
Set externalRef via POST /clients. |
| Contract / assignment | placements (ref) |
Resolve per person + client, valid on the date (starts_on ≤ date ≤ ends_on). |
| Shift id | Claim sourceRef (≤ 200 chars) |
Traceability. |
| Date, start, end, break | workDate, startMin, endMin, breakMin |
Minutes since midnight; endMin < startMin = night shift. |
| Worked hours | minutes (alternative to start/end) |
Placement lookup: GET /placements?person_id=<id>&active=true (paginated {items, next_cursor}) narrows the candidates. If Planbition holds its own contract ids, store them with PUT /placements/by-external/planbition/{contractId} and look them up from the response.
5. Step-by-step setup
export TT=https://app.timetracking.tech/api/v1
export KEY=tt_live_… # write scope
Option 1 — import a realised shift:
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":"import","sourceRef":"planbition:shift:88213"}'
The response is the resolved timesheet day (real capture, from a manual_office claim):
{ "data": { "id": "0ce9b17d-40ab-4542-937a-de57896a7358", "placement_id": "00000000-0000-4000-8000-0000000000e4",
"work_date": "2026-12-01", "minutes": 480, "start_min": 420, "end_min": 930, "break_min": 30,
"status": "draft", "resolution": "auto_single_source", "has_conflict": false, "version": 1 } }
If another source disagrees, the response shows "has_conflict": true and "minutes": null, and a conflict.created webhook fires.
Retries: claims are append-only and per source only the newest counts. Re-sending the same shift adds a duplicate import claim that replaces the earlier one with the same value — harmless for the day value. To be strictly idempotent, generate a deterministic UUID per Planbition shift and pass it as id: a claim with an existing id is not inserted again.
import { v5 as uuidv5 } from "uuid";
const NS = "6f1c0d5e-2b8a-4f43-9d0e-7a1c2b3d4e5f"; // your own namespace UUID
const id = uuidv5(`planbition:${shiftId}`, NS);
For volume, send up to 500 shifts per call to POST /claims/batch ({"items":[…]}, rows run sequentially, one bad row never aborts):
curl -s -X POST "$TT/claims/batch" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
-H "Idempotency-Key: planbition-2026-12-07-run1" \
-d '{"items":[
{"placementId":"96574dce-db96-4da1-82cf-de32021472d9","workDate":"2026-12-07","startMin":420,"endMin":930,"breakMin":30,"source":"import","sourceRef":"planbition:shift:88213"},
{"placementId":"96574dce-db96-4da1-82cf-de32021472d9","workDate":"2026-12-08","minutes":480,"source":"import","sourceRef":"planbition:shift:88214"}]}'
Real response shape (abridged; third row had no hours):
{ "data": { "ok": 2, "failed": 1, "results": [
{ "index": 0, "ok": true, "data": { "work_date": "2026-12-07", "minutes": 480, "status": "draft", "resolution": "auto_single_source" } },
{ "index": 1, "ok": true, "data": { "work_date": "2026-12-08", "minutes": 480, "status": "draft" } },
{ "index": 2, "ok": false, "error": { "code": "invalid", "message": "…Provide either minutes or start and end time…" } } ] } }
With an Idempotency-Key, a retried request with the same key replays the stored response (header idempotent-replayed: true) for 24 h. Each created claim emits claim.created {placementId, workDate, source, minutes, dayId, dayStatus}.
Check what is stored: GET /claims?from=2026-12-07&to=2026-12-13&source=import (raw claims) and GET /days?from=…&to=… (resolved day values), both with a read key.
Option 2 — an admin builds a per-day mapping in the dashboard (/export/mappings, copy preset-generic; its id is the UUID in the URL), for example:
{
"id": "planbition-realisatie",
"name": "Planbition — gerealiseerde uren",
"format": "csv", "delimiter": ";", "includeHeader": true, "encoding": "utf-8", "lineEnding": "crlf",
"columns": [
{ "header": "Personeelsnummer", "source": "employeeNumber" },
{ "header": "Klant", "source": "clientName" },
{ "header": "Datum", "source": "date", "transform": { "kind": "date", "format": "DD-MM-YYYY" } },
{ "header": "Soort", "source": "hourType" },
{ "header": "Uren", "source": "minutes", "transform": { "kind": "hours_hm" } }
],
"tables": {}, "validation": [{ "column": "Personeelsnummer", "required": true }]
}
Column names are assumptions; verify with your Planbition consultant. Then tt export --week 2026-11-30 --mapping planbition-realisatie --commit -o planbition-w49.csv.
6. Sync schedule and triggering events
- Option 1: daily import of yesterday's realised shifts, before the office reviews the week.
- Option 2: weekly after the close; on
export.completeddownload the file withGET /exports/runs/{runId}/file. claim.createdfires for every imported claim (useful for monitoring, noisy for large batches).conflict.created{timesheetDayId, placementId, workDate, candidates}tells you where Planbition and other sources disagree.
7. Corrections, conflicts and locked periods
- Employees cannot edit submitted days; office and imports can still add claims until a day is exported.
- A changed Planbition shift on an approved day sends the day back to
submittedand clears approvals when the value changes. - Claims on exported/locked days are refused with
423before anything is stored.
8. Testing checklist
- Placement lookup finds exactly one placement per person/client/date.
- A matching Planbition shift and app entry auto-match (
resolution: "auto_match"). - A different value creates a conflict visible in
GET /conflicts?week=. - Re-running the import with deterministic
ids changes nothing (GET /claims?source=importshows no extra rows). - A retried batch with the same
Idempotency-Keyreturnsidempotent-replayed: true.
9. Troubleshooting
| HTTP / code | Message | Fix |
|---|---|---|
404 not_found |
Placement not found | Wrong or foreign placement id. |
422 invalid |
Provide either minutes or start and end time | Send minutes or startMin+endMin. |
422 invalid |
Invalid input (issues[]) |
Date must be YYYY-MM-DD; minutes 0–1439 for times. |
403 forbidden |
API key is read-only | write key. |
423 locked |
This day is exported or locked; book a correction in an open period | Period closed; book corrections in an open period. |
429 rate_limited |
Rate limit of 600 requests per minute exceeded | Throttle to < 10 req/s; wait 60 s. |
10. Limits and known gaps
Batch max 500 rows; rows run sequentially, so large imports take time — stay under 600 requests/min.
Idempotency-Keyreplays only within 24 h; use deterministic claimids for permanent dedupe.Matching tolerance (total and clock) is an org setting; default 0 min total, 15 min clock.
Encoding: a mapping with
encoding: "windows-1252"is delivered as Windows-1252 (Latin-1) only byGET /exports/runs/{id}/file(content-type: text/csv; charset=windows-1252, no BOM). Characters outside Latin-1 (€, curly quotes, em dash) become?. The preview/commit JSONcontentand the dashboard download stay UTF-8.
Planned (not in API v1.1) — export-mapping CRUD via the API.
11. Build it with Claude
Build a daily Planbition → TimeTracking import.
Input: a Planbition CSV/API export of realised shifts (ask me for the columns; mark Planbition specifics TODO).
1. Load GET /api/v1/placements once; map (employee_number, client name, date) → placementId.
2. Send shifts in chunks of ≤500 to POST /api/v1/claims/batch (Idempotency-Key per chunk) with source "import", startMin/endMin/breakMin (minutes since midnight),
sourceRef "planbition:shift:<id>" and a deterministic UUIDv5 id so reruns are idempotent.
3. Skip dates in locked/exported periods (a 423 means stop for that day), throttle under 600 req/min,
and print a summary: created, auto-matched, conflicts (has_conflict true), errors.