# GET /api/v2/worklogs

List worklogs

- MCP id: worklogs.index
- Scope: api:read:times
- Permission: hours::read
- Rate cost: 1 unit, read bucket. `view=detail` makes it 3. Adds: `with_total=1` +2, each include +1, `q` +2.
- Cache-Control: private, no-store

Returns booked hours, newest first by `date`.

A caller sees only their own entries unless they hold `times::update_others` or `times::delete_others`, and that narrowing is applied before any client filter, before pagination and before the count, so `filter[user_id]` widens nothing on its own. `meta.page.total` is `null` unless you ask for it with `with_total=1`, which adds 2 read units, because worklogs are usually an organisation's largest collection. `q` matches anywhere in `description`. `project` and `work_item` are on every row already, so naming either in `?include=` is a `400`; `budget`, `task` and `user` are the includable relations, and each additionally requires the read permission and token scope of its own prefix. There is no `duration` string and no `is_in_review` flag: format `minutes` yourself, and read review state as `(exceeded_budget || billable_change_reason) && reviewed_by == null && invoice_id == null`.

## Parameters
- view (string, optional): Which field set to render.
- per_page (integer 1 to 100, optional): Rows per page. At most 25 when `include` names a to-many relation; a larger value is 400 invalid-query-parameter.
- cursor (string, optional): Opaque keyset cursor from a previous page's links.next. Never construct one by hand.
- with_total (boolean, optional): Whether meta.page.total is counted for this request.
- filter[budget_id] (integer, optional): Budget the hours are invoiced against. Operators: = in null.
- filter[created_at] (timestamp (YYYY-MM-DDThh:mm:ssZ), optional): Creation timestamp. Pair with `sort=id` for a stable full export. Operators: = gte lt.
- filter[date] (date (YYYY-MM-DD), optional): The calendar day the work happened. Combine gte and lte for a range. Operators: = gte lte.
- filter[exceeded_budget] (boolean, optional): Whether the entry was booked knowing it exceeds its budget.
- filter[invoice_id] (integer, optional): The invoice these hours were billed on. Use the `null` operator, `filter[invoice_id][null]=true`, to find uninvoiced hours. Operators: = in null.
- filter[is_not_billable] (boolean, optional): Whether the hours are excluded from invoicing.
- filter[is_overtime] (boolean, optional): Whether the hours count as overtime.
- filter[project_id] (integer, optional): Project the hours are booked against. Operators: = in null.
- filter[reviewed_by] (integer, optional): The reviewer who settled the entry. Use `filter[reviewed_by][null]=true` to find entries nobody has reviewed. Operators: = in null.
- filter[sales_invoice_row_id] (integer, optional): The sales-invoice line these hours were billed on. Operators: = in null.
- filter[task_id] (integer, optional): Task the hours are booked on. Operators: = in null.
- filter[updated_at] (timestamp (YYYY-MM-DDThh:mm:ssZ), optional): Last-write timestamp. Use for incremental sync. Operators: = gte lt.
- filter[user_id] (integer, optional): Whose hours to return. Accepts the literal `me`, and defaults to the authenticated user. Naming another employee requires the `times::update_others` permission. Operators: = in null.
- filter[work_item_id] (integer, optional): Work item the hours are attached to. Operators: = in null.
- q (string, optional): Free-text search over `description`. At most one substring match per request: `q` together with a `contains` filter is 400 invalid-query-parameter.
- sort (string, optional): Up to three keys, comma-separated, each optionally prefixed with `-` for descending.
- include (string, optional): Comma-separated names to inline, at most five.
- X-Request-Id (header, optional): A correlation id of your own. It is recorded in our logs next to the server's request id but never returned: the response header always carries the server's id. A value that does not match the pattern is discarded silently, never rejected.
- If-None-Match (header, optional): The ETag a read is conditional on. A match is answered 304 with no body.

## Response 200
- id (integer, required): Unique identifier.
- date (date (YYYY-MM-DD), required): The calendar day the work happened.
- minutes (integer, required): Duration in minutes, between 1 and 1440.
- description (string ≤ 255, required): What the employee worked on.
- project_id (integer or null, required): Project the hours are booked against; `null` once that project is deleted.
- project (Ref or ProjectSummary or null, required): The project the hours are booked against, always expanded. Its `customer_id` is the customer to fetch with `GET /api/v2/customers/{customer}`; the customer itself is not nested.
- budget_id (integer or null, required, server-assigned): Budget the hours are invoiced against. Derived from the task, never accepted on a write; `null` when no task is set or the budget is deleted.
- budget (Ref or BudgetSummary or null, required): The budget the hours are invoiced against. Expanding it with `?include=budget` additionally requires the `budgets::read` permission and a token scope covering the budgets prefix.
- task_id (integer or null, required): Task the hours are booked on; `null` when the entry is not attached to one, or once that task is deleted.
- task (Ref or TaskSummary or null, required): The task the hours are booked on. Expanding it with `?include=task` additionally requires the `kanban::manage_self` permission and a token scope covering the kanban prefix, and is limited to the tasks you can see on the tasks collection.
- work_item_id (integer or null, required): Work item the hours are attached to; `null` when the entry is not attached to one, or once that work item is deleted.
- work_item (Ref or WorkItemSummary or null, required): The work item the hours are attached to, always expanded.
- user_id (integer or null, required, server-assigned): Employee the hours belong to. Assigned on create (from the authenticated user, or from `on_behalf_of_user_id`) and never writable afterwards; reassigning a worklog is a DELETE plus a POST.
- user (Ref or UserSummary or null, required): The employee the hours belong to. Expanding it with `?include=user` additionally requires the `users::read` permission and a token scope covering the users prefix.
- is_not_billable (boolean, required): Whether these hours are excluded from invoicing. Inferred from the task's `is_billable` flag (`false` without a task) on create and again whenever `task_id` is sent. Sending a value that differs from the inferred or current one requires `billable_change_reason` and puts the entry in a reviewer's queue; without it the write is a `422` on `/billable_change_reason`.
- is_overtime (boolean, required): Whether the hours count as overtime. Sending it without the `times::write_overtime` permission leaves it `false` and adds an `input_ignored` warning rather than failing the write.
- exceeded_budget (boolean, required): Whether the entry was booked knowing it exceeds its budget. Set on create only, and `true` whenever a `reason` is sent; setting it puts the entry in a reviewer's queue.
- reason (string or null, required, detail only, multiline): Why the budget overrun was accepted. Sending it on create marks the entry as exceeding its budget. Once a reviewer has settled the entry it can no longer be changed: a PATCH that does answers `409` `reason_reviewed`.
- billable_change_reason (string or null, required, detail only, multiline): Why the inferred billability was overridden. Required whenever `is_not_billable` is sent with a value that differs from the inferred or current one, on create and on PATCH alike; the override puts the entry in a reviewer's queue, reopening one a reviewer had already settled.
- reviewed_by (integer or null, required, server-assigned): The reviewer who settled the entry, once one has. Set by the review queue in plnnrly; this API exposes no way to write it.
- invoice_id (integer or null, required, server-assigned): The invoice these hours were billed on, once they have been. While it is set, the entry is locked: a write answers `409` with `conflict.reason` `locked_by_invoice`.
- sales_invoice_row_id (integer or null, required, server-assigned): The sales-invoice line these hours were billed on, once they have been. Set by the invoicing flow.
- created_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): Creation timestamp.
- updated_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): Last-write timestamp; use for incremental sync.
- links (TimeLinks, required)
  - self (url, required): The canonical URI of this record.

## Errors
- 403 forbidden-by-policy | insufficient-permission | insufficient-token-scope: Forbidden.
- 304 : Not modified.
- 400 idempotency-key-required | invalid-cursor | invalid-query-parameter | malformed-request | unsupported-parameter: Bad request.
- 401 token-expired | unauthenticated: Unauthenticated.
- 404 endpoint-not-found | resource-not-found: Not found.
- 406 not-acceptable: Not acceptable.
- 429 rate-limit-exceeded: Too many requests.
- 500 internal-error: Server error.
- 503 tenant-unavailable: Service unavailable.

## Example
```bash
curl -G \
  'https://<your-url>/api/v2/worklogs' \
  --data-urlencode 'filter[user_id]=me' \
  --data-urlencode 'filter[date][gte]=2026-09-07' \
  --data-urlencode 'filter[date][lte]=2026-09-13' \
  --data-urlencode 'include=task' \
  -H 'Authorization: Bearer <token>' \
  -H 'Accept: application/json'
```
