# GET /api/v2/sprints

List sprints

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

Returns the organisation's sprints in each project's display order, with the id as the tiebreaker.

The list covers the whole organisation and interleaves projects, because `order` is unique within a project only; narrow it with `filter[project_id]`. A sprint's state is derived rather than stored, so there is no `filter[state]`; compose it instead: `filter[is_active]=true&filter[completed_at][null]=true` is the running sprint, `filter[is_active]=false&filter[completed_at][null]=true` the upcoming ones, and `filter[completed_at][null]=false` the history. Listing additionally requires `project_management::manage_sprints`.

## 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. This collection's default sort is a display position that changes whenever someone drags the list into a new order in plnnrly, so a page number can skip or repeat a row if the list is reordered between two page requests. These collections rarely exceed one page; for a stable walk, send sort=id and follow links.next without page.
- filter[completed_at] (timestamp (YYYY-MM-DDThh:mm:ssZ), optional): When the sprint was actually completed. `filter[completed_at][null]=false` is every completed sprint and `filter[completed_at][null]=true` every sprint still open. Operators: = gte lte 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[end_date] (date (YYYY-MM-DD), optional): Planned last day of the sprint. Operators: = gte lte null.
- filter[is_active] (boolean, optional): Whether the sprint is the project's running one. Combine with `filter[completed_at][null]` to express a sprint state; this filter alone does not distinguish upcoming from completed.
- filter[project_id] (integer, optional): Narrow the list to one project's sprints, or to several with `in`. Operators: = in.
- filter[start_date] (date (YYYY-MM-DD), optional): Planned first day of the sprint. Operators: = gte lte null.
- filter[started_at] (timestamp (YYYY-MM-DDThh:mm:ssZ), optional): When the sprint was actually started. `filter[started_at][null]=true` is every sprint that has never run. Operators: = gte lte null.
- filter[updated_at] (timestamp (YYYY-MM-DDThh:mm:ssZ), optional): Last-write timestamp. Use for incremental sync. Operators: = gte lt.
- q (string, optional): Free-text search over `name`. 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.
- project_id (integer, required): The project this sprint belongs to. A sprint cannot be moved between projects, so this is accepted on create only.
- name (string ≤ 255, required): Sprint name as shown in plnnrly, e.g. `Sprint 14`.
- description (string or null, required, multiline): Free-text sprint goal. Plain text, not markup.
- start_date (date (YYYY-MM-DD) or null, required): Planned first day of the sprint. Independent of `started_at`: planning a date does not start the sprint.
- end_date (date (YYYY-MM-DD) or null, required): Planned last day of the sprint. Independent of `completed_at`.
- started_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required, server-assigned): When the sprint was actually started. Written only by POST `sprints/{sprint}/actions/start`; `null` on an upcoming sprint.
- completed_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required, server-assigned): When the sprint was actually completed. Written only by POST `sprints/{sprint}/actions/complete`; `null` until then. Non-`null` is what makes the derived state `completed`.
- is_active (boolean, required, server-assigned): Whether this is the project's running sprint. Set by actions/start and cleared by actions/complete, never by PATCH. At most one sprint per project is active at a time.
- order (integer, required, server-assigned): Display position within the project. Assigned on create as the last position in that project's sprint order, changed only by reordering in plnnrly, and not writable.
- work_items_count (integer ≥ 0, required): Number of work items currently on this sprint, resolved and unresolved alike.
- 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.
- project (Ref or ProjectSummary, required): The project this sprint belongs to. Expanding it additionally requires the projects prefix's read grant and a token scope covering it.
- links (SprintLinks, required)
  - self (url, required): The canonical URI of this record.
  - work_items (url, required): Work items on this sprint.

## 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/sprints' \
  --data-urlencode 'filter[project_id]=1' \
  --data-urlencode 'filter[is_active]=false' \
  --data-urlencode 'filter[completed_at][null]=true' \
  --data-urlencode 'sort=order' \
  -H 'Authorization: Bearer <token>' \
  -H 'Accept: application/json'
```
