Time entries
Tracked time. Reads require time-entries:read, writes time-entries:write.
Changed: time entries used to ride the
projects:readability. They now have their own scope, so a token issued with onlyprojects:readno longer reads them. Reissue the token withtime-entries:read(andtime-entries:writeif it needs to log hours).
Feature-gated
Like projects, time entries are gated by the per-book projects_enabled toggle — a book with the feature off returns a blank 404, before validation runs.
Operations
| Method & path | Ability |
|---|---|
GET /api/v1/time-entries |
time-entries:read |
GET /api/v1/time-entries/{timeEntry} |
time-entries:read |
POST /api/v1/time-entries |
time-entries:write |
PATCH /api/v1/time-entries/{timeEntry} |
time-entries:write |
DELETE /api/v1/time-entries/{timeEntry} |
time-entries:write |
Get a time entry
curl https://billey.nl/api/v1/time-entries/3001 \
-H "Authorization: Bearer $TOKEN"
{
"data": {
"id": 3001,
"project_id": 55,
"invoice_id": null,
"is_invoiced": false,
"date": "2026-07-21",
"hours": "7.75",
"description": "Layout work"
}
}
is_invoiced is derived — it tells you whether the entry has already been billed. Uninvoiced entries can be claimed onto a new invoice by passing their ids as time_entry_ids when you create an invoice.
Entries belonging to a deleted project are not returned, even when the project itself was retained because it had billed hours.
Log a time entry
project_id, date, hours and description are all required. As a create it takes an Idempotency-Key.
curl -X POST https://billey.nl/api/v1/time-entries \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "project_id": 55, "date": "2026-07-21", "hours": "7.75", "description": "Layout work" }'
project_id must belong to your book, and must not be a project you have deleted.
hours is dot-decimal only, as everywhere else in this API — see Money and decimals. The minimum is 0.01. description is capped at 255 characters.
invoice_id is not accepted, here or on PATCH. Hours become billed by creating an invoice from the project, which claims them as a side effect. Letting a caller set invoice_id directly would mark hours as billed that no invoice line actually covers.
The idempotency key is scoped to the project
Everywhere else in this API an Idempotency-Key is unique per book. For time entries it is unique per project, because a time entry belongs to its project rather than directly to the book. So:
- Reusing a key on the same project replays the original entry and returns
200. - Reusing the same key on a different project creates a second entry.
Generate a fresh key per request — uuidgen above — and this never comes up.
Update a time entry
PATCH is a partial merge — only the keys you send are changed, and every field is optional.
curl -X PATCH https://billey.nl/api/v1/time-entries/3001 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "hours": "8.25" }'
project_id may be changed to move the entry to another project in the same book.
Refused with 409 while the entry is invoiced. Billed hours are frozen so they cannot drift out of agreement with the invoice line they were billed on — an entry silently edited from 8 hours to 2 after billing would leave the invoice saying one thing and your records another. To correct work that has already been billed, credit the invoice and re-invoice.
The freeze lifts if the claiming invoice is deleted. Only a draft invoice can be deleted, so its hours were never sent to anyone and are legitimately editable again.
Delete a time entry
DELETE permanently deletes the entry and returns 204 — time entries are not soft-deleted, and there is no include_deleted view to recover one from.
Refused with 409 once the entry has been invoiced. The per-day breakdown behind a billed invoice line exists only on these entries: invoicing groups hours by description and writes the summed total onto the line item, so the individual days are not recorded anywhere else. It is also the record a deleted project's invoices depend on to name the project they billed — see projects.
See the OpenAPI spec for the full field list.