Easyflex payroll and placements
Sync Easyflex flex workers and placements into TimeTracking and send approved hours back to Easyflex per placement, using CSV import, the REST API and an hours file.
Last updated: · Markdown version
1. Summary
| What syncs | In: flex workers, clients (inleners), placements. Out: approved hours per placement per hour type, expenses, leave |
| Direction | Easyflex → TimeTracking (master data), TimeTracking → Easyflex (hours) |
| Mechanism | Master data: CSV import (POST /import) or REST upserts. Hours: CSV export with a custom mapping imported as an Easyflex hours file, or a script against Easyflex web services |
| Trigger | Master data daily or on change; hours per payroll week |
Status: Recipe — works today with files and your own script. No native Easyflex connector.
2. Mechanism
Easyflex is a staffing back office: it owns flex workers, placements (plaatsingen) and payroll per placement. TimeTracking needs the same placements so hours are recorded per placement.
- Master data in. Export flex workers and placements from Easyflex to CSV and import them with CSV import (
personeelsnummer,opdrachtgever,plaatsing,startdatum…). People are matched onemployee_number, placements onref, clients on name. For a scripted sync, use the by-external upserts and batch endpoints instead (step 2b). - Hours out. After approval, a custom export mapping writes one line per placement per day (or per week) per hour type with the Easyflex placement number. You import it in Easyflex as an hours file.
Assumption — Easyflex accepts hours imports per placement (plaatsingnummer) with an hour code or wage component (uurcode / looncomponent) and supports SOAP/XML web services for flex workers, placements and hours. File layout, web service names and authentication must be verified with your Easyflex consultant.
3. Prerequisites
- TimeTracking admin; API key with
writescope. - Easyflex flex worker number (registratienummer) stored as TimeTracking
employee_number. - Easyflex placement number stored as TimeTracking placement
ref. - Easyflex hour codes per TimeTracking hour type.
- Rule pack aligned with the CAO (ABU/NBBU or inlenersbeloning). Assumption — some agencies let Easyflex calculate surcharges itself; then export only worked hours per day with times and map every hour type to the base code (see section 10).
4. Entity mapping
| Easyflex object / field | TimeTracking | Notes / external ID |
|---|---|---|
| Flexkracht registratienummer | people.employee_number |
CSV header must be personeelsnummer, werknemersnummer, nummer or employee_number; registratienummer is not an accepted alias, so rename it. Only stable key today. |
| Voornaam / achternaam / e-mail / mobiel | first_name, last_name, email, phone |
CSV aliases voornaam, achternaam, email, telefoon/mobiel. |
| Relatie / inlener | clients.name |
Import matches clients by name (case-insensitive). external_ref can be set via POST /clients. |
| Plaatsing (nummer) | placements.ref |
CSV plaatsing / plaatsingsnummer. Used to update existing placements. |
| Functie | placements.role_title |
CSV functie. |
| Begindatum / einddatum | starts_on / ends_on |
DD-MM-JJJJ or YYYY-MM-DD. |
| Uurcode / looncomponent | PayLine hourType via map |
|
| Uren | minutes → hours_decimal |
|
| Datum | PayLine date |
Do not groupBy if Easyflex needs per-day lines. |
| Declaraties | expense_<kind> lines, amountCents |
External IDs: upsert people, clients and placements idempotently with PUT /{people|clients|placements}/by-external/{source}/{ref} (or POST /…/batch with externalSource + externalRef per row, up to 500 rows). The response includes external_source, external_ref and created: true|false. Use source easyflex with the registratienummer, relatie number and plaatsingnummer as refs. Easyflex stays the source of truth.
5. Step-by-step setup
export TT=https://app.timetracking.tech/api/v1
export KEY=tt_live_… # write scope
- Export placements from Easyflex to CSV and rename headers to TimeTracking aliases:
personeelsnummer;voornaam;achternaam;email;telefoon;opdrachtgever;plaatsing;functie;startdatum;einddatum
104577;Piotr;Nowak;piotr@example.com;+48500100200;Logistiek Parkstad;PL-2026-103;Heftruckchauffeur;01-10-2026;
- Dry run, then import:
tt import easyflex-plaatsingen.csv # dry run
tt import easyflex-plaatsingen.csv --commit
Real dry-run response shape:
{ "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": [] } }
2b. For a scripted daily sync, upsert by Easyflex ids instead of the CSV import (idempotent, safe to rerun):
# client
curl -s -X PUT "$TT/clients/by-external/easyflex/REL-3301" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
-d '{"name":"Logistiek Parkstad","externalRef":"REL-3301","approvalMode":"client"}'
# flex worker
curl -s -X PUT "$TT/people/by-external/easyflex/104577" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
-d '{"employeeNumber":"104577","firstName":"Piotr","lastName":"Nowak","email":"piotr@example.com","locale":"pl"}'
# placement, linked by the external refs of the same source
curl -s -X PUT "$TT/placements/by-external/easyflex/PL-2026-103" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
-d '{"personExternalRef":"104577","clientExternalRef":"REL-3301","ref":"PL-2026-103","roleTitle":"Heftruckchauffeur","startsOn":"2026-10-01"}'
Real response shape (placement upsert, demo data):
{ "data": { "id": "96574dce-db96-4da1-82cf-de32021472d9", "person_id": "69a47995-bc6a-4bc9-9b9b-070935b974bb",
"client_id": "a3254709-d152-49b4-ad5e-51d50147910a", "ref": "PL-555", "role_title": "Lasser", "starts_on": "2026-12-01",
"ends_on": null, "active": true, "external_source": "bullhorn", "external_ref": "PL-555", "created": true } }
For volume, send up to 500 rows per call to POST /people/batch or POST /placements/batch with externalSource and externalRef on each row; the response lists {index, ok, data | error} per row and one bad row never aborts the batch. Ended placements: DELETE /placements/{id} deactivates (active: false). Send an Idempotency-Key header on writes so a network retry replays the stored response (idempotent-replayed: true).
- Create the mapping yourself (admin, dashboard): open the export mappings screen (
/export/mappings, linked from Export), copypreset-genericorpreset-sdworx, edit columns, code tables and validation, and optionally set it as the org default. Saved mappings are validated; max 50 per org. The mapping id is the UUID in its URL (/export/mappings/<uuid>): pass it asmappingIdtoPOST /exportsandtt export --mapping. Example JSON for the editor (assumed layout — verify with your Easyflex consultant):
{
"id": "easyflex-uren",
"name": "Easyflex — urenbestand per plaatsing",
"format": "csv",
"delimiter": ";",
"includeHeader": true,
"encoding": "utf-8",
"lineEnding": "crlf",
"columns": [
{ "header": "Registratienummer", "source": "employeeNumber" },
{ "header": "Plaatsingnummer", "source": "placementRef" },
{ "header": "Datum", "source": "date", "transform": { "kind": "date", "format": "DD-MM-YYYY" } },
{ "header": "Uurcode", "source": "hourType", "transform": { "kind": "map", "table": "uurcode" } },
{ "header": "Uren", "source": "minutes", "transform": { "kind": "hours_decimal", "decimals": 2, "decimalSeparator": "," } },
{ "header": "Bedrag", "source": "amountCents", "transform": { "kind": "cents_decimal", "decimalSeparator": "," } }
],
"tables": { "uurcode": { "normal": "100", "overtime_125": "125", "overtime_150": "150", "ort_evening": "201",
"ort_night": "202", "ort_saturday": "203", "ort_sunday": "204", "holiday": "205",
"expense_km": "KM", "expense_public_transport": "OV" } },
"validation": [
{ "column": "Registratienummer", "required": true },
{ "column": "Uurcode", "required": true },
{ "column": "Uren", "min": 0, "max": 24 }
]
}
Leave lines have no placement (placementRef empty). If Easyflex needs a placement for leave, keep leave in Easyflex and leave leave_* unmapped — then remove Uurcode required or add a fallback.
- Weekly: preview, commit, import:
tt export --week 2026-11-30 --mapping easyflex-uren
tt export --week 2026-11-30 --mapping easyflex-uren --commit -o easyflex-w49.csv
- After Easyflex processed the week, an admin locks it in the dashboard.
6. Sync schedule and triggering events
| Flow | Schedule | Event |
|---|---|---|
| Placements Easyflex → TimeTracking | Daily, or when a placement starts/ends | Upserts emit person.created/person.updated and placement.created/placement.updated |
| Hours TimeTracking → Easyflex | Weekly after close | export.completed → download with GET /exports/runs/{runId}/file |
| Period final | After Easyflex payroll | period.locked |
7. Corrections, conflicts and locked periods
- Shortening a placement fails with
409"N registration(s) exist after " when hours exist after the new end date. - Only approved, conflict-free days are exported; exported/locked days return
423on new claims. - Corrections for an exported week: record them in the current open week; they reach Easyflex in the next file with their own date.
- Reconcile before export:
GET /days?from=2026-11-30&to=2026-12-06&status=submitted,approved&client_id=<id>(read key) lists day values per placement with status and conflict flag.
8. Testing checklist
- Dry-run import has no failed rows and
unmappedHeaders: []. - Every active Easyflex placement exists in TimeTracking with the same
ref. - Hours file: every line has
Plaatsingnummer; codes mapped. - Test import in Easyflex; compare per-placement totals.
9. Troubleshooting
| HTTP / code | Message | Fix |
|---|---|---|
200 import row ok:false |
personeelsnummer ontbreekt / naam ontbreekt / startdatum ontbreekt of is ongeldig (gebruik DD-MM-JJJJ) | Fix the CSV row. |
409 conflict |
N registration(s) exist after | Hours exist after the new end date; correct hours first. |
403 forbidden |
API key is read-only | write key. |
404 not_found |
Export mapping not found | Check mapping id (UUID from /export/mappings/<uuid>). |
422 invalid |
Person and client must exist (by id or external ref) before the placement | Upsert person and client (same source) first. |
422 invalid |
Export has N validation issue(s) | Preview and fix. |
423 locked |
This day is exported or locked; book a correction in an open period | Correct in an open period. |
429 rate_limited |
Rate limit … exceeded | Wait 60 s. |
10. Limits and known gaps
Clients are matched by name in the CSV import; renaming a client in Easyflex creates a new client. The by-external upserts avoid this.
Neither import nor upserts deactivate missing records; call
DELETE /people/{id}/DELETE /placements/{id}yourself.Mappings are edited by admins in the dashboard only; there is no mapping API.
If Easyflex calculates surcharges itself, use the
plainrule pack so TimeTracking exports onlynormalhours.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 an Easyflex ↔ TimeTracking sync script.
1. Preferred: upsert clients, people and placements with PUT /api/v1/{clients|people|placements}/by-external/easyflex/{ref}
(or the /batch endpoints, 500 rows max). Fallback: read an Easyflex placements CSV export, rename columns to the TimeTracking import aliases
(personeelsnummer, voornaam, achternaam, email, telefoon, opdrachtgever, plaatsing, functie, startdatum, einddatum),
and call POST /api/v1/import with dryRun true; show failed rows. Only import with --commit.
2. Weekly: POST /api/v1/exports with mappingId "easyflex-uren", preview then (only with --commit) commit, and save the CSV.
3. Report placements that exist in Easyflex but not in TimeTracking (match on placement ref) and vice versa.
Mark any Easyflex web-service call as TODO (verify with Easyflex consultant). Use TT_API_KEY from the environment.