Time entries
Log and adjust manual time on tasks and subtasks.
A time entry records work time against a task or subtask. Manual entries are posted via this API; entries produced by the in-app timer are server-generated and not creatable via REST.
Endpoints
| Method | Path | Notes |
|---|---|---|
GET | /api/v1/time-entries | List with filters. Cursor pagination. |
POST | /api/v1/time-entries | Log a manual entry. Accepts Idempotency-Key. |
GET | /api/v1/time-entries/{id} | Single entry with user, task, subtask. |
PATCH | /api/v1/time-entries/{id} | Edit description, start time, duration. |
DELETE | /api/v1/time-entries/{id} | Delete the entry. |
The in-app timer endpoints (start, stop, switch) are intentionally not exposed via REST in v1.
TimeEntry model
{
"id": "e1…",
"organizationId": "...",
"userId": "u1…",
"taskId": "t1…",
"subtaskId": null,
"description": "Initial wiring",
"startedAt": "2026-05-19T13:00:00.000Z",
"endedAt": "2026-05-19T14:30:00.000Z",
"durationSeconds": 5400,
"source": "manual",
"autoStopped": false,
"createdAt": "...",
"updatedAt": "..."
}| Field | Notes |
|---|---|
source | manual (this API) or timer (in-app start/stop). |
autoStopped | true when the 8h-cap sweeper persisted a timer-driven entry. |
endedAt | Always startedAt + durationSeconds — recomputed by the server on update. |
Each entry belongs to exactly one of taskId or subtaskId (XOR). The parent is immutable after creation.
Listing entries
GET /api/v1/time-entries?userId=...&startDate=2026-05-01&endDate=2026-05-31| Param | Type | Notes |
|---|---|---|
userId | UUID | One member's entries. |
taskId | UUID | Direct task entries. |
subtaskId | UUID | Subtask entries. |
projectId | UUID | Entries on tasks/subtasks of one project. |
startDate | ISO datetime | Inclusive lower bound on startedAt. |
endDate | ISO datetime | Inclusive upper bound on startedAt. |
cursor | UUID | From a previous response. |
limit | int 1–100 | Default 50. |
Returns { data, pagination: { nextCursor } }. Entries are filtered by project visibility — standard members may not see entries on tasks they cannot access.
Logging a manual entry
POST /api/v1/time-entries
Content-Type: application/json
Idempotency-Key: ...
{
"taskId": "t1…",
"startedAt": "2026-05-19T13:00:00.000Z",
"durationSeconds": 5400,
"description": "Initial wiring"
}Required:
| Field | Type | Constraint |
|---|---|---|
Exactly one of taskId or subtaskId | UUID | Sending both, or neither, returns 422. |
startedAt | ISO datetime | — |
durationSeconds | int | 1 – 86 400 (≤ 24 h). |
Optional:
| Field | Type | Constraint |
|---|---|---|
description | string | ≤ 1000 chars. |
Response: 201 Created with the entry. The created entry is always attributed to the API key's user.
Editing an entry
PATCH /api/v1/time-entries/{id}
{ "durationSeconds": 7200 }Caller must be one of:
- The entry's author, or
- An organization owner / admin, or
- A project manager on the entry's parent project.
Allowed fields: description, startedAt, durationSeconds. Updating startedAt or durationSeconds recomputes endedAt.
Deleting an entry
DELETE /api/v1/time-entries/{id}Returns { "data": { "success": true } }. Same authorization rules as edit.
Common precondition failures
| Code | Cause |
|---|---|
422 | Body sent both taskId and subtaskId, or neither. |
403 | Caller can read the entry but cannot edit/delete it. |
404 | Cross-org id, or entry belongs to a task the caller cannot access. |