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.

Status lifecycle

How a timesheet day moves from draft to submitted, approved, exported and locked; who may make each transition; and how client approval modes and corrections interact.

Last updated: · Markdown version

Every day (one placement on one date) has a status. The same state machine runs in the web app, the mobile app, the API and database triggers.

            employee/office          office · client · admin          system (export commit)     admin/system (lock)
  draft ───────────────────► submitted ───────────────────► approved ─────────────────────► exported ───────────────► locked
    ▲                           │  ▲                            │
    └─── employee/office ───────┘  └──── office/admin ──────────┘
         (office needs a reason)        (reason required)

Transitions

From → To Who API Notes
draft → submitted employee, office, admin POST /weeks/submit {weekStart, personId} Submits every draft day of the ISO week that has minutes or a conflict. weekStart is normalised to the Monday. Returns {submitted: n}. Emits timesheet.submitted.
submitted → draft employee, office (with a reason), admin dashboard (reject) Sends hours back so the employee can edit them.
submitted → approved office, client, admin POST /days/approve {dayIds} or the client link Only conflict-free submitted days. See approval modes below. Emits timesheet.approved (office) and timesheet.client_approved (client).
approved → submitted office, admin (reason required) dashboard; also automatic when the value changes —
approved → exported system POST /exports {"mode":"commit"} Days get export_run_id. Emits export.completed.
exported → locked admin, system POST /periods/lock (admin key or admin user) Emits period.locked.

A disallowed transition returns 409 with code invalid_transition, forbidden or reason_required.

Client approval modes

Each client has an approval_mode:

Mode Office approves Client approves (magic link) Day becomes approved when
office required optional the office approves
client optional required the client approves (office approval only records approved_by_office_at)
both required required both have approved, in either order

POST /days/approve reports the split. This response was captured on a placement whose client has approval_mode: client:

{ "data": { "approved": 0, "waitingForClient": 1, "skipped": 0 } }

skipped counts days that were not submitted or that have a conflict.

Immutability: exported and locked days never change

exported and locked are final. Any write that would change such a day is refused:

{ "error": { "code": "locked", "message": "This day is exported or locked; book a correction in an open period" } }

That response has HTTP status 423. Corrections for an exported day are recorded as new hours in an open period. Example: an extra manual_office claim on a day in the current week, with a note that refers to the original date. The payroll system then processes the difference. See periods and locking.

The check runs before anything is stored, so a refused claim leaves no trace in GET /claims.

Changes invalidate approvals

When a new claim changes a day's value (minutes, start or end) or creates a conflict:

  • an approved day goes back to submitted, and
  • both office and client approvals are cleared, so they must be given again for the new value.

This keeps the client's signature tied to the hours they actually saw.

Employees vs office

  • Employees can change only draft days of their own placements, using sources app or kiosk. After submitting they get 409: "These hours are already submitted; ask the office to change them".
  • Office credentials (including write-scope API keys) can add claims at any status except exported/locked.

Reading statuses

  • Week view: GET /org/week gives a status per day cell and per row (draft, submitted, approved, exported, locked, mixed, or empty when nothing was recorded).
  • Any range: GET /days?from=…&to=…&status=submitted,approved. See pagination.
  • Events: timesheet.submitted, timesheet.approved, timesheet.client_approved, export.completed, period.locked. See events.