Consultants declare out-of-pocket expenses; operations settle them. An expense has a category, a gross amount, an optional VAT amount, and a link to the mission it belongs to (which decides whether it is billable to the client). It moves through pending, approved, paid, rejected, or cancelled.
Who does what
| Action | Role |
|---|---|
| Create | The consultant (own), an admin, or a team leader for their team |
| Update or cancel | The owner or an admin, while still pending (cancel also from approved) |
| Approve or reject | Admin or the consultant’s team leader |
| Pay | Admin or operations |
1. Create an expense
POST /expenses HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{
"date": "2026-01-14",
"category": "train",
"description": "Client site visit",
"amount": "120.00",
"vat_amount": "24.00",
"mission_id": "0190a1c2-...",
"billable": true
}
Returns 201 Created with an ExpenseResponse. Key points:
amountis the gross figure;net_amountis derived and returned for you.categoryis one oftrain,taxi,fuel,tolls,meal,hotel,office_supplies,software,telephony,other.billablemarks the expense for re-invoicing to the client; setmission_idto attach it to a mission.receiptis an optional data URL (data:<mime>;base64,<data>); the response exposes ahas_receiptboolean, and the full receipt only onGET /expenses/{id}.
{
"id": "0190a1c2-...",
"consultant_id": "0190a1c2-...",
"consultant_full_name": "Ada Lovelace",
"date": "2026-01-14",
"category": "train",
"description": "Client site visit",
"amount": "120.00",
"vat_amount": "24.00",
"net_amount": "96.00",
"mission_id": "0190a1c2-...",
"mission_name": "Apollo",
"billable": true,
"has_receipt": false,
"status": "pending",
"created_at": "2026-01-14T12:00:00Z"
}
A consultant-role token with no linked profile cannot create an expense (422).
2. Edit or cancel before settlement
While pending, the owner or an admin can update the expense with PUT /expenses/{id} (same fields as create). Cancelling withdraws it:
POST /expenses/{id}/cancel HTTP/1.1
Authorization: Bearer <access_token>
Cancel is allowed from pending or approved, but not once paid (409 otherwise).
3. Approve or reject
A manager reviews a pending expense:
POST /expenses/{id}/approve HTTP/1.1
Authorization: Bearer <access_token>
POST /expenses/{id}/reject HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{ "reason": "No receipt attached" }
Both return 200 OK. Acting on an expense that is not pending returns 409.
4. Pay
Operations settles an approved expense:
POST /expenses/{id}/pay HTTP/1.1
Authorization: Bearer <access_token>
Returns 200 OK with status now paid. Paying an expense that is not approved returns 409.
The monthly summary
GET /expenses/summary rolls totals up by status, optionally for one month and one consultant:
GET /expenses/summary?month=2026-01 HTTP/1.1
Authorization: Bearer <access_token>
{
"date_from": "2026-01-01",
"date_to": "2026-01-31",
"statuses": [
{ "status": "pending", "count": 3, "total": "420.00" },
{ "status": "paid", "count": 12, "total": "1850.00" }
]
}
Reading expenses
GET /expenses is paged and filterable by consultant_id, status, category, mission_id, date_from/date_to, search, and scope (mine/team). See Conventions.
Endpoints at a glance
| Action | Method | Path | Success |
|---|---|---|---|
| List | GET | /expenses | 200 |
| Summary | GET | /expenses/summary | 200 |
| Get | GET | /expenses/{id} | 200 |
| Create | POST | /expenses | 201 |
| Update | PUT | /expenses/{id} | 200 |
| Cancel | POST | /expenses/{id}/cancel | 200 |
| Approve | POST | /expenses/{id}/approve | 200 |
| Reject | POST | /expenses/{id}/reject | 200 |
| Pay | POST | /expenses/{id}/pay | 200 |