Timesheets
Upload the hours people actually worked, as dated shifts. Shifts live on the
employee, so they attach to every payslip whose pay period covers them —
including a fortnightly or monthly payslip, which is priced against each
calendar week's hours separately. Approved rows are evidence and can produce a
shortfall in dollars; unapproved rows are estimates and only ever flag.
POST /timesheets — upload a file
Multipart form upload. Same file the web import accepts (.csv or .xlsx, up
to 5 MB, one row per shift).
curl -X POST https://faircheck.com.au/api/v1/timesheets \
-H "Authorization: Bearer $TOKEN" \
-F "file=@timesheet.csv"
Add -F dry_run=1 to match rows without writing anything.
Columns
Header row required; column order does not matter; names are case-insensitive.
| Column | Required | Format |
|---|---|---|
employee_ref |
one of ref/name | The payroll id printed on the payslip. Matched first. |
employee_name |
one of ref/name | Used when there is no ref. Two people of one name are never guessed between. |
work_date |
yes | YYYY-MM-DD or DD/MM/YYYY |
start |
yes | HH:MM, 24-hour |
finish |
yes | HH:MM; a finish at or before the start reads as overnight |
break_minutes |
no | Unpaid break minutes. Blank means none. |
approved |
no | yes/no. yes marks the hours as actually worked (evidence). |
public_holiday |
no | yes/no |
The template is available at GET /payslip-compliance/timesheets/template.csv
when signed in.
Matching
People are matched exactly as payslips are: by payroll ref, then by name. A
person not on file is reported as unmatched and their rows are not written —
create them through the web import (which asks which employer to file them
under) or the employee screen, then upload again. An ambiguous match (two
people share the name and the file carries no ref) is reported and never
resolved by guessing.
Re-uploading rows for dates already on file replaces those dates' shifts, so
the upload is safe to repeat.
Response
201 Created (or 200 OK for a dry run):
{
"data": {
"dry_run": false,
"rows": 12,
"shifts_written": 10,
"shifts_replaced": 0,
"people": [
{
"employee_ref": "EMP-042",
"employee_name": "Jane Citizen",
"status": "matched",
"employee_id": 17,
"rows": 10,
"hours": 76.0,
"errors": []
},
{
"employee_ref": null,
"employee_name": "N Brown",
"status": "ambiguous",
"employee_id": null,
"rows": 2,
"hours": 15.0,
"errors": []
}
]
},
"errors": null,
"meta": {"matched": 1, "unmatched": 0, "ambiguous": 1, "invalid": 0}
}
status is one of matched, unmatched, ambiguous, invalid. Row-level
problems (a bad time, an overlapping shift) are listed under errors with the
file line number; a person with any invalid row has none of their rows written.
| Status | Meaning |
|---|---|
201 |
Written (or 200 for a dry run) |
413 |
File larger than 5 MB |
422 |
Not a readable timesheet (wrong type, or a required column missing), or no file |
How the hours are used
Each payslip's check reads the shifts covering its pay period from the employee
record (Rosters::PeriodShifts), splits them into ordinary, overtime, weekend
and public-holiday hours per calendar week, and prices them against the award.
Hours supplied directly on a payslip (hours on POST /payslips) still take
precedence for that payslip. See Payslips.