# Nmbrs payroll export

> Bring approved TimeTracking hours, allowances, expenses and leave into Nmbrs as hours and variable wage components, via file import or the Nmbrs API.

URL: https://timetracking.tech/developers/integrations/payroll/nmbrs
Last updated: 2026-10-24

## 1. Summary

| | |
|---|---|
| What syncs | Approved hours per hour type, allowances, approved expenses, approved leave |
| Direction | TimeTracking → Nmbrs |
| Mechanism | CSV export (custom mapping) imported in Nmbrs, or a script that pushes rows to the Nmbrs API |
| Trigger | Per payroll period, after the weekly close |

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

## 2. Mechanism

**A. File import.** A Nmbrs-shaped export mapping renders one line per employee per hour code (or wage component) per period. You import it in Nmbrs for the run. Assumption — Nmbrs supports importing hours and variable wage components per employee per period from Excel/CSV, keyed on employee number and a code; verify the exact template with your Nmbrs consultant.

**B. API push.** A script commits the TimeTracking export and writes the lines through the Nmbrs API. Assumption — Nmbrs exposes employee hour components and variable wage components per period in its API (classic SOAP services and a newer REST API); verify endpoint names, period semantics and authentication with your Nmbrs consultant.

Nmbrs distinguishes hour codes (uren) from wage components (looncomponenten, amounts). Map hours-type lines (`componentType: "hours"`) to hour codes and money lines (expenses, allowances) to wage components. Because one mapping produces one file, use two mappings if Nmbrs needs two imports, or one mapping with both a quantity and an amount column.

## 3. Prerequisites

- TimeTracking admin; API key with `write` scope.
- `employee_number` in TimeTracking = Nmbrs employee number.
- Nmbrs hour codes and wage component codes for each TimeTracking hour type, expense kind and leave type.
- Nmbrs company id and payroll period convention (week, 4 weeks, month). Pass the matching `startsOn`/`endsOn`.
- For option B: Nmbrs API credentials.

## 4. Entity mapping

| Nmbrs object / field | TimeTracking | Notes / external ID |
|---|---|---|
| Company | TimeTracking org | `const:` column if needed. |
| Employee number | `employee_number` → `employeeNumber` | Only stable key today. |
| Hour code (uursoort) | `hourType` (`normal`, `overtime_125`, `ort_*`, `holiday`, `leave_*`) via `map` | Lines with `componentType: "hours"`. |
| Hours value | `minutes` → `hours_decimal` | Decimal separator per template. |
| Wage component code | `expense_<kind>`, `allowance_<code>` via `map` | Lines with `componentType: "allowance"`. |
| Wage component value | `amountCents` → `cents_decimal` | |
| Period / year | Export range | Not in PayLine; constant or chosen on import. |
| Cost centre / department | `clientName` / `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 `nmbrs` with the Nmbrs employee id as `ref`.

## 5. Step-by-step setup

```sh
export TT=https://app.timetracking.tech/api/v1
export KEY=tt_live_…        # write scope
```

1. Verify employee numbers: `tt people list` (column with the employee number) or `curl -s "$TT/people" -H "Authorization: Bearer $KEY"`.

2. 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 Nmbrs consultant):

```json
{
  "id": "nmbrs-uren",
  "name": "Nmbrs — uren en looncomponenten",
  "format": "csv",
  "delimiter": ";",
  "includeHeader": true,
  "encoding": "utf-8",
  "lineEnding": "crlf",
  "groupBy": ["employeeNumber", "hourType"],
  "columns": [
    { "header": "EmployeeNumber", "source": "employeeNumber" },
    { "header": "Code", "source": "hourType", "transform": { "kind": "map", "table": "codes" } },
    { "header": "Hours", "source": "minutes", "transform": { "kind": "hours_decimal", "decimals": 2, "decimalSeparator": "," } },
    { "header": "Amount", "source": "amountCents", "transform": { "kind": "cents_decimal", "decimalSeparator": "," } },
    { "header": "Type", "source": "componentType", "transform": { "kind": "upper" } }
  ],
  "tables": {
    "codes": {
      "normal": "U100", "overtime_125": "U125", "overtime_150": "U150",
      "ort_evening": "U210", "ort_night": "U220", "ort_saturday": "U230", "ort_sunday": "U240", "holiday": "U250",
      "leave_vacation": "U300", "leave_sick": "U310",
      "expense_km": "L410", "expense_public_transport": "L420", "expense_parking": "L430", "expense_meal": "L440"
    }
  },
  "validation": [
    { "column": "EmployeeNumber", "required": true },
    { "column": "Code", "required": true }
  ]
}
```

Codes `U100`… are placeholders. `Type` renders `HOURS` or `ALLOWANCE`, so one file can be split by type before import if Nmbrs needs two imports.

3. Preview, then commit (irreversible) and import:

```sh
curl -s -X POST "$TT/exports" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"mappingId":"nmbrs-uren","startsOn":"2026-11-02","endsOn":"2026-11-29","mode":"preview"}' \
  | jq '.data | {ok, issues, warnings, pendingCount, rows: (.rows|length)}'

tt export --from 2026-11-02 --to 2026-11-29 --mapping nmbrs-uren --commit -o nmbrs-p11.csv
```

4. Import `nmbrs-p11.csv` in Nmbrs for the matching period. For option B, parse `data.content` from the commit response (or download it later with `GET /exports/runs/{runId}/file`) and post each line to the Nmbrs API (see the Loket guide for a TypeScript parsing sketch: [Loket.nl](https://timetracking.tech/developers/integrations/payroll/loket#5-step-by-step-setup)).

5. After Nmbrs processed the run, an admin locks the period in the dashboard.

## 6. Sync schedule and triggering events

- Per Nmbrs payroll period. For 4-weekly payroll pass the 28-day range; for monthly the calendar month.
- `export.completed` and `period.locked` webhooks signal progress (an admin creates them in the dashboard, or via the API with an admin user token or an `admin`-scope key). On `export.completed`, download the file with `GET /exports/runs/{runId}/file`. See [Webhook events](https://timetracking.tech/developers/webhooks/events).
- 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.

## 7. Corrections, conflicts and locked periods

- Unapproved and conflicted days stay out of the file (`pendingCount`).
- Exported and locked days are immutable (`423`). Book corrections in an open period.
- If an Nmbrs import is rejected after commit, fix it on the Nmbrs side using the saved file. Do not record the hours again in TimeTracking.

## 8. Testing checklist

- [ ] All codes mapped; preview `issues: []`.
- [ ] Hours vs amounts land in the correct Nmbrs component type.
- [ ] Test import into a Nmbrs test run; totals per employee equal the TimeTracking overview.
- [ ] Leave hours are not counted twice if leave is also registered in Nmbrs.

## 9. Troubleshooting

| HTTP / code | Message | Fix |
|---|---|---|
| 401 `unauthenticated` | Invalid or revoked API key | New key. |
| 403 `forbidden` | API key is read-only | `write` key for exports. |
| 404 `not_found` | Export mapping not found | Check the mapping id. |
| 409 `conflict` | This period was just exported; refresh before exporting again | Avoid double commit. |
| 422 `invalid` | Export has N validation issue(s) | Preview and fix missing numbers/codes. |
| 422 `invalid` | Nothing approved to export in this period | Approve days first. |
| 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 | Retry after 60 s. |

## 10. Limits and known gaps

- Mappings are edited by admins in the dashboard only; there is no mapping API.
- One mapping = one file; hours and wage components share it unless you use two mappings.
- `groupBy` keeps the first line's date.
- Leave double-counting risk if leave is managed in both systems: pick one source.

- 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

```text
Help me connect TimeTracking to Nmbrs.
1. Using the TimeTracking MCP tools, list people and flag any without an employee number.
2. Run export_preview with mappingId "preset-generic" for 2026-11-02..2026-11-29 and list every distinct hour_type.
3. Draft a Nmbrs ExportMapping JSON (schema: docs/developers/data-out/export-mappings.md) with a codes table that
   has one TODO code per hour_type; split hours vs amounts using componentType.
4. Do not commit any export. Mark every Nmbrs-specific field as "verify with Nmbrs consultant".
```
