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.

Exact Online payroll and project hours

Send approved TimeTracking hours to Exact Online, either as payroll variable data (Exact Online Loonadministratie) or as project time transactions for invoicing.

Last updated: · Markdown version

1. Summary

What syncs Approved hours (per hour type), allowances, approved expenses; optionally per client for invoicing
Direction TimeTracking → Exact Online
Mechanism A. CSV export (custom mapping) imported in Exact Online payroll. B. Script: commit export → Exact Online REST API (project time transactions)
Trigger Payroll period close (A) or weekly (B)

Status: Recipe — works today with an export file or your own script. No native Exact Online connector.

2. Mechanism

A. Payroll (Exact Online Loonadministratie / Payroll NL). A custom export mapping writes one line per employee per wage component per period. Assumption — Exact Online payroll can import variable payroll mutations (looncomponenten per employee per period) from a CSV/Excel template; verify the template with your Exact consultant.

B. Project hours (for invoicing clients). A script commits the TimeTracking export with a per-day mapping that includes clientName and placementRef, then posts each line to Exact Online. Assumption — Exact Online exposes TimeTransactions in its REST API (/api/v1/{division}/project/TimeTransactions, OAuth 2.0), keyed on Employee, Account (customer), Project/Item, Date and Quantity (hours). Verify required fields (Item, HourStatus) with your Exact consultant.

3. Prerequisites

  • TimeTracking admin; API key with write scope.
  • employee_number = Exact Online employee code.
  • For A: Exact payroll wage component codes per hour type.
  • For B: Exact Online app registration (client id/secret, redirect URI), division number, and a mapping client → Exact Account code (store it in TimeTracking clients.external_ref), placement → Exact Project code (store it in placement ref if unique), hour type → Exact Item (hour type item).

4. Entity mapping

Exact Online object / field TimeTracking Notes / external ID
Employee (code) employee_number → employeeNumber Only stable key today.
Account (customer code) clients.external_ref Not a PayLine field; look it up via GET /clients by clientName.
Project placements.ref → placementRef Or a project per client.
Item (hour type, e.g. normal/overtime) hourType via map
Quantity minutes → hours_decimal (decimal point for the API)
Date PayLine date No groupBy for project hours.
Wage component (payroll) hourType via map Option A.
Amount amountCents Expenses (A: wage component; B: separate cost transactions).

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 exact with the Exact employee code, Account code and project code as refs.

5. Step-by-step setup

export TT=https://app.timetracking.tech/api/v1
export KEY=tt_live_…        # write scope
  1. Store the Exact Account code on each client (send all fields; POST /clients with id replaces the record):
curl -s -X POST "$TT/clients" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"id":"00000000-0000-4000-8000-0000000000c3","name":"Bakkerij Ruijters","externalRef":"20031","approvalMode":"office"}'

Note: mailDomains is replaced too; include it if the client has inbound mail domains (comma-separated string).

  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 Exact consultant):
{
  "id": "exact-project",
  "name": "Exact Online — projecturen",
  "format": "csv", "delimiter": ";", "includeHeader": true, "encoding": "utf-8", "lineEnding": "lf",
  "columns": [
    { "header": "Employee", "source": "employeeNumber" },
    { "header": "Client", "source": "clientName" },
    { "header": "Project", "source": "placementRef" },
    { "header": "Date", "source": "date", "transform": { "kind": "date", "format": "YYYY-MM-DD" } },
    { "header": "Item", "source": "hourType", "transform": { "kind": "map", "table": "items", "fallback": "UREN" } },
    { "header": "Hours", "source": "minutes", "transform": { "kind": "hours_decimal", "decimals": 2 } }
  ],
  "tables": { "items": { "normal": "UREN", "overtime_125": "UREN125", "ort_sunday": "UREN-ZO" } },
  "validation": [{ "column": "Employee", "required": true }, { "column": "Project", "required": true }]
}

Item codes are placeholders.

  1. Commit and push (Python sketch):
import csv, io, os, requests

TT = "https://app.timetracking.tech/api/v1"
H = {"Authorization": f"Bearer {os.environ['TT_API_KEY']}"}
accounts = {c["name"]: c["external_ref"] for c in requests.get(f"{TT}/clients", headers=H).json()["data"]}
run = requests.post(f"{TT}/exports", headers=H, json={
    "mappingId": "exact-project", "startsOn": "2026-11-30", "endsOn": "2026-12-06", "mode": "commit"}).json()
if "error" in run:
    raise SystemExit(run["error"])
open("exact-w49.csv", "w", encoding="utf-8").write(run["data"]["content"])
for row in csv.DictReader(io.StringIO(run["data"]["content"].lstrip("")), delimiter=";"):
    tx = {"Employee": row["Employee"], "Account": accounts.get(row["Client"]), "Project": row["Project"],
          "Date": row["Date"], "Item": row["Item"], "Quantity": float(row["Hours"])}
    # Assumption: resolve codes to Exact GUIDs, then
    # POST https://start.exactonline.nl/api/v1/{division}/project/TimeTransactions (verify fields with Exact consultant)
  1. Option A: an admin builds a payroll mapping in the dashboard (same shape as the Loket example, with your Exact wage component codes), then tt export --from 2026-11-01 --to 2026-11-30 --mapping exact-payroll --commit -o exact-loon-2026-11.csv and import the file in Exact payroll.

  2. An admin locks the period in the dashboard after Exact processed it.

6. Sync schedule and triggering events

  • A: per payroll period. B: weekly after approval (invoicing).
  • export.completed {runId,startsOn,endsOn,rows,mapping} → download with GET /exports/runs/{runId}/file; period.locked {startsOn,endsOn,kind}. Admin creates webhooks.
  • For invoicing without a payroll commit, read approved day values directly: GET /days?from=…&to=…&status=approved,exported&client_id=<id> (read key; real minutes, start/end, per placement). Hour types are not split there; map everything to one Exact item or use the export.
  • Note: one commit moves days to exported. If you need both payroll (A) and invoicing (B) from the same days, do one commit and derive both files from the saved content of a mapping that holds all fields, because a second mapping cannot re-export the same days.

7. Corrections, conflicts and locked periods

  • Only approved, conflict-free days are exported.
  • Exported/locked days are immutable (423). Book corrections in an open week; they become new Exact transactions.
  • Push failures after commit: retry from the saved file, never re-commit.

8. Testing checklist

  • Every client has an Exact Account code in external_ref; every placement ref is a valid Exact project.
  • Item mapping covers all hour types in the preview.
  • Test push in an Exact test division.
  • Payroll and invoicing both use the same committed run (no double commit).

9. Troubleshooting

HTTP / code Message Fix
403 forbidden API key is read-only write key.
404 not_found Export mapping not found Check mapping id.
409 conflict This period was just exported; refresh before exporting again Avoid double commit.
422 invalid Export has N validation issue(s) Missing employee number or placement ref.
422 invalid Nothing approved to export in this period Approve first; or days already exported by another mapping.
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

  • Exports are exclusive: committed days cannot be exported again with another mapping.

  • clients.external_ref is not a PayLine source; join it yourself via GET /clients.

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

  • 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.

  • 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 TimeTracking → Exact Online project-hours push in Python.
- Load clients via GET /api/v1/clients?limit=500 (follow next_cursor; external_ref = Exact Account code).
- For invoicing only, prefer GET /api/v1/days?status=approved,exported&client_id=… over committing an export.
- Preview POST /api/v1/exports (mappingId "exact-project"); on --commit, commit once and save content + runId.
- Post each line to Exact Online TimeTransactions (OAuth 2.0, division from env). Mark field names TODO
  ("verify with Exact consultant"); resolve codes to GUIDs with cached lookups.
- Make the push resumable from the saved file; log failures per line.