> For the complete documentation index, see [llms.txt](https://docs.currents.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.currents.dev/api/resources/projects.md).

# Projects

Manage and query projects in your organization.

Projects are the top-level containers for test runs, specs, and test results. Each project has a unique project ID and can be configured with various settings.

## List projects

> Get all projects for your organization with optional pagination

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Projects","description":"Manage and query projects in your organization.\n\nProjects are the top-level containers for test runs, specs, and test results.\nEach project has a unique project ID and can be configured with various settings."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["projects:read"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"LimitQuery":{"name":"limit","in":"query","required":false,"description":"Maximum number of items to return (default: 10, max: 50)","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}},"StartingAfterQuery":{"name":"starting_after","in":"query","required":false,"description":"Cursor for pagination. Returns items after this cursor value.","schema":{"type":"string"}},"EndingBeforeQuery":{"name":"ending_before","in":"query","required":false,"description":"Cursor for pagination. Returns items before this cursor value.","schema":{"type":"string"}}},"schemas":{"ProjectsListResponse":{"type":"object","required":["status","has_more","data"],"properties":{"status":{"type":"string","enum":["OK"]},"has_more":{"type":"boolean","description":"Whether there are more items to fetch"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ProjectWithCursor"}}}},"ProjectWithCursor":{"allOf":[{"$ref":"#/components/schemas/Project"},{"type":"object","properties":{"cursor":{"type":"string","description":"Cursor for pagination"}}}]},"Project":{"type":"object","required":["projectId","name","createdAt"],"properties":{"projectId":{"type":"string","description":"Unique identifier for the project"},"name":{"type":"string","description":"Human-readable name for the project"},"createdAt":{"type":"string","format":"date-time","description":"When the project was created"},"failFast":{"type":"boolean","nullable":true,"description":"Whether fail-fast is enabled for this project"},"inactivityTimeoutSeconds":{"type":"integer","nullable":true,"description":"Timeout in seconds after which inactive runs are marked as timed out"},"defaultBranchName":{"type":"string","nullable":true,"description":"Default branch name for the project"}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/projects":{"get":{"summary":"List projects","description":"Get all projects for your organization with optional pagination","operationId":"listProjects","tags":["Projects"],"parameters":[{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/StartingAfterQuery"},{"$ref":"#/components/parameters/EndingBeforeQuery"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectsListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}}}}
```

## Get project

> Get a single project by ID

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Projects","description":"Manage and query projects in your organization.\n\nProjects are the top-level containers for test runs, specs, and test results.\nEach project has a unique project ID and can be configured with various settings."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["projects:read"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"ProjectIdPath":{"name":"projectId","in":"path","required":true,"description":"The project ID","schema":{"type":"string"}}},"schemas":{"ProjectResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"$ref":"#/components/schemas/Project"}}},"Project":{"type":"object","required":["projectId","name","createdAt"],"properties":{"projectId":{"type":"string","description":"Unique identifier for the project"},"name":{"type":"string","description":"Human-readable name for the project"},"createdAt":{"type":"string","format":"date-time","description":"When the project was created"},"failFast":{"type":"boolean","nullable":true,"description":"Whether fail-fast is enabled for this project"},"inactivityTimeoutSeconds":{"type":"integer","nullable":true,"description":"Timeout in seconds after which inactive runs are marked as timed out"},"defaultBranchName":{"type":"string","nullable":true,"description":"Default branch name for the project"}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/projects/{projectId}":{"get":{"summary":"Get project","description":"Get a single project by ID","operationId":"getProject","tags":["Projects"],"parameters":[{"$ref":"#/components/parameters/ProjectIdPath"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## List project runs

> Get all runs for a project with optional pagination and filtering. Supports filtering by branch, tags, environment, status, completion state, date range, search text, author, and pull request id (pr\_id maps to meta.pr.id).

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Projects","description":"Manage and query projects in your organization.\n\nProjects are the top-level containers for test runs, specs, and test results.\nEach project has a unique project ID and can be configured with various settings."},{"name":"Runs","description":"Query and manage test runs.\n\nA run represents a single execution of your test suite. Runs contain spec files\nwhich are executed across multiple instances. You can query run details, cancel\nruns, reset failed specs, and delete runs.\n\nPer-test outcome counters on specs (`results.stats`) use the `RecordedTestOutcome` vocabulary—see **components/schemas** for how **pending** and **skipped** differ from **failures**."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["results:read"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"ProjectIdPath":{"name":"projectId","in":"path","required":true,"description":"The project ID","schema":{"type":"string"}},"LimitQuery":{"name":"limit","in":"query","required":false,"description":"Maximum number of items to return (default: 10, max: 50)","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}},"StartingAfterQuery":{"name":"starting_after","in":"query","required":false,"description":"Cursor for pagination. Returns items after this cursor value.","schema":{"type":"string"}},"EndingBeforeQuery":{"name":"ending_before","in":"query","required":false,"description":"Cursor for pagination. Returns items before this cursor value.","schema":{"type":"string"}},"TagsQuery":{"name":"tags[]","in":"query","required":false,"description":"Filter by tags (can be specified multiple times using bracket notation, e.g., tags[]=value1&tags[]=value2)","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"BranchesQuery":{"name":"branches[]","in":"query","required":false,"description":"Filter by git branch. Use trailing `*` for prefix match (e.g. `release/*`). Max 250 characters per value.","schema":{"type":"array","items":{"$ref":"#/components/schemas/BranchFilterQueryValue"}},"style":"form","explode":true},"AuthorsQuery":{"name":"authors[]","in":"query","required":false,"description":"Filter by git authors (can be specified multiple times using bracket notation, e.g., authors[]=author1&authors[]=author2)","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"EnvironmentsQuery":{"name":"environments[]","in":"query","required":false,"description":"Filter by environment (can be specified multiple times using bracket notation, e.g., environments[]=staging&environments[]=production)","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"RunFeedBranchQuery":{"name":"branch","in":"query","required":false,"deprecated":true,"description":"Deprecated. Use branches[] instead. Filter runs by git branch name (single branch). Supports a trailing `*` for prefix search (e.g., `release/*`). Branch value is limited to 250 characters (exact or wildcard).","schema":{"$ref":"#/components/schemas/BranchFilterQueryValue"}},"RunFeedTagQuery":{"name":"tag","in":"query","required":false,"deprecated":true,"description":"Deprecated. Use tags[] instead. Filter runs by tags (can be specified multiple times). Use tag_operator to control matching behavior.","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"TagOperatorQuery":{"name":"tag_operator","in":"query","required":false,"description":"Logical operator for tag filtering. AND requires all tags to be present (default), OR requires any tag to be present.","schema":{"type":"string","enum":["AND","OR"],"default":"AND"}},"RunFeedSearchQuery":{"name":"search","in":"query","required":false,"description":"Search runs by ciBuildId or commit message (case-insensitive). The raw value must be printable ASCII only (no control characters). Leading and trailing whitespace is trimmed before length checks; whitespace-only values are rejected. After trimming, the value must be 1–200 characters.","schema":{"type":"string","pattern":"^[ -~]+$"}},"RunFeedPrIdQuery":{"name":"pr_id","in":"query","required":false,"description":"Filter runs by normalized pull request id (stored as meta.pr.id). Printable ASCII only, max 128 characters.","schema":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[!-~]+$"}},"RunFeedAuthorQuery":{"name":"author","in":"query","required":false,"deprecated":true,"description":"Deprecated. Use authors[] instead. Filter runs by git commit author name (can be specified multiple times).","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"RunStatusQuery":{"name":"status","in":"query","required":false,"description":"Filter runs by status (can be specified multiple times). PASSED: all tests passed, FAILED: some tests failed, RUNNING: run is in progress and passing, FAILING: run is in progress but has failures.","schema":{"type":"array","items":{"type":"string","enum":["PASSED","FAILED","RUNNING","FAILING"]}},"style":"form","explode":true},"RunCompletionStateQuery":{"name":"completion_state","in":"query","required":false,"description":"Filter runs by completion state (can be specified multiple times). COMPLETE: run finished normally, IN_PROGRESS: run is still executing, CANCELED: run was canceled, TIMEOUT: run timed out.","schema":{"type":"array","items":{"type":"string","enum":["COMPLETE","IN_PROGRESS","CANCELED","TIMEOUT"]}},"style":"form","explode":true},"OptionalDateStartQuery":{"name":"date_start","in":"query","required":false,"description":"Filter runs created on or after this ISO 8601 date-time. A date without a time (`2024-01-01`) means midnight UTC of that day.","schema":{"type":"string"}},"OptionalDateEndQuery":{"name":"date_end","in":"query","required":false,"description":"Filter runs created before this ISO 8601 date-time. A date without a time (`2024-12-31`) means midnight UTC of that day, so the range ends before that day.","schema":{"type":"string"}}},"schemas":{"BranchFilterQueryValue":{"description":"Branch filter value: an exact branch name, or a trailing `*` for prefix match (e.g. `release/*`). Max 250 characters.","oneOf":[{"type":"string","description":"Exact branch name (must not contain `*` or spaces).","pattern":"^[!-)+-~]+$","maxLength":250},{"type":"string","description":"Prefix match: branch prefix followed by `*`.","pattern":"^[!-)+-~]+\\*$","maxLength":250}]},"RunsListResponse":{"type":"object","required":["status","has_more","data"],"properties":{"status":{"type":"string","enum":["OK"]},"has_more":{"type":"boolean","description":"Whether there are more items to fetch"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RunFeedItem"}}}},"RunFeedItem":{"allOf":[{"type":"object","properties":{"runId":{"type":"string"},"projectId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"durationMs":{"type":"integer","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"environments":{"type":"array","items":{"type":"string"},"nullable":true},"previousRunId":{"type":"string","nullable":true},"isSession":{"type":"boolean","description":"Recorded from a browser session rather than by a reporter in CI. Absent on runs recorded by a CI reporter."},"cursor":{"type":"string"},"timeout":{"type":"object","properties":{"isTimeout":{"type":"boolean"},"timeoutValueMs":{"type":"integer","nullable":true}}},"cancellation":{"type":"object","nullable":true},"groups":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"meta":{"$ref":"#/components/schemas/RunMeta"},"completionState":{"type":"string"},"status":{"type":"string"}}}]},"RunGroup":{"type":"object","properties":{"groupId":{"type":"string"},"platform":{"type":"object","properties":{"osName":{"type":"string"},"osVersion":{"type":"string"},"browserName":{"type":"string"},"browserVersion":{"type":"string"}}},"tags":{"type":"array","items":{"type":"string"}},"environment":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time","nullable":true},"coverageEnabled":{"type":"boolean"},"durationMs":{"type":"integer"},"specCount":{"type":"integer"},"instances":{"type":"object","properties":{"overall":{"type":"integer"},"claimed":{"type":"integer"},"complete":{"type":"integer"},"passes":{"type":"integer"},"failures":{"type":"integer"}}},"tests":{"type":"object","description":"Test counts for this group. Use `total` (scheduled suite size) and `recorded` (tests with a reported outcome so far). `tests` and `overall` are deprecated aliases.","properties":{"total":{"type":"integer","description":"Total tests scheduled/expected in this group, including tests still running or not yet started."},"recorded":{"type":"integer","description":"Tests in this group that have reported an outcome so far (passes + failures + pending + skipped). Less than or equal to `total` while the group is in progress."},"tests":{"type":"integer","deprecated":true,"description":"Deprecated alias of `total` (scheduled suite size). Use `total`."},"overall":{"type":"integer","deprecated":true,"description":"Deprecated alias of `recorded`. Despite the name this is NOT the suite total — use `recorded` for the reported-outcome count, or `total` for the suite size."},"passes":{"type":"integer"},"failures":{"type":"integer"},"pending":{"type":"integer","description":"**pending** outcomes (intentional skips)."},"retries":{"type":"integer"},"skipped":{"type":"integer","description":"**skipped** outcomes (separate from `failures` here; see schema for Tests Explorer difference)."},"flaky":{"type":"integer"}}}}},"RunMeta":{"type":"object","properties":{"ciBuildId":{"type":"string","description":"CI build identifier"},"pr":{"type":"object","nullable":true,"description":"Normalized pull request metadata when the run is associated with a PR","properties":{"id":{"type":"string","nullable":true},"link":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"source":{"type":"string","nullable":true},"destination":{"type":"string","nullable":true}}},"commit":{"type":"object","properties":{"sha":{"type":"string"},"branch":{"type":"string"},"authorName":{"type":"string"},"authorEmail":{"type":"string"},"message":{"type":"string"},"remoteOrigin":{"type":"string"}}},"framework":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"}}}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/projects/{projectId}/runs":{"get":{"summary":"List project runs","description":"Get all runs for a project with optional pagination and filtering. Supports filtering by branch, tags, environment, status, completion state, date range, search text, author, and pull request id (pr_id maps to meta.pr.id).","operationId":"listProjectRuns","tags":["Projects","Runs"],"parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/StartingAfterQuery"},{"$ref":"#/components/parameters/EndingBeforeQuery"},{"$ref":"#/components/parameters/TagsQuery"},{"$ref":"#/components/parameters/BranchesQuery"},{"$ref":"#/components/parameters/AuthorsQuery"},{"$ref":"#/components/parameters/EnvironmentsQuery"},{"$ref":"#/components/parameters/RunFeedBranchQuery"},{"$ref":"#/components/parameters/RunFeedTagQuery"},{"$ref":"#/components/parameters/TagOperatorQuery"},{"$ref":"#/components/parameters/RunFeedSearchQuery"},{"$ref":"#/components/parameters/RunFeedPrIdQuery"},{"$ref":"#/components/parameters/RunFeedAuthorQuery"},{"$ref":"#/components/parameters/RunStatusQuery"},{"$ref":"#/components/parameters/RunCompletionStateQuery"},{"$ref":"#/components/parameters/OptionalDateStartQuery"},{"$ref":"#/components/parameters/OptionalDateEndQuery"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunsListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## List pull requests

> Lists pull requests for this project together with summarized run activity. Each result includes rollup stats for the PR, its most recent run, and an optional preview of additional runs (\`runs\_per\_pr\`: 0 to 10, default 1). Each result also has \`recentRuns\`: up to 10 of its latest runs, newest first, whatever \`runs\_per\_pr\` is, each with its id, creation time, status, completion state, duration, commit and number of failed tests. Send \`include\_total=true\` to also get \`total\`, the number of pull requests that match all filters over all pages. Optional filters choose which runs count: branches\[], tags\[], authors\[], environments\[], \`pr\_id\`, a \`date\_start\`/\`date\_end\` range on run creation time (no range by default), or \`search\` (case-insensitive substring match on ciBuildId or commit message); pull requests with no runs matching the applied filters are omitted. \`status\`, \`completion\_state\` and \`pr\_search\` (title, number or source branch) are checked on each pull request's latest run among the runs that match the other filters. For each remaining pull request, the status shown corresponds to its latest run that matches your filters. Branch values may end with \`\*\` for a prefix match. Results are sorted by \`order\` (\`last\_run\`, \`first\_run\` or \`run\_count\`; default \`last\_run\`) in the direction set by \`dir\` (default \`desc\`). Pagination uses opaque tokens: advance with \`starting\_after\`, go back with \`ending\_before\`; use the returned \`cursor\` from each item as the token and keep the same filters, \`order\` and \`dir\` across pages. A cursor returned for another \`order\` or \`dir\` is rejected with 400. Sending both cursor parameters at once is not supported. Because new runs can arrive while you page, a pull request can occasionally move between pages or appear twice in sequence—repeat the same filters on every request so results stay consistent.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Projects","description":"Manage and query projects in your organization.\n\nProjects are the top-level containers for test runs, specs, and test results.\nEach project has a unique project ID and can be configured with various settings."},{"name":"Runs","description":"Query and manage test runs.\n\nA run represents a single execution of your test suite. Runs contain spec files\nwhich are executed across multiple instances. You can query run details, cancel\nruns, reset failed specs, and delete runs.\n\nPer-test outcome counters on specs (`results.stats`) use the `RecordedTestOutcome` vocabulary—see **components/schemas** for how **pending** and **skipped** differ from **failures**."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["results:read"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"ProjectIdPath":{"name":"projectId","in":"path","required":true,"description":"The project ID","schema":{"type":"string"}},"LimitQuery":{"name":"limit","in":"query","required":false,"description":"Maximum number of items to return (default: 10, max: 50)","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}},"StartingAfterQuery":{"name":"starting_after","in":"query","required":false,"description":"Cursor for pagination. Returns items after this cursor value.","schema":{"type":"string"}},"EndingBeforeQuery":{"name":"ending_before","in":"query","required":false,"description":"Cursor for pagination. Returns items before this cursor value.","schema":{"type":"string"}},"PullRequestRunsPerPrQuery":{"name":"runs_per_pr","in":"query","required":false,"description":"How many most recent runs to include in each card’s `timelinePreview` (does not cap total stats). 0 returns an empty `timelinePreview`.","schema":{"type":"integer","minimum":0,"maximum":10,"default":1}},"TagsQuery":{"name":"tags[]","in":"query","required":false,"description":"Filter by tags (can be specified multiple times using bracket notation, e.g., tags[]=value1&tags[]=value2)","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"BranchesQuery":{"name":"branches[]","in":"query","required":false,"description":"Filter by git branch. Use trailing `*` for prefix match (e.g. `release/*`). Max 250 characters per value.","schema":{"type":"array","items":{"$ref":"#/components/schemas/BranchFilterQueryValue"}},"style":"form","explode":true},"AuthorsGlobQuery":{"name":"authors[]","in":"query","required":false,"description":"Filter by git author name. Use trailing `*` for prefix match (e.g. `Adam*`). May contain spaces. Max 250 characters per value.","schema":{"type":"array","items":{"$ref":"#/components/schemas/AuthorGlobFilterQueryValue"}},"style":"form","explode":true},"TagOperatorQuery":{"name":"tag_operator","in":"query","required":false,"description":"Logical operator for tag filtering. AND requires all tags to be present (default), OR requires any tag to be present.","schema":{"type":"string","enum":["AND","OR"],"default":"AND"}},"RunFeedSearchQuery":{"name":"search","in":"query","required":false,"description":"Search runs by ciBuildId or commit message (case-insensitive). The raw value must be printable ASCII only (no control characters). Leading and trailing whitespace is trimmed before length checks; whitespace-only values are rejected. After trimming, the value must be 1–200 characters.","schema":{"type":"string","pattern":"^[ -~]+$"}},"PullRequestSearchQuery":{"name":"pr_search","in":"query","required":false,"description":"Find pull requests by title, number or source branch. Checked on each pull request's latest run among the runs that match the run filters (dates, branches, tags, authors, environments, pr_id, search), so a renamed pull request is found by its latest title. Matches when that run's pull request title or source branch contains the text (case-insensitive), or when the text is a pull request number (`123` or `#123`) equal to the number in the pull request id. When more than one of status, completion_state and pr_search is sent, that one run must match all of them. Unlike `search`, the value may have any characters except control characters. Leading and trailing whitespace is trimmed before length checks; whitespace-only values are rejected. After trimming, the value must be 1–200 characters.","schema":{"type":"string","pattern":"^[^\\x00-\\x1F\\x7F-\\x9F]+$"}},"PullRequestStatusQuery":{"name":"status","in":"query","required":false,"description":"Filter pull requests by run status (can be specified multiple times). The status is checked on each pull request's latest run among the runs that match the run filters (dates, branches, tags, authors, environments, pr_id, search). When more than one of status, completion_state and pr_search is sent, that one run must match all of them. PASSED: all tests passed, FAILED: some tests failed, RUNNING: run is in progress and passing, FAILING: run is in progress but has failures.","schema":{"type":"array","items":{"type":"string","enum":["PASSED","FAILED","RUNNING","FAILING"]}},"style":"form","explode":true},"PullRequestCompletionStateQuery":{"name":"completion_state","in":"query","required":false,"description":"Filter pull requests by run completion state (can be specified multiple times). The completion state is checked on each pull request's latest run among the runs that match the run filters (dates, branches, tags, authors, environments, pr_id, search). When more than one of status, completion_state and pr_search is sent, that one run must match all of them. COMPLETE: run finished normally, IN_PROGRESS: run is still executing, CANCELED: run was canceled, TIMEOUT: run timed out.","schema":{"type":"array","items":{"type":"string","enum":["COMPLETE","IN_PROGRESS","CANCELED","TIMEOUT"]}},"style":"form","explode":true},"EnvironmentsQuery":{"name":"environments[]","in":"query","required":false,"description":"Filter by environment (can be specified multiple times using bracket notation, e.g., environments[]=staging&environments[]=production)","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"RunFeedPrIdQuery":{"name":"pr_id","in":"query","required":false,"description":"Filter runs by normalized pull request id (stored as meta.pr.id). Printable ASCII only, max 128 characters.","schema":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[!-~]+$"}},"OptionalDateStartQuery":{"name":"date_start","in":"query","required":false,"description":"Filter runs created on or after this ISO 8601 date-time. A date without a time (`2024-01-01`) means midnight UTC of that day.","schema":{"type":"string"}},"OptionalDateEndQuery":{"name":"date_end","in":"query","required":false,"description":"Filter runs created before this ISO 8601 date-time. A date without a time (`2024-12-31`) means midnight UTC of that day, so the range ends before that day.","schema":{"type":"string"}},"PullRequestOrderQuery":{"name":"order","in":"query","required":false,"description":"Sort pull requests by `last_run` (creation time of the latest run), `first_run` (creation time of the oldest run) or `run_count` (number of runs), in the direction set by `dir`. Only runs that match the run filters (dates, branches, tags, authors, environments, pr_id, search) count, so with `date_start` the first run is the oldest run in the range. Pull requests with the same value are sorted by pull request id, in the same direction.","schema":{"type":"string","enum":["last_run","first_run","run_count"],"default":"last_run"}},"SortDirectionQuery":{"name":"dir","in":"query","required":false,"description":"Sort direction: `desc` (default) or `asc`.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},"PullRequestIncludeTotalQuery":{"name":"include_total","in":"query","required":false,"description":"When true, the response has `total`: the number of pull requests that match all filters, including `status`, `completion_state` and `pr_search`, over all pages. It takes one more query, which with `status`, `completion_state` or `pr_search` checks the latest run of every pull request that matches the other filters.","schema":{"type":"boolean","default":false}}},"schemas":{"BranchFilterQueryValue":{"description":"Branch filter value: an exact branch name, or a trailing `*` for prefix match (e.g. `release/*`). Max 250 characters.","oneOf":[{"type":"string","description":"Exact branch name (must not contain `*` or spaces).","pattern":"^[!-)+-~]+$","maxLength":250},{"type":"string","description":"Prefix match: branch prefix followed by `*`.","pattern":"^[!-)+-~]+\\*$","maxLength":250}]},"AuthorGlobFilterQueryValue":{"description":"Author name filter value: an exact name, or a trailing `*` for prefix match (e.g. `Adam*`). May contain spaces and non-ASCII letters; control characters are not allowed. Max 250 characters.","oneOf":[{"type":"string","description":"Exact author name (must not contain `*`).","pattern":"^[^\\x00-\\x1F\\x7F-\\x9F*]+$","maxLength":250},{"type":"string","description":"Prefix match: name prefix followed by `*`.","pattern":"^[^\\x00-\\x1F\\x7F-\\x9F*]+\\*$","maxLength":250}]},"PullRequestsListResponse":{"type":"object","required":["status","has_more","data"],"properties":{"status":{"type":"string","enum":["OK"]},"has_more":{"type":"boolean","description":"Whether more PR rows exist after this page"},"data":{"type":"array","items":{"$ref":"#/components/schemas/PullRequestCard"}},"total":{"type":"integer","description":"Only with `include_total=true`: the number of pull requests that match all filters, over all pages"}}},"PullRequestCard":{"type":"object","required":["pr","stats","latestRun","timelinePreview","hasMoreRuns","cursor","recentRuns","stackedOn"],"properties":{"pr":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Canonical pull request id (`meta.pr.id`)"},"prNumber":{"type":"string","nullable":true,"description":"Numeric PR/MR number extracted from the canonical id (e.g. \"42\" from \"github:github.com/org/repo#42\")"},"link":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"source":{"type":"string","nullable":true},"destination":{"type":"string","nullable":true},"repository":{"type":"string","nullable":true,"description":"Repository from the canonical id (e.g. \"org/repo\" from \"github:github.com/org/repo#42\"). The first segment of the id's target is dropped as the host when it contains a dot; otherwise the whole target is returned, so an id from a hostname without a dot keeps it (\"gitlab/acme/web\" from \"gitlab:gitlab/acme/web#12\"). An \"<org>.visualstudio.com\" host is replaced by \"<org>\", as in dev.azure.com ids. Null for a bare PR number or an id in an unknown format."}}},"stats":{"$ref":"#/components/schemas/PullRequestStats"},"latestRun":{"$ref":"#/components/schemas/RunFeedItem"},"timelinePreview":{"type":"array","description":"Most recent runs for this PR (newest first), up to `runs_per_pr` from the request (default 1, max 10; empty for 0)","items":{"$ref":"#/components/schemas/RunFeedItem"}},"recentRuns":{"type":"array","description":"Up to 10 most recent runs for this PR (newest first), whatever `runs_per_pr` is. Taken from the same runs as `latestRun` and `stats`: the runs that match the filters, up to and including `latestRun`, so the first item is `latestRun`. A run created while the request is being served appears on the next request.","items":{"$ref":"#/components/schemas/PullRequestRecentRun"}},"hasMoreRuns":{"type":"boolean","description":"True when more runs exist for this PR than returned in `timelinePreview`"},"cursor":{"type":"string","description":"Opaque pagination cursor for this PR row; pass as `starting_after` for the next page or as `ending_before` for the previous page, with the same filters, `order` and `dir`"},"stackedOn":{"type":"object","nullable":true,"required":["id","prNumber","title"],"description":"The pull request this one is stacked on: a pull request from the same vendor with the same `repository` (null matches null) whose source branch is this pull request's `destination`. It must have a run in this project from 90 days before the oldest `latestRun` on this page (but no earlier than 180 days before the newest one) to 90 days after the newest one, and does not have to match the filters or be on this page. A pull request whose source and destination are the same branch is never used. When several pull requests have that source branch, the one with the newest run in that time span is used. Null when `destination` is null, `main`, `master` or the project's default branch, when no such pull request is found, when more than 5 pull requests on this page have the same `repository` and `destination`, and when the lookup fails or takes more than 2 seconds. Because the time span and the count come from the page, a request that returns fewer pull requests (for example with `pr_id`, other filters or a `limit` of 5 or less) can return a parent where a larger page returns null, or another `title` for the same parent.","properties":{"id":{"type":"string","description":"Canonical pull request id (`meta.pr.id`)"},"prNumber":{"type":"string","nullable":true},"title":{"type":"string","nullable":true,"description":"The title stored on its latest run in the time span above"}}}}},"PullRequestStats":{"type":"object","required":["total","passed","failed","running","failing","canceled","timedOut"],"properties":{"total":{"type":"integer","description":"Number of runs grouped under this pull request"},"passed":{"type":"integer","description":"Runs with PASSED status"},"failed":{"type":"integer","description":"Runs with FAILED status"},"running":{"type":"integer","description":"Runs with RUNNING status"},"failing":{"type":"integer","description":"Runs with FAILING status"},"canceled":{"type":"integer","description":"Runs with CANCELED completion state. The status counters also count these runs, so `canceled` overlaps them."},"timedOut":{"type":"integer","description":"Runs with TIMEOUT completion state. The status counters also count these runs, so `timedOut` overlaps them."}}},"RunFeedItem":{"allOf":[{"type":"object","properties":{"runId":{"type":"string"},"projectId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"durationMs":{"type":"integer","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"environments":{"type":"array","items":{"type":"string"},"nullable":true},"previousRunId":{"type":"string","nullable":true},"isSession":{"type":"boolean","description":"Recorded from a browser session rather than by a reporter in CI. Absent on runs recorded by a CI reporter."},"cursor":{"type":"string"},"timeout":{"type":"object","properties":{"isTimeout":{"type":"boolean"},"timeoutValueMs":{"type":"integer","nullable":true}}},"cancellation":{"type":"object","nullable":true},"groups":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"meta":{"$ref":"#/components/schemas/RunMeta"},"completionState":{"type":"string"},"status":{"type":"string"}}}]},"RunGroup":{"type":"object","properties":{"groupId":{"type":"string"},"platform":{"type":"object","properties":{"osName":{"type":"string"},"osVersion":{"type":"string"},"browserName":{"type":"string"},"browserVersion":{"type":"string"}}},"tags":{"type":"array","items":{"type":"string"}},"environment":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time","nullable":true},"coverageEnabled":{"type":"boolean"},"durationMs":{"type":"integer"},"specCount":{"type":"integer"},"instances":{"type":"object","properties":{"overall":{"type":"integer"},"claimed":{"type":"integer"},"complete":{"type":"integer"},"passes":{"type":"integer"},"failures":{"type":"integer"}}},"tests":{"type":"object","description":"Test counts for this group. Use `total` (scheduled suite size) and `recorded` (tests with a reported outcome so far). `tests` and `overall` are deprecated aliases.","properties":{"total":{"type":"integer","description":"Total tests scheduled/expected in this group, including tests still running or not yet started."},"recorded":{"type":"integer","description":"Tests in this group that have reported an outcome so far (passes + failures + pending + skipped). Less than or equal to `total` while the group is in progress."},"tests":{"type":"integer","deprecated":true,"description":"Deprecated alias of `total` (scheduled suite size). Use `total`."},"overall":{"type":"integer","deprecated":true,"description":"Deprecated alias of `recorded`. Despite the name this is NOT the suite total — use `recorded` for the reported-outcome count, or `total` for the suite size."},"passes":{"type":"integer"},"failures":{"type":"integer"},"pending":{"type":"integer","description":"**pending** outcomes (intentional skips)."},"retries":{"type":"integer"},"skipped":{"type":"integer","description":"**skipped** outcomes (separate from `failures` here; see schema for Tests Explorer difference)."},"flaky":{"type":"integer"}}}}},"RunMeta":{"type":"object","properties":{"ciBuildId":{"type":"string","description":"CI build identifier"},"pr":{"type":"object","nullable":true,"description":"Normalized pull request metadata when the run is associated with a PR","properties":{"id":{"type":"string","nullable":true},"link":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"source":{"type":"string","nullable":true},"destination":{"type":"string","nullable":true}}},"commit":{"type":"object","properties":{"sha":{"type":"string"},"branch":{"type":"string"},"authorName":{"type":"string"},"authorEmail":{"type":"string"},"message":{"type":"string"},"remoteOrigin":{"type":"string"}}},"framework":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"}}}}},"PullRequestRecentRun":{"type":"object","required":["runId","createdAt","status","completionState","durationMs","commit","failures"],"properties":{"runId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["PASSED","FAILED","RUNNING","FAILING"]},"completionState":{"type":"string","enum":["COMPLETE","IN_PROGRESS","CANCELED","TIMEOUT"]},"durationMs":{"type":"integer","nullable":true,"description":"Null when the run has no recorded duration"},"commit":{"type":"object","required":["sha","message","authorName"],"properties":{"sha":{"type":"string","nullable":true},"message":{"type":"string","nullable":true},"authorName":{"type":"string","nullable":true}}},"failures":{"type":"integer","description":"Failed tests, summed over the groups of the run. Skipped tests are not counted, so a run with FAILED status can have 0."}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/projects/{projectId}/pull-requests":{"get":{"summary":"List pull requests","description":"Lists pull requests for this project together with summarized run activity. Each result includes rollup stats for the PR, its most recent run, and an optional preview of additional runs (`runs_per_pr`: 0 to 10, default 1). Each result also has `recentRuns`: up to 10 of its latest runs, newest first, whatever `runs_per_pr` is, each with its id, creation time, status, completion state, duration, commit and number of failed tests. Send `include_total=true` to also get `total`, the number of pull requests that match all filters over all pages. Optional filters choose which runs count: branches[], tags[], authors[], environments[], `pr_id`, a `date_start`/`date_end` range on run creation time (no range by default), or `search` (case-insensitive substring match on ciBuildId or commit message); pull requests with no runs matching the applied filters are omitted. `status`, `completion_state` and `pr_search` (title, number or source branch) are checked on each pull request's latest run among the runs that match the other filters. For each remaining pull request, the status shown corresponds to its latest run that matches your filters. Branch values may end with `*` for a prefix match. Results are sorted by `order` (`last_run`, `first_run` or `run_count`; default `last_run`) in the direction set by `dir` (default `desc`). Pagination uses opaque tokens: advance with `starting_after`, go back with `ending_before`; use the returned `cursor` from each item as the token and keep the same filters, `order` and `dir` across pages. A cursor returned for another `order` or `dir` is rejected with 400. Sending both cursor parameters at once is not supported. Because new runs can arrive while you page, a pull request can occasionally move between pages or appear twice in sequence—repeat the same filters on every request so results stay consistent.","operationId":"listProjectPullRequests","tags":["Projects","Runs"],"parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/StartingAfterQuery"},{"$ref":"#/components/parameters/EndingBeforeQuery"},{"$ref":"#/components/parameters/PullRequestRunsPerPrQuery"},{"$ref":"#/components/parameters/TagsQuery"},{"$ref":"#/components/parameters/BranchesQuery"},{"$ref":"#/components/parameters/AuthorsGlobQuery"},{"$ref":"#/components/parameters/TagOperatorQuery"},{"$ref":"#/components/parameters/RunFeedSearchQuery"},{"$ref":"#/components/parameters/PullRequestSearchQuery"},{"$ref":"#/components/parameters/PullRequestStatusQuery"},{"$ref":"#/components/parameters/PullRequestCompletionStateQuery"},{"$ref":"#/components/parameters/EnvironmentsQuery"},{"$ref":"#/components/parameters/RunFeedPrIdQuery"},{"$ref":"#/components/parameters/OptionalDateStartQuery"},{"$ref":"#/components/parameters/OptionalDateEndQuery"},{"$ref":"#/components/parameters/PullRequestOrderQuery"},{"$ref":"#/components/parameters/SortDirectionQuery"},{"$ref":"#/components/parameters/PullRequestIncludeTotalQuery"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PullRequestsListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"503":{"description":"The runs of every pull request on the page changed while the page loaded. Retry the request after the number of seconds in `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the request.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Get project insights

> Get aggregated run and test metrics for a project within a date range

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Projects","description":"Manage and query projects in your organization.\n\nProjects are the top-level containers for test runs, specs, and test results.\nEach project has a unique project ID and can be configured with various settings."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["analytics:read"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"ProjectIdPath":{"name":"projectId","in":"path","required":true,"description":"The project ID","schema":{"type":"string"}},"DateStartQuery":{"name":"date_start","in":"query","required":true,"description":"Start date in ISO 8601 format","schema":{"type":"string","format":"date-time"}},"DateEndQuery":{"name":"date_end","in":"query","required":true,"description":"End date in ISO 8601 format","schema":{"type":"string","format":"date-time"}},"ResolutionQuery":{"name":"resolution","in":"query","required":false,"description":"Time resolution for histogram data","schema":{"type":"string","enum":["1d","1w"],"default":"1d"}},"TagsQuery":{"name":"tags[]","in":"query","required":false,"description":"Filter by tags (can be specified multiple times using bracket notation, e.g., tags[]=value1&tags[]=value2)","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"BranchesQuery":{"name":"branches[]","in":"query","required":false,"description":"Filter by git branch. Use trailing `*` for prefix match (e.g. `release/*`). Max 250 characters per value.","schema":{"type":"array","items":{"$ref":"#/components/schemas/BranchFilterQueryValue"}},"style":"form","explode":true},"GroupsQuery":{"name":"groups[]","in":"query","required":false,"description":"Filter by groups (can be specified multiple times using bracket notation, e.g., groups[]=group1&groups[]=group2)","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},"AuthorsQuery":{"name":"authors[]","in":"query","required":false,"description":"Filter by git authors (can be specified multiple times using bracket notation, e.g., authors[]=author1&authors[]=author2)","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true}},"schemas":{"BranchFilterQueryValue":{"description":"Branch filter value: an exact branch name, or a trailing `*` for prefix match (e.g. `release/*`). Max 250 characters.","oneOf":[{"type":"string","description":"Exact branch name (must not contain `*` or spaces).","pattern":"^[!-)+-~]+$","maxLength":250},{"type":"string","description":"Prefix match: branch prefix followed by `*`.","pattern":"^[!-)+-~]+\\*$","maxLength":250}]},"ProjectInsightsResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"type":"object","required":["projectId","orgId","results","metrics"],"properties":{"projectId":{"type":"string"},"orgId":{"type":"string"},"dateStart":{"type":"string","format":"date-time"},"dateEnd":{"type":"string","format":"date-time"},"resolution":{"type":"string"},"results":{"type":"object","properties":{"overall":{"type":"object","properties":{"runs":{"$ref":"#/components/schemas/RunMetrics"},"tests":{"$ref":"#/components/schemas/TestMetrics"}}},"timeline":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","properties":{"runs":{"$ref":"#/components/schemas/RunMetrics"},"tests":{"$ref":"#/components/schemas/TestMetrics"}}}}}}},"metrics":{"type":"object","description":"Duration and health of the project's tests and spec files over the date range, each against the range of the same length before it. Filters are applied to each execution, not to the run it belongs to, so these numbers are not derivable from the run counts in `results`.","required":["tests","specs"],"properties":{"tests":{"$ref":"#/components/schemas/MetricsSummary"},"specs":{"$ref":"#/components/schemas/MetricsSummary"}}}}}}},"RunMetrics":{"type":"object","properties":{"total":{"type":"integer","description":"Total number of runs"},"cancelled":{"type":"integer","description":"Cancelled runs"},"timeouts":{"type":"integer","description":"Timed out runs"},"completed":{"type":"integer","description":"Fully completed runs"},"failed":{"type":"integer","description":"Failed runs"},"passed":{"type":"integer","description":"Passed runs"},"nonFullyReported":{"type":"integer","description":"Runs that did not fully report"},"avgDurationSeconds":{"type":"number","description":"Average run duration in seconds"},"avgSuccessRate":{"type":"number","description":"Average success rate (0-1)"}}},"TestMetrics":{"type":"object","properties":{"total":{"type":"integer","description":"Total test executions"},"failed":{"type":"integer","description":"Failed tests"},"passed":{"type":"integer","description":"Passed tests"},"pending":{"type":"integer","description":"**pending** outcomes (intentional skips). See `RecordedTestOutcome`."},"skipped":{"type":"integer","description":"**skipped** outcomes (runner skip / interrupted). See `RecordedTestOutcome`."},"flaky":{"type":"integer","description":"Flaky tests"}}},"MetricsSummary":{"type":"object","description":"Duration percentiles and outcome rates for a selection of tests or spec files, plus the duration histogram.","required":["current","previous","minMs","maxMs","distribution"],"properties":{"current":{"$ref":"#/components/schemas/PeriodMetrics"},"previous":{"allOf":[{"$ref":"#/components/schemas/PeriodMetrics"}],"description":"The range of the same length before the one asked for."},"minMs":{"type":"number","nullable":true,"description":"Shortest execution in the current range."},"maxMs":{"type":"number","nullable":true,"description":"Longest execution in the current range."},"distribution":{"type":"array","description":"Executions per duration bucket, in the current range. Buckets widen with duration; empty buckets between the first and last are included.","items":{"type":"object","required":["minMs","maxMs","executions"],"properties":{"minMs":{"type":"number"},"maxMs":{"type":"number"},"executions":{"type":"number"}}}}}},"PeriodMetrics":{"type":"object","required":["executions","overallExecutions","avgMs","p50Ms","p90Ms","p95Ms","passRate","failureRate","flakinessRate","suiteSize","testsPerExecution"],"properties":{"executions":{"type":"number","description":"Executions the duration metrics are calculated over. Narrower than `overallExecutions` when the average-duration metric setting excludes a status."},"overallExecutions":{"type":"number","description":"Executions the rates are calculated over."},"avgMs":{"type":"number","nullable":true},"p50Ms":{"type":"number","nullable":true},"p90Ms":{"type":"number","nullable":true},"p95Ms":{"type":"number","nullable":true},"passRate":{"type":"number","nullable":true,"description":"Rate as a 0-1 float. Tests only."},"failureRate":{"type":"number","nullable":true,"description":"Rate as a 0-1 float. Failures are failed and interrupted executions together."},"flakinessRate":{"type":"number","nullable":true},"suiteSize":{"type":"number","nullable":true,"description":"Tests in the most recent execution of a spec file. Null unless the metrics describe a single spec file."},"testsPerExecution":{"type":"number","nullable":true,"description":"Average tests per spec execution. Spec files only."}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/projects/{projectId}/insights":{"get":{"summary":"Get project insights","description":"Get aggregated run and test metrics for a project within a date range","operationId":"getProjectInsights","tags":["Projects"],"parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/DateStartQuery"},{"$ref":"#/components/parameters/DateEndQuery"},{"$ref":"#/components/parameters/ResolutionQuery"},{"$ref":"#/components/parameters/TagsQuery"},{"$ref":"#/components/parameters/BranchesQuery"},{"$ref":"#/components/parameters/GroupsQuery"},{"$ref":"#/components/parameters/AuthorsQuery"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectInsightsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## List project terms for a single type

> Returns cursor-paginated terms for one kind (\`tag\`, \`branch\`, \`authorName\`, etc.). Rows are ordered by each term's last update time; use \`dir\` as \`asc\` or \`desc\` (default \`desc\`, most recently updated first). Page with \`starting\_after\` or \`ending\_before\` using the opaque \`cursor\` value from a returned item; do not send both cursors in one request. Reuse the same \`dir\`, \`limit\`, search filter, and path when requesting additional pages.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Projects","description":"Manage and query projects in your organization.\n\nProjects are the top-level containers for test runs, specs, and test results.\nEach project has a unique project ID and can be configured with various settings."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["projects:read"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"ProjectIdPath":{"name":"projectId","in":"path","required":true,"description":"The project ID","schema":{"type":"string"}},"TermTypePath":{"name":"termType","in":"path","required":true,"description":"Term kind to list","schema":{"type":"string","enum":["tag","group","branch","authorName","authorEmail","framework","frameworkVersion","clientVersion","ann_type","ann_desc"]}},"TermsLimitQuery":{"name":"limit","in":"query","required":false,"description":"Maximum number of terms to return (default: 100, max: 100)","schema":{"type":"integer","minimum":1,"maximum":100,"default":100}},"SortDirectionQuery":{"name":"dir","in":"query","required":false,"description":"Sort direction: `desc` (default) or `asc`.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},"StartingAfterQuery":{"name":"starting_after","in":"query","required":false,"description":"Cursor for pagination. Returns items after this cursor value.","schema":{"type":"string"}},"EndingBeforeQuery":{"name":"ending_before","in":"query","required":false,"description":"Cursor for pagination. Returns items before this cursor value.","schema":{"type":"string"}},"TermsSearchQuery":{"name":"search","in":"query","required":false,"description":"Optional case-insensitive prefix filter on term values (up to 128 characters)","schema":{"type":"string","maxLength":128}}},"schemas":{"ProjectTermsListResponse":{"type":"object","required":["status","has_more","data"],"properties":{"status":{"type":"string","enum":["OK"]},"has_more":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ProjectTermsTermItem"},"description":"Ordered term rows for the requested type"}}},"ProjectTermsTermItem":{"type":"object","required":["id","name","count","updated_at","cursor"],"properties":{"id":{"type":"string","description":"Unique identifier for this term"},"name":{"type":"string","description":"Term value"},"count":{"type":"integer","description":"Number of times this term was used"},"updated_at":{"type":"string","format":"date-time","nullable":true,"description":"Last time the term stats were updated"},"cursor":{"type":"string","description":"Opaque pagination token for this row. Pass as `starting_after` or `ending_before` on follow-up requests to fetch the next or previous page along the same sort order."}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/projects/{projectId}/terms/{termType}":{"get":{"summary":"List project terms for a single type","description":"Returns cursor-paginated terms for one kind (`tag`, `branch`, `authorName`, etc.). Rows are ordered by each term's last update time; use `dir` as `asc` or `desc` (default `desc`, most recently updated first). Page with `starting_after` or `ending_before` using the opaque `cursor` value from a returned item; do not send both cursors in one request. Reuse the same `dir`, `limit`, search filter, and path when requesting additional pages.","operationId":"listProjectTermsByType","tags":["Projects"],"parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/TermTypePath"},{"$ref":"#/components/parameters/TermsLimitQuery"},{"$ref":"#/components/parameters/SortDirectionQuery"},{"$ref":"#/components/parameters/StartingAfterQuery"},{"$ref":"#/components/parameters/EndingBeforeQuery"},{"$ref":"#/components/parameters/TermsSearchQuery"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectTermsListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Delete a project term

> Permanently removes a term from the project filter list. Only terms of type \`tag\` can be deleted; other term types return 404. This does not remove the term from historical runs; it only deletes the term metadata document. Requires a write-scoped API key.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Projects","description":"Manage and query projects in your organization.\n\nProjects are the top-level containers for test runs, specs, and test results.\nEach project has a unique project ID and can be configured with various settings."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["projects:write"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"ProjectIdPath":{"name":"projectId","in":"path","required":true,"description":"The project ID","schema":{"type":"string"}},"TermTypePath":{"name":"termType","in":"path","required":true,"description":"Term kind to list","schema":{"type":"string","enum":["tag","group","branch","authorName","authorEmail","framework","frameworkVersion","clientVersion","ann_type","ann_desc"]}},"TermIdPath":{"name":"termId","in":"path","required":true,"description":"Unique identifier of the term to delete. Only terms of type `tag` can be deleted.","schema":{"type":"string"}}},"schemas":{"ProjectTermDeleteResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"$ref":"#/components/schemas/ProjectTermsTermItem"}}},"ProjectTermsTermItem":{"type":"object","required":["id","name","count","updated_at","cursor"],"properties":{"id":{"type":"string","description":"Unique identifier for this term"},"name":{"type":"string","description":"Term value"},"count":{"type":"integer","description":"Number of times this term was used"},"updated_at":{"type":"string","format":"date-time","nullable":true,"description":"Last time the term stats were updated"},"cursor":{"type":"string","description":"Opaque pagination token for this row. Pass as `starting_after` or `ending_before` on follow-up requests to fetch the next or previous page along the same sort order."}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/projects/{projectId}/terms/{termType}/{termId}":{"delete":{"summary":"Delete a project term","description":"Permanently removes a term from the project filter list. Only terms of type `tag` can be deleted; other term types return 404. This does not remove the term from historical runs; it only deletes the term metadata document. Requires a write-scoped API key.","operationId":"deleteProjectTerm","tags":["Projects"],"parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/TermTypePath"},{"$ref":"#/components/parameters/TermIdPath"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectTermDeleteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.currents.dev/api/resources/projects.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
