# Periods and locking

> Payroll period kinds, how export commit and period lock freeze hours, the preconditions for locking, and how to handle corrections after a period is closed.

URL: https://timetracking.tech/developers/concepts/periods-and-locking
Last updated: 2026-10-24

## Payroll period kinds

| `kind` | Range | Notes |
|---|---|---|
| `week` | ISO week, Monday–Sunday | The default |
| `four_weeks` | 28 days from an anchor Monday | The admin sets the anchor in "Instellingen"; default anchor `2024-01-01` |
| `semi_month` | 1st–15th and 16th–end of month | — |
| `month` | Calendar month | — |

Admins set the org's period kind in "Instellingen" → "Organisatie en verloning". The API accepts **any** `startsOn`/`endsOn` range for exports and locks, so you can export a month in one call. The dashboard's overview, export and lock screens still work per week (known gap).

## Two freezing steps

| Step | Endpoint | Who | Effect | Event |
|---|---|---|---|---|
| **Export commit** | `POST /exports {"mode":"commit"}` | write key / office | Approved days in range → `exported`. Approved expenses → `exported`. Approved leave days are recorded as exported (each leave day is exported once). An immutable export run is stored | `export.completed` |
| **Period lock** | `POST /periods/lock {startsOn, endsOn, kind}` | **admin key or admin user** | Exported days in range → `locked`. The period is recorded as locked | `period.locked` |

Both are irreversible. Typically you commit when the file goes to payroll and lock after payroll has processed it.

### Commit rules

- Only `approved`, conflict-free days are included. The preview's `pendingCount` tells you how many days in the range are still `draft` or `submitted`.
- A commit fails with **422** when nothing is approved (`"Nothing approved to export in this period"`) or when the mapping reports validation issues (`"Export has N validation issue(s)"`).
- Committing the same range twice within one minute returns **409** (`"This period was just exported; refresh before exporting again"`).
- Days exported in an earlier run are not exported again. A later commit over the same range only picks up days approved since.

### Lock preconditions

`POST /periods/lock` refuses with **409** while:

- any day in the range with minutes is still `draft`, `submitted` or `approved` (captured: `{"code":"conflict","message":"1 day(s) in this period are not exported yet"}`), or
- any expense in the range is `submitted` or `approved` (`"N expense(s) in this period are not exported or rejected yet"`).

Success returns `{ "data": { "days": 7 } }`, the number of calendar days in the range.

Write keys get **403** `{"code":"forbidden","message":"Admin role required"}` on this endpoint. Use an API key with the `admin` scope, an admin user access token, or the dashboard ("Export" → "Periode vergrendelen"). `tt lock` and the MCP tool `lock_period` work when they run with an admin key.

## Corrections after export or lock

Exported and locked days are immutable. A claim on such a day is refused with **423** `{"code":"locked","message":"This day is exported or locked; book a correction in an open period"}` and nothing is stored.

To correct:

1. Record the difference as new hours on a date in an **open** period. For example, a `manual_office` claim on a day in the current week with `note: "Correction for 2026-12-01: +30 min"`. A separate correction placement also works if your payroll needs it.
2. Approve and export it in the next run. The payroll system processes the difference.
3. For negative corrections, agree a procedure with your payroll provider. Pay lines are never negative.

## Reading what was exported

- `GET /exports/runs` lists committed runs: id, range, row count, content hash, created at. It is paginated.
- `GET /exports/runs/{id}` returns the mapping snapshot, `pay_lines` and `warnings`.
- `GET /exports/runs/{id}/file` returns the exact file as committed (`text/csv`).
- `GET /days?from=…&to=…&status=exported,locked` shows which days are frozen, with `export_run_id`.
