# GET /api/v2/projects

List projects

- MCP id: projects.index
- Scope: api:read:projects
- Permission: projects::read
- Rate cost: 1 unit, read bucket. `view=detail` makes it 3. Adds: each include +1 (to-many +3), `q` +2.
- Cache-Control: private, no-store

Returns the projects the caller may read, with their customer, sub-customer, project manager and account manager inlined.

The default sort is title A→Z, not recent activity; pass `sort=-last_activity` for that. Defaults to the summary view; pass `view=detail` for the full payload at three read units instead of one. Pass `include=logged_minutes` for the worklog total the caller may see.

## 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.
- page (integer 1 to 1000, optional): Opt in to page-number paging: 1-based, at most 1000. Omit it, the default, to page by opaque cursor, which is what links.next hands you. Cannot be combined with cursor. Published only on collections that count their rows on every request.
- filter[account_manager_id] (integer, optional): Responsible account manager. Accepts the literal `me`. Operators: = in null.
- filter[application_id] (integer, optional): Linked application, managed by platform administrators. 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[customer.code] (string, optional): Short code of the owning customer, e.g. ACM.
- filter[customer_id] (integer, optional): Owning customer. Operators: = in null.
- filter[end_date] (date (YYYY-MM-DD), optional): Last day of the project. Operators: = gte lte null.
- filter[is_billable] (boolean, optional): Whether time is billable.
- filter[is_continuous] (boolean, optional): Whether the project is open-ended.
- filter[last_activity] (timestamp (YYYY-MM-DDThh:mm:ssZ), optional): Most recent write anywhere in the project. Operators: = gte lt null.
- filter[project_manager_id] (integer, optional): Responsible project manager. Accepts the literal `me`. Operators: = in null.
- filter[start_date] (date (YYYY-MM-DD), optional): First day of the project. Operators: = gte lte null.
- filter[status] (string, optional): Lifecycle status of the project. Operators: = ne in.
- filter[sub_customer_id] (integer, optional): Sub-customer. Operators: = in null.
- filter[tags.title] (string, optional): Projects carrying this exact tag title.
- filter[updated_at] (timestamp (YYYY-MM-DDThh:mm:ssZ), optional): Last-write timestamp. Use for incremental sync. Operators: = gte lt.
- filter[workflow_id] (integer, optional): Workflow the project's work items move through. Operators: = in null.
- filter[workflow_mode] (string, optional): Sprints or a simple board.
- q (string, optional): Free-text search over `title`. 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.
- trashed (string, optional): Whether soft-deleted rows are included.
- 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.
- title (string ≤ 255, required): Project title as shown in the UI.
- full_title (string, required): Application name plus title. Never carries the project number, even when the organisation shows numbers in plnnrly.
- description (html or null, required, detail only): Free-text description. HTML from the rich editor, sanitised.
- status (string, required): Lifecycle status. Setting it to invoiced finishes every task on every budget of the project.
- workflow_mode (string or null, required): Whether work is planned in sprints or on a simple board.
- start_date (date (YYYY-MM-DD) or null, required): First day of the project.
- end_date (date (YYYY-MM-DD) or null, required): Last day, `null` if open-ended.
- is_billable (boolean, required): Whether time on this project is billable.
- is_continuous (boolean, required): Whether the project has no planned end.
- last_activity (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required): Most recent write anywhere in the project.
- 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.
- deleted_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required): Soft-delete timestamp, `null` while live.
- customer_id (integer, required): Owning customer.
- sub_customer_id (integer or null, required): Optional sub-customer.
- project_manager_id (integer or null, required): Responsible project manager.
- account_manager_id (integer or null, required, server-assigned): Responsible account manager. Set to the creating user and not writable.
- application_id (integer or null, required): Linked application. Writable only while the organisation has the Infrastructure feature: sending a non-`null` value without it is a `422` `prohibited`. Null clears the link regardless.
- workflow_id (integer or null, required): Workflow whose statuses this project's work items move through.
- web_url (string, required): Absolute URL that opens this project's work in plnnrly, on the organisation's own hostname. It is a stable link, safe to store: it redirects to the first of the project's backlog, all-work list or board the signed-in user may open, and to the project page when none of them applies. Read-only.
- customer (Ref or CustomerSummary, required): Owning customer.
- sub_customer (Ref or CustomerSummary or null, required): Optional sub-customer.
- project_manager (Ref or UserSummary or null, required): Responsible project manager.
- account_manager (Ref or UserSummary or null, required): Responsible account manager.
- sprints_count (integer ≥ 0, required): Number of sprints.
- budgets_count (integer ≥ 0, required): Number of budgets.
- tags_count (integer ≥ 0, required): Number of tags.
- tasks_count (integer ≥ 0, required): Number of kanban tasks on this project that the caller may see.
- workflow_mode_options (array of string, required, detail only): Workflow modes this project may switch to. Empty until a workflow is linked.
- links (ProjectLinks, required)
  - self (url, required): The canonical URI of this record.
  - tasks (url, required): Kanban tasks on this project.
  - budgets (url, required): Budgets on this project.
  - sprints (url, required): Sprints on this project.
- tags (array of TagSummary ≤ 50, optional, only with include): Tags on this project. A project takes at most 50 tags, so the include lists them all. `tag_ids` replaces the whole set. Wrapped in `{ data, meta }`; `meta.truncated` says whether the cap was hit and `meta.count` is the full count.
  - id (integer, required): Unique identifier.
  - title (string ≤ 255, required): Tag title as shown in the UI.
- logged_minutes (integer or null, optional, on request): Sum of the worklog minutes on this project that the caller may see. Worklogs are per-user unless the caller holds `times::update_others` or `times::delete_others`. Null when the caller lacks `hours::read` or a token covering `api:read:times`.

## 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/projects' \
  --data-urlencode 'filter[status]=active' \
  --data-urlencode 'filter[customer_id]=1' \
  --data-urlencode 'sort=-start_date' \
  -H 'Authorization: Bearer <token>' \
  -H 'Accept: application/json'
```
