A timesheet covers one consultant over one period. Each timesheet holds one or more lines, one per assignment, and each line records a status for every day in the period. The record moves through a fixed lifecycle: draft, submitted, then validated or rejected.
Lifecycle
draft --submit--> submitted --validate--> validated
^ |
+------reject---------+ (rejected is editable again, like draft)
draftandrejectedare editable.submittedis locked pending review.validatedis final.
Who does what
| Action | Role |
|---|---|
| Create a timesheet, add a line, delete a draft | Admin |
| Mark days, edit a line comment | The owning consultant or an admin, while draft/rejected |
| Submit | The owning consultant, from draft/rejected |
| Validate or reject | Admin (or the consultant’s team leader) |
1. Create the timesheet
An admin creates the sheet for a consultant and a period. The period dates follow the account’s cadence (see Conventions).
POST /timesheets HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{
"consultant_id": "0190a1c2-...",
"period_start": "2026-01-01",
"period_end": "2026-01-31"
}
Returns 201 Created with a TimesheetDetailResponse - the timesheet plus its lines. A sheet already covering that consultant and period returns 409 Conflict.
2. Add a line per assignment
Each line ties the timesheet to one assignment (a consultant on a mission). See Run a mission to create assignments.
POST /timesheets/{timesheet_id}/lines HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{ "assignment_id": "0190a1c2-..." }
Returns 201 Created. The assignment must belong to the same consultant; otherwise you get 422.
3. Mark days
For each day the consultant worked, set a status on the line. status is worked, half_day, or not_worked.
PUT /timesheets/{timesheet_id}/lines/{line_id}/day HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{ "date": "2026-01-05", "status": "worked" }
Returns 200 OK with the updated line. A date outside the period returns 422. The line’s work_day_count is derived from its days (a worked day counts 1, half_day counts 0.5).
Optionally add a comment to the line:
PUT /timesheets/{timesheet_id}/lines HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{ "line_id": "0190a1c2-...", "comment": "Client onboarding sprint" }
4. Submit
The consultant submits the finished sheet. It becomes read-only.
POST /timesheets/{timesheet_id}/submit HTTP/1.1
Authorization: Bearer <access_token>
Returns 200 OK with the TimesheetResponse (status now submitted). Submitting a sheet that is not draft/rejected returns 409.
5. Validate or reject
An admin or the consultant’s team leader decides.
POST /timesheets/{timesheet_id}/validate HTTP/1.1
Authorization: Bearer <access_token>
POST /timesheets/{timesheet_id}/reject HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{ "reason": "Missing the Friday entry" }
Validate moves the sheet to validated (final). Reject moves it to rejected with a reason, and it becomes editable again so the consultant can fix and resubmit.
Reading timesheets
GET /timesheets lists them, paged and filterable by consultant_id, status, period_start/period_end, search, and scope (mine or team). GET /timesheets/{id} returns the full detail with lines and days.
A line looks like this (note billing_rate is null for consultant-role callers):
{
"id": "0190a1c2-...",
"timesheet_id": "0190a1c2-...",
"assignment_id": "0190a1c2-...",
"mission_name": "Apollo",
"billing_rate": "900.00",
"work_day_count": "21",
"comment": "Client onboarding sprint",
"days": [
{ "date": "2026-01-05", "status": "worked" },
{ "date": "2026-01-06", "status": "half_day" }
]
}
Endpoints at a glance
| Action | Method | Path | Success |
|---|---|---|---|
| List | GET | /timesheets | 200 |
| Get detail | GET | /timesheets/{id} | 200 |
| Create | POST | /timesheets | 201 |
| Add line | POST | /timesheets/{id}/lines | 201 |
| Set a day | PUT | /timesheets/{id}/lines/{line_id}/day | 200 |
| Edit line | PUT | /timesheets/{id}/lines | 200 |
| Submit | POST | /timesheets/{id}/submit | 200 |
| Validate | POST | /timesheets/{id}/validate | 200 |
| Reject | POST | /timesheets/{id}/reject | 200 |
| Delete draft | DELETE | /timesheets/{id} | 204 |