> 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/sessions.md).

# Sessions

Browser sessions an agent recorded: a title, how it ended, the git commit and pull request, and attachments such as a trace, screenshots and a video.

A session is not a run. It does not appear in run lists or in analytics, and it has no spec files or tests. Share one with `POST /share` and `sessionId`.

## Create a session

> Records a browser session and signs an upload URL for each attachment declared in the request. Upload each file with a PUT to its \`uploadUrl\`, sending the headers in \`uploadHeaders\`; when \`sizeBytes\` was sent, the body must be exactly that long. The session is complete when created: there is nothing to finish. Besides a bearer token, this route accepts a record key in the \`x-currents-key\` header.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Sessions","description":"Browser sessions an agent recorded: a title, how it ended, the git commit and pull request, and attachments such as a trace, screenshots and a video.\n\nA session is not a run. It does not appear in run lists or in analytics, and it has no spec files or tests. Share one with `POST /share` and `sessionId`."}],"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":["runs:write"]},{"RecordKey":[]}],"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. 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","shares:write":"Create public links to test results that anyone with the link can open until they expire"}}}},"RecordKey":{"type":"apiKey","in":"header","name":"x-currents-key","description":"An organization's record key, the one a reporter sends. Accepted only by the two attachment routes under `/runs`, `POST /sessions`, `POST /sessions/{sessionId}/attachments` and, for a session, `POST /share`, so a CI job can attach files, record a session and share it with the key it already has."}},"schemas":{"CreateSessionRequest":{"type":"object","required":["projectId","title","status"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"title":{"type":"string","minLength":1,"maxLength":1024,"description":"What the session set out to show"},"status":{"type":"string","enum":["passed","failed"]},"error":{"type":"string","maxLength":10000,"description":"For a failed session"},"durationMs":{"type":"integer","minimum":0,"maximum":86400000},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":255},"maxItems":20},"commit":{"type":"object","properties":{"sha":{"type":"string"},"branch":{"type":"string"},"message":{"type":"string"},"authorName":{"type":"string"},"authorEmail":{"type":"string"},"remoteOrigin":{"type":"string"}}},"pr":{"type":"object","description":"The pull request, by `link` or `id`","properties":{"link":{"type":"string"},"id":{"type":"string"}}},"ci":{"type":"object","description":"CI variables the pull request is read from when `pr` is not sent","properties":{"provider":{"type":"string"},"buildNumber":{"type":"string"},"params":{"type":"object"}}},"attachments":{"type":"array","maxItems":50,"description":"Like an attachment request, but `sizeBytes` is optional. Without it the upload URL does not pin the size or type.","items":{"type":"object","required":["name","type","contentType"],"properties":{"name":{"type":"string","minLength":1,"maxLength":1024},"type":{"type":"string","enum":["trace","screenshot","video","attachment"],"description":"Allowed per level: attachment on a run, spec file and test; screenshot, video and attachment on a spec file; trace, screenshot, video and attachment on an attempt."},"contentType":{"type":"string","description":"A trace is application/zip, a screenshot is image/png, image/jpeg, image/webp or image/gif, a video is video/*."},"sizeBytes":{"type":"integer","minimum":1,"maximum":1073741824,"description":"Signed into the upload URL, so storage refuses another size. Organizations that store files in their own S3 bucket behind CloudFront get an upload URL without this check."},"caption":{"type":"string","maxLength":1000},"meta":{"type":"object","additionalProperties":{"type":"string","maxLength":256},"description":"At most 20 keys of letters, digits, \"_\" and \"-\", and 2 KB in total."}}}}}},"SessionAttachmentsResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"type":"object","required":["sessionId","attachments","uploadExpiresInSeconds"],"properties":{"sessionId":{"type":"string"},"uploadExpiresInSeconds":{"type":"integer"},"attachments":{"type":"array","description":"In the order of the request","items":{"type":"object","required":["attachmentId","name","type","uploadUrl","uploadHeaders"],"properties":{"attachmentId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"uploadUrl":{"type":"string"},"uploadHeaders":{"type":"object","additionalProperties":{"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`."},"candidates":{"type":"array","description":"With `code: ambiguous_target`: the runs, spec files or tests the request matched. Send one of their IDs.","items":{"type":"object","properties":{"runId":{"type":"string"},"ciBuildId":{"type":"string"},"instanceId":{"type":"string"},"groupId":{"type":"string"},"spec":{"type":"string"},"machineId":{"type":"string","nullable":true},"testId":{"type":"string"},"title":{"type":"array","items":{"type":"string"}}}}}}}},"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. Five 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`), an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`), and an API key on an endpoint that answers for a user (`user_credential_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` and `user_credential_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":{"/sessions":{"post":{"summary":"Create a session","description":"Records a browser session and signs an upload URL for each attachment declared in the request. Upload each file with a PUT to its `uploadUrl`, sending the headers in `uploadHeaders`; when `sizeBytes` was sent, the body must be exactly that long. The session is complete when created: there is nothing to finish. Besides a bearer token, this route accepts a record key in the `x-currents-key` header.","operationId":"createSession","tags":["Sessions"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSessionRequest"}}}},"responses":{"200":{"description":"The session ID and upload URLs, in the order of the request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionAttachmentsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Recording is suspended or the subscription has expired"},"429":{"description":"Too many requests for the organization (`rate_limited`)"}}}}}}
```

## Get a session

> The session and its attachments, oldest first, each with a read URL signed for an hour.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Sessions","description":"Browser sessions an agent recorded: a title, how it ended, the git commit and pull request, and attachments such as a trace, screenshots and a video.\n\nA session is not a run. It does not appear in run lists or in analytics, and it has no spec files or tests. Share one with `POST /share` and `sessionId`."}],"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. 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","shares:write":"Create public links to test results that anyone with the link can open until they expire"}}}}},"parameters":{"SessionIdPath":{"name":"sessionId","in":"path","required":true,"description":"The session ID","schema":{"type":"string"}}},"schemas":{"Session":{"allOf":[{"$ref":"#/components/schemas/SessionListItem"},{"type":"object","properties":{"attachments":{"type":"array","items":{"$ref":"#/components/schemas/SessionAttachment"}}}}]},"SessionListItem":{"type":"object","properties":{"sessionId":{"type":"string"},"projectId":{"type":"string"},"title":{"type":"string"},"status":{"type":"string","enum":["passed","failed"]},"error":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"durationMs":{"type":"integer"},"git":{"type":"object","properties":{"sha":{"type":"string","nullable":true},"branch":{"type":"string","nullable":true},"message":{"type":"string","nullable":true},"authorName":{"type":"string","nullable":true},"authorEmail":{"type":"string","nullable":true},"remoteOrigin":{"type":"string","nullable":true}}},"pr":{"type":"object","nullable":true,"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}}},"attachmentCount":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"cursor":{"type":"string","description":"Pass as `starting_after` or `ending_before`"}}},"SessionAttachment":{"type":"object","required":["attachmentId","name","type","contentType","sizeBytes","caption","meta","createdAt","readUrl"],"properties":{"attachmentId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["trace","screenshot","video","attachment"]},"contentType":{"type":"string","nullable":true},"sizeBytes":{"type":"integer","nullable":true},"caption":{"type":"string","nullable":true},"meta":{"type":"object","additionalProperties":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"readUrl":{"type":"string","description":"Signed, valid for an hour; an attachment downloads under its own name."}}},"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`."},"candidates":{"type":"array","description":"With `code: ambiguous_target`: the runs, spec files or tests the request matched. Send one of their IDs.","items":{"type":"object","properties":{"runId":{"type":"string"},"ciBuildId":{"type":"string"},"instanceId":{"type":"string"},"groupId":{"type":"string"},"spec":{"type":"string"},"machineId":{"type":"string","nullable":true},"testId":{"type":"string"},"title":{"type":"array","items":{"type":"string"}}}}}}}},"responses":{"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. Five 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`), an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`), and an API key on an endpoint that answers for a user (`user_credential_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` and `user_credential_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":{"/sessions/{sessionId}":{"get":{"summary":"Get a session","description":"The session and its attachments, oldest first, each with a read URL signed for an hour.","operationId":"getSession","tags":["Sessions"],"parameters":[{"$ref":"#/components/parameters/SessionIdPath"}],"responses":{"200":{"description":"The session","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["OK"]},"data":{"$ref":"#/components/schemas/Session"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Delete a session

> The session stops being readable at once. Its attachments, share links and share assets are removed in the background.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Sessions","description":"Browser sessions an agent recorded: a title, how it ended, the git commit and pull request, and attachments such as a trace, screenshots and a video.\n\nA session is not a run. It does not appear in run lists or in analytics, and it has no spec files or tests. Share one with `POST /share` and `sessionId`."}],"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":["runs: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. 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","shares:write":"Create public links to test results that anyone with the link can open until they expire"}}}}},"parameters":{"SessionIdPath":{"name":"sessionId","in":"path","required":true,"description":"The session ID","schema":{"type":"string"}}},"responses":{"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. Five 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`), an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`), and an API key on an endpoint that answers for a user (`user_credential_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` and `user_credential_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"}}}}},"schemas":{"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`."},"candidates":{"type":"array","description":"With `code: ambiguous_target`: the runs, spec files or tests the request matched. Send one of their IDs.","items":{"type":"object","properties":{"runId":{"type":"string"},"ciBuildId":{"type":"string"},"instanceId":{"type":"string"},"groupId":{"type":"string"},"spec":{"type":"string"},"machineId":{"type":"string","nullable":true},"testId":{"type":"string"},"title":{"type":"array","items":{"type":"string"}}}}}}}}},"paths":{"/sessions/{sessionId}":{"delete":{"summary":"Delete a session","description":"The session stops being readable at once. Its attachments, share links and share assets are removed in the background.","operationId":"deleteSession","tags":["Sessions"],"parameters":[{"$ref":"#/components/parameters/SessionIdPath"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["OK"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## GET /sessions/{sessionId}/attachments

> List the attachments of a session

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Sessions","description":"Browser sessions an agent recorded: a title, how it ended, the git commit and pull request, and attachments such as a trace, screenshots and a video.\n\nA session is not a run. It does not appear in run lists or in analytics, and it has no spec files or tests. Share one with `POST /share` and `sessionId`."}],"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. 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","shares:write":"Create public links to test results that anyone with the link can open until they expire"}}}}},"parameters":{"SessionIdPath":{"name":"sessionId","in":"path","required":true,"description":"The session ID","schema":{"type":"string"}}},"schemas":{"SessionAttachment":{"type":"object","required":["attachmentId","name","type","contentType","sizeBytes","caption","meta","createdAt","readUrl"],"properties":{"attachmentId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["trace","screenshot","video","attachment"]},"contentType":{"type":"string","nullable":true},"sizeBytes":{"type":"integer","nullable":true},"caption":{"type":"string","nullable":true},"meta":{"type":"object","additionalProperties":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"readUrl":{"type":"string","description":"Signed, valid for an hour; an attachment downloads under its own name."}}},"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`."},"candidates":{"type":"array","description":"With `code: ambiguous_target`: the runs, spec files or tests the request matched. Send one of their IDs.","items":{"type":"object","properties":{"runId":{"type":"string"},"ciBuildId":{"type":"string"},"instanceId":{"type":"string"},"groupId":{"type":"string"},"spec":{"type":"string"},"machineId":{"type":"string","nullable":true},"testId":{"type":"string"},"title":{"type":"array","items":{"type":"string"}}}}}}}},"responses":{"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. Five 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`), an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`), and an API key on an endpoint that answers for a user (`user_credential_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` and `user_credential_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":{"/sessions/{sessionId}/attachments":{"get":{"summary":"List the attachments of a session","operationId":"listSessionAttachments","tags":["Sessions"],"parameters":[{"$ref":"#/components/parameters/SessionIdPath"}],"responses":{"200":{"description":"The attachments, oldest first","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["OK"]},"data":{"type":"object","properties":{"attachments":{"type":"array","items":{"$ref":"#/components/schemas/SessionAttachment"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Add attachments to a session

> Signs an upload URL for each attachment. A session takes at most 200 attachments. The body takes no target fields such as \`instanceId\` or \`testId\`: a session has none. Besides a bearer token, this route accepts a record key in the \`x-currents-key\` header.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Sessions","description":"Browser sessions an agent recorded: a title, how it ended, the git commit and pull request, and attachments such as a trace, screenshots and a video.\n\nA session is not a run. It does not appear in run lists or in analytics, and it has no spec files or tests. Share one with `POST /share` and `sessionId`."}],"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":["runs:write"]},{"RecordKey":[]}],"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. 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","shares:write":"Create public links to test results that anyone with the link can open until they expire"}}}},"RecordKey":{"type":"apiKey","in":"header","name":"x-currents-key","description":"An organization's record key, the one a reporter sends. Accepted only by the two attachment routes under `/runs`, `POST /sessions`, `POST /sessions/{sessionId}/attachments` and, for a session, `POST /share`, so a CI job can attach files, record a session and share it with the key it already has."}},"parameters":{"SessionIdPath":{"name":"sessionId","in":"path","required":true,"description":"The session ID","schema":{"type":"string"}}},"schemas":{"AttachmentRequest":{"type":"object","required":["name","type","contentType","sizeBytes"],"properties":{"name":{"type":"string","minLength":1,"maxLength":1024},"type":{"type":"string","enum":["trace","screenshot","video","attachment"],"description":"Allowed per level: attachment on a run, spec file and test; screenshot, video and attachment on a spec file; trace, screenshot, video and attachment on an attempt."},"contentType":{"type":"string","description":"A trace is application/zip, a screenshot is image/png, image/jpeg, image/webp or image/gif, a video is video/*."},"sizeBytes":{"type":"integer","minimum":1,"maximum":1073741824,"description":"Signed into the upload URL, so storage refuses another size. Organizations that store files in their own S3 bucket behind CloudFront get an upload URL without this check."},"caption":{"type":"string","maxLength":1000},"meta":{"type":"object","additionalProperties":{"type":"string","maxLength":256},"description":"At most 20 keys of letters, digits, \"_\" and \"-\", and 2 KB in total."}}},"SessionAttachmentsResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"type":"object","required":["sessionId","attachments","uploadExpiresInSeconds"],"properties":{"sessionId":{"type":"string"},"uploadExpiresInSeconds":{"type":"integer"},"attachments":{"type":"array","description":"In the order of the request","items":{"type":"object","required":["attachmentId","name","type","uploadUrl","uploadHeaders"],"properties":{"attachmentId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"uploadUrl":{"type":"string"},"uploadHeaders":{"type":"object","additionalProperties":{"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`."},"candidates":{"type":"array","description":"With `code: ambiguous_target`: the runs, spec files or tests the request matched. Send one of their IDs.","items":{"type":"object","properties":{"runId":{"type":"string"},"ciBuildId":{"type":"string"},"instanceId":{"type":"string"},"groupId":{"type":"string"},"spec":{"type":"string"},"machineId":{"type":"string","nullable":true},"testId":{"type":"string"},"title":{"type":"array","items":{"type":"string"}}}}}}}},"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. Five 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`), an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`), and an API key on an endpoint that answers for a user (`user_credential_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` and `user_credential_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":{"/sessions/{sessionId}/attachments":{"post":{"summary":"Add attachments to a session","description":"Signs an upload URL for each attachment. A session takes at most 200 attachments. The body takes no target fields such as `instanceId` or `testId`: a session has none. Besides a bearer token, this route accepts a record key in the `x-currents-key` header.","operationId":"addSessionAttachments","tags":["Sessions"],"parameters":[{"$ref":"#/components/parameters/SessionIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["attachments"],"additionalProperties":false,"properties":{"attachments":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/AttachmentRequest"}}}}}}},"responses":{"200":{"description":"Upload URLs, in the order of the request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionAttachmentsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"Too many requests for the organization (`rate_limited`)"}}}}}}
```

## List the sessions of a project

> Newest first. Filters match as they do on the run list; \`search\` looks at the title, branch, commit message and pull request.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Sessions","description":"Browser sessions an agent recorded: a title, how it ended, the git commit and pull request, and attachments such as a trace, screenshots and a video.\n\nA session is not a run. It does not appear in run lists or in analytics, and it has no spec files or tests. Share one with `POST /share` and `sessionId`."}],"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. 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","shares:write":"Create public links to test results that anyone with the link can open until they expire"}}}}},"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"}},"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},"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},"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"}},"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"}}},"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}]},"SessionListItem":{"type":"object","properties":{"sessionId":{"type":"string"},"projectId":{"type":"string"},"title":{"type":"string"},"status":{"type":"string","enum":["passed","failed"]},"error":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"durationMs":{"type":"integer"},"git":{"type":"object","properties":{"sha":{"type":"string","nullable":true},"branch":{"type":"string","nullable":true},"message":{"type":"string","nullable":true},"authorName":{"type":"string","nullable":true},"authorEmail":{"type":"string","nullable":true},"remoteOrigin":{"type":"string","nullable":true}}},"pr":{"type":"object","nullable":true,"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}}},"attachmentCount":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"cursor":{"type":"string","description":"Pass as `starting_after` or `ending_before`"}}},"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`."},"candidates":{"type":"array","description":"With `code: ambiguous_target`: the runs, spec files or tests the request matched. Send one of their IDs.","items":{"type":"object","properties":{"runId":{"type":"string"},"ciBuildId":{"type":"string"},"instanceId":{"type":"string"},"groupId":{"type":"string"},"spec":{"type":"string"},"machineId":{"type":"string","nullable":true},"testId":{"type":"string"},"title":{"type":"array","items":{"type":"string"}}}}}}}},"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. Five 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`), an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`), and an API key on an endpoint that answers for a user (`user_credential_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` and `user_credential_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}/sessions":{"get":{"summary":"List the sessions of a project","description":"Newest first. Filters match as they do on the run list; `search` looks at the title, branch, commit message and pull request.","operationId":"getProjectSessions","tags":["Sessions"],"parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/StartingAfterQuery"},{"$ref":"#/components/parameters/EndingBeforeQuery"},{"$ref":"#/components/parameters/BranchesQuery"},{"$ref":"#/components/parameters/TagsQuery"},{"$ref":"#/components/parameters/TagOperatorQuery"},{"$ref":"#/components/parameters/RunFeedPrIdQuery"},{"name":"search","in":"query","required":false,"description":"Case-insensitive substring search over a session's title, branch, commit message, and pull request id and title. Printable ASCII only; leading and trailing whitespace is trimmed, and the trimmed value must be 1–200 characters.","schema":{"type":"string","pattern":"^[ -~]+$"}},{"$ref":"#/components/parameters/OptionalDateStartQuery"},{"$ref":"#/components/parameters/OptionalDateEndQuery"}],"responses":{"200":{"description":"A page of sessions, without their attachments","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["OK"]},"has_more":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/SessionListItem"}}}}}}},"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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
