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.

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 write scope.
  • A shared employee key: Planbition employee number = TimeTracking employee_number.
  • A way to resolve the TimeTracking placementId for a Planbition shift (person + client + date). Build a lookup from GET /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.completed download the file with GET /exports/runs/{runId}/file.
  • claim.created fires 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 submitted and clears approvals when the value changes.
  • Claims on exported/locked days are refused with 423 before 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=import shows no extra rows).
  • A retried batch with the same Idempotency-Key returns idempotent-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-Key replays only within 24 h; use deterministic claim ids 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 by GET /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 JSON content and 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.