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.

Loket.nl payroll export

Get approved TimeTracking hours, allowances, expenses and leave into Loket.nl as variable payroll data, via a CSV export mapping or the Loket API.

Last updated: · Markdown version

1. Summary

What syncs Approved hours per hour type, allowances, approved expenses, approved leave
Direction TimeTracking → Loket.nl
Mechanism CSV export (custom mapping or preset-generic) imported as variable data ("mutaties"), or a script that posts the rows to the Loket API
Trigger Weekly close / payroll period close

Status: Recipe — works today with an export file plus a Loket import, or with your own script against the TimeTracking and Loket APIs. There is no native Loket connector.

2. Mechanism

Two options:

A. File import (recommended to start). TimeTracking renders a CSV with a Loket-shaped export mapping. You import it in Loket.nl as variable data for the payroll period. An admin builds the Loket mapping in the dashboard (copy preset-generic, edit columns and codes); preset-generic alone lacks amounts and codes.

B. API push. A script commits the export (POST /exports, mode: "commit") — or reacts to the export.completed webhook and downloads the file with GET /exports/runs/{runId}/file — parses the CSV, and writes each line to Loket via the Loket.nl REST API. Assumption — Loket.nl offers a REST API (OAuth 2.0) with endpoints for variable wage components per employment and payroll period; endpoint names and required fields must be verified with your Loket consultant or the Loket developer portal.

In both options TimeTracking does the hour-type split (rule pack), so Loket receives final quantities per wage component.

3. Prerequisites

  • TimeTracking admin; API key with write scope (export needs an office-capable credential).
  • employee_number in TimeTracking = Loket personnel number (werknemersnummer). If one person has several employments in Loket, agree which number to use. Assumption — Loket keys variable data on the employment (dienstverband); verify with your Loket consultant.
  • The Loket wage component codes (looncomponenten) for each TimeTracking hour type.
  • The exact Loket import layout for variable data (columns, separator, decimal sign, period notation). Assumption — verify with your Loket consultant.
  • For option B: Loket API credentials (client id/secret) and the employer id.

4. Entity mapping

Loket.nl object / field TimeTracking Notes / external ID
Werkgever TimeTracking org Constant column via const:<werkgeversnummer> if the import needs it.
Werknemer / dienstverband (personnel number) people.employee_number → employeeNumber Only stable key today.
Looncomponent (code) hourType via a map table One code per hour type, allowance, expense kind, leave type.
Waarde / aantal (hours) minutes → decimal hours hours_decimal, decimal separator per Loket layout.
Bedrag amountCents → cents_decimal Allowances and expenses.
Periode Export range startsOn–endsOn Not a PayLine field; add as const: or let the import screen pick the period.
Verlof Approved leave → leave_<code> Or keep leave in Loket only and leave those codes unmapped (they will then fail validation if marked required — use a fallback).
Kostenplaats clientName or placementRef Optional.

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 for example source loket with the Loket employment id as ref.

5. Step-by-step setup

export TT=https://app.timetracking.tech/api/v1
export KEY=tt_live_…        # write scope
  1. Check that every paid person has an employee_number:
tt people list --json | jq -r '.[] | select(.active and (.employee_number==null)) | "\(.first_name) \(.last_name)"'
  1. Start with the generic preset to see your data:
curl -s -X POST "$TT/exports" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"mappingId":"preset-generic","startsOn":"2026-11-30","endsOn":"2026-12-06","mode":"preview"}' | jq -r '.data.content'

Real output (demo data):

employee_number;employee_name;client;placement;date;hour_type;hours
1004;Sanne Janssen;Bakkerij Ruijters;PL-2026-104;2026-12-01;normal;8.00
1004;Sanne Janssen;Bakkerij Ruijters;PL-2026-104;2026-12-02;normal;7.50

preset-generic has no amount column: expenses and allowances appear with hours 0.00 and lose their money amount. Use a custom mapping for real Loket files.

  1. Create the mapping yourself (admin, dashboard): open the export mappings screen (/export/mappings, linked from Export), copy preset-generic or preset-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 as mappingId to POST /exports and tt export --mapping. Example JSON for the editor (assumed layout — verify with your Loket consultant):
{
  "id": "loket-variabel",
  "name": "Loket.nl — variabele gegevens",
  "format": "csv",
  "delimiter": ";",
  "includeHeader": true,
  "encoding": "utf-8",
  "lineEnding": "crlf",
  "groupBy": ["employeeNumber", "hourType"],
  "columns": [
    { "header": "Werkgever", "source": "const:123456" },
    { "header": "Werknemernummer", "source": "employeeNumber" },
    { "header": "Looncomponent", "source": "hourType", "transform": { "kind": "map", "table": "loket", "fallback": "" } },
    { "header": "Aantal", "source": "minutes", "transform": { "kind": "hours_decimal", "decimals": 2, "decimalSeparator": "," } },
    { "header": "Bedrag", "source": "amountCents", "transform": { "kind": "cents_decimal", "decimalSeparator": "," } }
  ],
  "tables": {
    "loket": {
      "normal": "1001", "overtime_125": "1125", "overtime_150": "1150",
      "ort_evening": "1201", "ort_night": "1202", "ort_saturday": "1203", "ort_sunday": "1204", "holiday": "1205",
      "expense_km": "4010", "expense_public_transport": "4011", "expense_parking": "4012",
      "leave_vacation": "3001", "leave_sick": "3002"
    }
  },
  "validation": [
    { "column": "Werknemernummer", "required": true },
    { "column": "Looncomponent", "required": true },
    { "column": "Aantal", "min": 0, "max": 400 }
  ]
}

The codes 1001… are placeholders. Use your own Loket codes.

  1. Preview with your mapping's id (shown here as loket-variabel; in practice the UUID from /export/mappings/<uuid>), check issues, then commit:
tt export --from 2026-11-30 --to 2026-12-06 --mapping loket-variabel            # preview
tt export --from 2026-11-30 --to 2026-12-06 --mapping loket-variabel --commit -o loket-w49.csv
  1. Import loket-w49.csv in Loket.nl for the right payroll period.

  2. Option B (API push) sketch in TypeScript:

const TT = "https://app.timetracking.tech/api/v1";
const res = await fetch(`${TT}/exports`, {
  method: "POST",
  headers: { authorization: `Bearer ${process.env.TT_API_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ mappingId: "loket-variabel", startsOn: "2026-11-30", endsOn: "2026-12-06", mode: "commit" }),
});
const { data, error } = await res.json();
if (error) throw new Error(`${res.status} ${error.code}: ${error.message}`);
const [header, ...lines] = data.content.replace(/^/, "").trim().split("\r\n").map((l: string) => l.split(";"));
for (const cols of lines) {
  const row = Object.fromEntries(header.map((h: string, i: number) => [h, cols[i]]));
  // Assumption: post to the Loket variable-data endpoint for this employment and period.
  // await loket.post(`/…/variablewagecomponents`, { … row … });
}
console.log("run", data.runId, data.contentHash);
  1. After Loket processed the payroll, an admin locks the period in the dashboard.

6. Sync schedule and triggering events

  • Run per payroll period (week, 4 weeks or month) after the weekly close.
  • export.completed {runId,startsOn,endsOn,rows,mapping} tells downstream tools a run exists. It does not carry the file: fetch it with GET /exports/runs/{runId}/file.
  • Committed files stay retrievable: GET /exports/runs (paginated {items, next_cursor}), GET /exports/runs/{id} (metadata, mapping_snapshot, pay_lines, warnings) and GET /exports/runs/{id}/file (the exact committed file, text/csv). A read-only key is enough.
  • period.locked {startsOn,endsOn,kind} marks the period final.
  • Webhooks are set up by an admin in the dashboard. See Webhook events.

7. Corrections, conflicts and locked periods

  • Only approved, conflict-free days are exported. pendingCount in the preview shows what is left out.
  • Exported/locked days cannot change (423 This day is exported or locked; book a correction in an open period). Record a correction in an open period; it reaches Loket in the next run.
  • For option B: if the Loket push fails halfway, the TimeTracking run is already committed. Re-download the file with GET /exports/runs/{runId}/file and retry only the Loket side. Do not commit again; a retry with the same Idempotency-Key returns the stored commit response (idempotent-replayed: true).

8. Testing checklist

  • Every personnel number in the file exists in Loket for the payroll period.
  • Every hour type in the preview maps to a Loket code (no empty Looncomponent).
  • Decimal separator and header names match the Loket import screen.
  • Test import in a Loket test period, then compare totals per employee with the TimeTracking week overview.
  • Expenses and leave arrive with the right code and amount.

9. Troubleshooting

HTTP / code Message Fix
401 unauthenticated Invalid or revoked API key New key.
403 forbidden API key is read-only Use a write key for POST /exports.
404 not_found Export mapping not found Use the UUID from /export/mappings/<uuid> or a preset id.
409 conflict This period was just exported; refresh before exporting again Do not double-commit.
422 invalid Export has N validation issue(s) Preview, fix issues (missing number or code).
422 invalid Nothing approved to export in this period Approve first or range already exported.
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 (Retry-After).

10. Limits and known gaps

  • Mappings are edited by admins in the dashboard only; there is no mapping API.

  • groupBy keeps the first line's date; if Loket needs a per-day file, omit groupBy.

  • No period field in PayLine; use a constant or select the period in Loket.

  • Send an Idempotency-Key on POST /exports commits; without one, the API still refuses the same range within a minute (409).

  • 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 small Node script "tt-to-loket" that:
1. Calls POST https://app.timetracking.tech/api/v1/exports with mode "preview" for a given startsOn/endsOn and
   mappingId (default "loket-variabel"), prints issues/warnings/pendingCount and stops if ok is false.
2. Only with --commit: calls mode "commit", saves data.content to loket-<start>_<end>.csv and run.json
   (runId, rowCount, contentHash). Never commits twice for the same range; to recover a lost file use
   GET /api/v1/exports/runs/{runId}/file.
3. Optional --push: parses the CSV and posts each row to the Loket.nl API (ask me for the exact endpoint and
   fields; mark them as TODO until verified with our Loket consultant).
Use TT_API_KEY from the environment. Handle 409/422/423/429 (respect Retry-After) with clear messages.