# Projects API reference

> Canonical HTML: https://screenrig.ai/docs/api/projects
> Source SHA-256: e0c997a2ab813502f0d99a41d2ed1d3d6b26dbaf1a3d7e514c1cce0278ff8422

Projects, invitations, and sign-in. This group has 147 operations, each with its authentication, parameters, request body fields, request template and responses. The [API overview](https://screenrig.ai/docs/api.md) covers services, authentication, requests and retries, problem codes and versioning.

Replace uppercase placeholders. Save a schema-valid JSON body in request.json when required. Cookie examples use a local cookie jar from an authenticated session.

## Projects, invitations, and sign-in

### GET /api/project/deletion-preview

**Preview Project Deletion Agent**

Returns current confirmation details, deletion blockers and archived devices that will be disconnected. allowed is false, with a reason, when the caller may not delete the project (only its owner or an administrator of its organization may; for an agent, its responsible person) or a blocker remains.


Operation: previewProjectDeletionAgent.

- Authentication: projectBearer OR dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| project_id | query | optional | Expected project ID; a changed selection is refused. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/project/deletion-preview'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectDeletionPreview (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PUT /api/project/owner

**Transfer Project Owner Agent**

The owner transfers ownership to a verified existing project member; this grants no new membership. The new owner's membership leaves any screen_scope and upload_scope, so the new owner reaches the whole project.

A dashboard session of an admin of the project's organization who is a member transfers ownership the same way, from whoever owns the project; an organization admin who is not a member uses PUT /api/organizations/{org_id}/projects/{project_id}/owner.


Operation: transferProjectOwnerAgent.

- Authentication: projectBearer OR dashboardSession
- Request body: ProjectOwnerTransfer

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Expected project ID. The server refuses the action if the current credential or cookie session targets another project; browser clients send the page's project ID. minLength: 1 maxLength: 200 |
| user_id | required | string | minLength: 1 maxLength: 200 |

#### Request template

~~~sh
curl --request PUT --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/project/owner'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectContext (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/project/move

**Move Project Agent**

Owner-authorized move transfers screen occupancy and new usage to the destination payer atomically; historical charges keep their payer. A destination already holding as many live projects as its plan allows is project_limit_reached, whose required_plan names the plan with room, absent at Enterprise.


Operation: moveProjectAgent.

- Authentication: projectBearer OR dashboardSession
- Request body: ProjectMove

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Expected project ID. The server refuses the action if the current credential or cookie session targets another project; browser clients send the page's project ID. minLength: 1 maxLength: 200 |
| organization_id | required | string | minLength: 1 maxLength: 200 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/project/move'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectContext (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/project/moves

**Allowed Project Moves Agent**

Lists destinations the owner administers or whose verified domain matches their verified email.


Operation: allowedProjectMovesAgent.

- Authentication: projectBearer OR dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| project_id | query | optional | Expected project ID; a changed selection is refused. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/project/moves'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectMoveDestinations (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects

**List Agent Projects**

Lists every active membership of the identity, with organization and owner context, including memberships omitted from this token or its grant's consent. Each entry marks in_token; discovery adds no authority and has no 64-project cap.


Operation: listAgentProjects.

- Authentication: agentIdentityBearer OR dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/projects'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectContextList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects

**Create Agent Project**

Creates a named project owned by the verified responsible person. With no organization named, it goes to the person's verified-domain organization, else the current project's organization, else the organization the person used most recently among those they administer or hold as their free one, else a new organization.

An organization name selects exactly one visible match, otherwise creates a new organization. A person holds one free Standard organization; a new organization beyond it is free_organization_limit, naming the one they hold.

An organization holds at most as many live projects as its plan allows (Standard 1, Plus 3, Premium 10, Education 50, Enterprise 100, widened by its featuresets); one more is project_limit_reached, whose required_plan names the plan with room, absent at Enterprise (contact support).

An exact idempotent retry redelivers its sealed membership credential for ten minutes. Enrollment is exempt from the limit.

Under an agent access JWT (identity scope, access:manage, a grant with identity access Manage) no project token is delivered; the project joins the grant with Manage and every capability, and a refresh with its project_id returns a token for it.


Operation: createAgentProject.

- Authentication: agentIdentityBearer OR dashboardSession
- Request body: ProjectCreate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |
| organization_id | optional | string | minLength: 1 maxLength: 200 |
| organization_name | optional | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |

#### Request template

~~~sh
curl --request POST --cookie cookies.txt --cookie-jar cookies.txt --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/projects'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Project lifecycle result | CreatedProject (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/invitations

**List Invitations**

Lists invitation lifecycle records for the current project, excluding sign-in resets. Recipient addresses are visible to the owning project, which supplied them. Tokens and credential URLs are never listed.


Operation: listInvitations.

- Authentication: projectBearer OR dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| kind | query | optional |  |
| status | query | optional |  |
| cursor | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/invitations'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Lists invitation lifecycle records for the current project, excluding sign-in resets. | InvitationList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/invitations

**Create Invitations**

Creates member or ad-buyer invitations. Email delivery never returns a credential. Link delivery is member-only and returns one URL, including on exact idempotent replay within 24 hours. Email invitations expire after seven days; links expire after 24 hours. Outstanding email invitations of the same kind, address, and project are reused.

A member limited to screens does not invite people (outside_screen_scope).

A project_member email invitation to a person who would be an external guest of the project's organization (it is not their home organization) is subject to that organization's guest policy, and guest_not_allowed (403) answers when the policy (allow, domains or none) does not admit the address. Accepting the invitation checks again.

Staff and the organization's own admins are exempt. A screen_scope on the invitation is screen scope, which the Enterprise plan includes; a project whose plan lacks it is refused with feature_not_in_plan (402).


Operation: createInvitations.

- Authentication: projectBearer OR dashboardSession
- Request body: InvitationCreate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| kind | required | string | Allowed: "project_member", "ad_buyer" |
| delivery | optional | string | Allowed: "email", "link" default: "email" |
| emails | optional | array |  |
| advertising | optional | InvitationAdvertising | Seller-owned scope: at least one screen or slot. Screens only, slots only, or both are accepted. |
| upload_scope | optional | UploadScope | What an uploader may use (docs/uploaders.md): the playlists it changes pages of and the media folders it uploads into. Folders are exactly the listed ones, never their subfolders. An id that names no live object of the project when written is invalid_request at its position; a later deletion leaves a dead id, which grants nothing. The scope is read on each request, so a change applies to the next one. |
| screen_scope | optional | ScreenScope | What a scoped member or agent may act on (docs/screen-scope.md): the screens in the listed screen folders and in every folder inside them, and the listed screens. At least one id in all. An id that names no live folder or screen of the project when written is invalid_request at its position; a later deletion leaves a dead id, which grants nothing. The scope is read on each request, so a change applies to the next one. Tags are never scope. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/invitations'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Creates member or ad-buyer invitations. | InvitationCreated (application/json); headers: RateLimit-Policy, RateLimit |
| 402 | feature_not_in_plan — a screen scope needs Enterprise, and an upload scope (an uploader) needs Plus; the problem names the feature, the plan the account is on and required_plan. Narrowing an upload scope and removing either scope are always allowed. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |

### POST /api/invitations/{id}/revoke

**Revoke Invitation**

Revokes an outstanding invitation. An accepted invitation returns invitation_consumed. A member limited to screens does not revoke invitations (outside_screen_scope).


Operation: revokeInvitation.

- Authentication: projectBearer OR dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| id | path | required |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/invitations/RESOURCE_ID/revoke'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 204 | Revokes an outstanding invitation. | no body |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/sign-in-resets

**Request Sign In Reset**

Accepts a sign-in reset request without revealing whether a person holds the address.

A person whose address is unverified, such as a self-serve signup, is found too, and completing that reset, which its owner must confirm with replace_unverified_account, gives the inbox's owner a new account (a new person with the reset's credential, a new organization and project) and deletes the unverified person, whom nobody had shown the address belonged to.

A reset link expires one hour after it was last requested. While a person's link is live and less than a day old, another request sends that same link again (at most once every ten minutes) and moves its expiry to one hour from the request, so a request never stops the link already in the inbox; otherwise a new link replaces it. There is no limit per address.

Completing a reset removes the person's other passkeys (every passkey for a password reset), removes the password when the new credential is a passkey, discards a pending email change and ends every session. With or without a person, the request also re-sends at most ten invitations addressed to the email, and any one invitation at most once an hour.

Of the ten most recent non-deleted projects whose contact address matches, each active project with a catalog plan and ownership still pending from enrollment gets its enrollment invitation back, even when it expired or its recipient already joined as an ordinary member.

Accepting that enrollment invitation with the existing sign-in completes the ownership claim; requesting recovery alone changes no ownership or credentials. Pending emailed member and ad-buyer invitations to active projects with a catalog plan, and pending organization admin invitations to live organizations, fill the rest, newest first.

A re-sent invitation keeps its ID and gets a new link and expiry; the old link stops working and the project's outstanding invitation count is unchanged. An expired enrollment returns as a new invitation that takes an invitation slot again, so the count rises by one, and it is skipped when the project is full.

Other expired invitations, and revoked, accepted and link invitations, are not re-sent.


Operation: requestSignInReset.

- Authentication: none
- Request body: SignInResetRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| email | required | string | minLength: 3 maxLength: 254 format: "email" |

#### Request template

~~~sh
curl --request POST --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/sign-in-resets'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 202 | Accepts a sign-in reset request without revealing whether a person holds the address. | SignInResetAccepted (application/json); headers: RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |

### POST /api/enrollments

**Enroll CLI**

Creates a new project and its first agent through explicit bundled-CLI enrollment, even when the contact address already belongs to a person or project. The contact email stays unverified and grants no authority; malformed addresses return invalid_request.

Queues an enrollment invitation in the same transaction. project_name is optional; omission uses a readable two-word name. Exact idempotent replay precedes rate limits. Persist the permanent credential before reporting success; its replay envelope expires at issuance_expires_at and later retries return credential_issuance_expired.

The idempotency record lasts 24 hours.


Operation: enrollCLI.

- Authentication: none
- Request body: CLIEnrollmentRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| credential_format | optional | string | Must be oauth, which answers the agent JWT pair (CLIEnrollmentOAuth). legacy, or omitting it, answers 410 client_update_required, because the legacy permanent bearers are retired. A server that is not issuing agent JWTs answers not_ready. Allowed: "oauth", "legacy" |
| intent | optional | string | Optional enrollment purpose. Omit or signage keeps screens=true, advertiser=false. advertising sets advertiser=true, screens=false, and never changes the billing-plan assignment. Allowed: "signage", "advertising" |
| client_id | required | string | Stable anonymous installation identity generated from 32 CSPRNG bytes and persisted before the first request. It is rate-limit material, not a project credential. pattern: "^cli_[A-Za-z0-9_-]{43}$" |
| email | optional | string | Unverified project contact address. Exactly one of email or agentid_claim is required. Enrollment always creates a new project and queues its invitation; an address alone grants no authority. minLength: 3 maxLength: 254 format: "email" |
| agentid_claim | optional | string | Single-use AgentID claim code from the AgentID sign-in page. Exactly one of email or agentid_claim is required. The verified AgentID owner address becomes the project contact and invitation recipient. pattern: "^agid_[A-Za-z0-9_-]{43}$" |
| beta_key | optional | string | Required when the server has a configured enrollment beta key. Omit when the server does not gate enrollment. |
| name | optional | string | default: "Agent" minLength: 1 maxLength: 80 |
| agent_type | optional | string | default: "cli" pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$" |
| platform | optional | string | maxLength: 80 |
| version | optional | string | maxLength: 40 |
| install_source | optional | string | Optional. Where the integration was installed from, the host and, after a slash, the marketplace (claude/screenrig, codex/screenrig, grok/screenrig), or a bare host such as npm. Software reports it, not a person, so a missing or wrongly shaped value is ignored and never fails enrollment. It only counts where installs came from and grants nothing. pattern: "^[a-z][a-z0-9-]{0,31}(/[a-z0-9][a-z0-9._-]{0,63})?$" |
| project_name | optional | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |
| setup_code | optional | string | Optional setup code the person copied from the website with the setup prompt (two groups of four letters and digits, case and hyphen ignored). It links the new project to that website visit; an unknown or expired code is ignored and never fails enrollment. It grants nothing. pattern: "^[0-9A-Za-z]{4}-?[0-9A-Za-z]{4}$" |
| organization | optional | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |

#### Request template

~~~sh
curl --request POST --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/enrollments'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | New project and permanent bearer credential. The project carries the unverified contact address in its trimmed supplied form. Persist and verify the token before reporting enrollment success. The credential has no scheduled expiry but its encrypted exact-retry delivery envelope exists only until issuance_expires_at. | CLIEnrollmentOAuth (application/json); headers: Cache-Control, RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 410 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| 500 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /agentid/start

**Start Agent IDSign In**

Starts an AgentID (OIDC) browser sign-in on the API host derived from the runtime public origin (https://play.<domain> -> https://api.<domain>). Sets a single-use binding cookie and redirects to the AgentID authorization endpoint with PKCE S256. Answers only on the API host.

While the relying party has no client id and secret the route renders a plain unavailable page with 503.


Operation: startAgentIDSignIn.

- Authentication: none
- Request body: none

#### Request template

~~~sh
curl --request GET 'https://api.screenrig.ai/agentid/start'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 302 | Redirect to the AgentID authorization endpoint. | no body |
| 503 | AgentID sign-in is unavailable. | string (text/html) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /agentid/callback

**Complete Agent IDSign In**

Completes an AgentID browser sign-in on the API host. Requires iss equal to the configured issuer, the binding cookie, and an unexpired single-use transaction.

Exchanges the code with client_secret_basic and PKCE, verifies the ES256 id_token (iss, aud, exp with 60 s skew, nonce) against the cached JWKS, calls userinfo and enforces the owner rules, then renders a single-use claim-code page. A subject already bound to an agent renders the already-enrolled page.

This route never creates a project, identity, or invitation.


Operation: completeAgentIDSignIn.

- Authentication: none
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| code | query | optional |  |
| state | query | optional |  |
| iss | query | optional |  |
| error | query | optional |  |

#### Request template

~~~sh
curl --request GET 'https://api.screenrig.ai/agentid/callback'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Single-use AgentID claim-code page with plugin install steps. | string (text/html) |
| 409 | The AgentID subject is already enrolled. | string (text/html) |
| 503 | AgentID sign-in is unavailable. | string (text/html) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/project

**Get Project**

The calling project. Does not debit the 1-credit API meter, so a zero-remaining project can still read remaining and recover. After a successful project principal, authenticated /api JSON and problem responses include ScreenRig-Credits-Remaining, ScreenRig-Credits-Reset, and ScreenRig-Credits-Included as whole credits.

SSE, runtime, content, and unauthenticated responses omit them.


Operation: getProject.

- Authentication: projectBearer OR dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/project'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project | Project (application/json); headers: ScreenRig-Credits-Remaining, ScreenRig-Credits-Reset, ScreenRig-Credits-Included, ScreenRig-Credits-Low |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PATCH /api/project

**Update Project**

Renames the current project. Names contain 1–60 printable characters and no URL-looking text.


Operation: updateProject.

- Authentication: projectBearer OR dashboardSession
- Request body: ProjectPatch

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Expected project ID. The server refuses the action if the current credential or cookie session targets another project; browser clients send the page's project ID. minLength: 1 maxLength: 200 |
| name | required | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |

#### Request template

~~~sh
curl --request PATCH --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/project'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Renames the current project. | Project (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### DELETE /api/project

**Delete Project Agent**

Only the project's owner or an administrator of its organization may delete it; any other caller is refused with forbidden. A person must also be a member of the project.

An agent needs the target membership project capability and deletes for its responsible person, who must be the owner or an organization administrator; an agent without a responsible person cannot delete, except the agent that enrolled the project while no person has accepted its enrollment.

Also requires the current name and revision, zero non-archived screens, no credit, reservations or debt in its own wallet and no live advertising. A negative balance run up under free or launch terms does not block; deletion writes it off with a project_delete_writeoff ledger entry. Queues member notices and uses the irreversible tombstone.


Operation: deleteProjectAgent.

- Authentication: projectBearer OR dashboardSession
- Request body: ProjectDeleteConfirmation

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Expected project ID. The server refuses the action if the current credential or cookie session targets another project; browser clients send the page's project ID. minLength: 1 maxLength: 200 |
| empty_only | optional | boolean | Require an empty Screens project in the deletion transaction, including content, other agents or members, work and correspondence. The CLI additionally requires its saved enrollment provenance before using this cleanup guard. Omission keeps explicit member deletion. |
| name | required | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |
| revision | required | integer | minimum: 1 |

#### Request template

~~~sh
curl --request DELETE --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/project'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | DeletedProject (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/project/capabilities

**Get Project Capabilities**

The calling project's plan, independent advertiser/screens flags with their revision, the derived effective capability set, the plan features it has (plan_features) and its limits (limits). Never reflects a client claim; an unknown plan fails closed as forbidden.


Operation: getProjectCapabilities.

- Authentication: projectBearer OR dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/project/capabilities'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project capabilities. | ProjectCapabilities (application/json); headers: Cache-Control |
| 403 | forbidden - the project plan is not in the catalog. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/audit

**List Organization Audit**

The organization's own audit log: changes to its administrators, domains and billing settings, with the same entries, filters and formats as GET /api/audit. Requires an administrator of the organization. Part of the audit log, which the Enterprise plan includes; an organization whose plan lacks it is refused with feature_not_in_plan (402).

Every plan records its changes.


Operation: listOrganizationAudit.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| actor | query | optional |  |
| actor_kind | query | optional |  |
| source | query | optional |  |
| action | query | optional |  |
| since | query | optional |  |
| until | query | optional |  |
| cursor | query | optional |  |
| limit | query | optional |  |
| format | query | optional | json (default) returns the report as JSON. csv, xlsx and pdf return the same rows as a file attachment (every page of a paged report, up to 50000 rows). |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/audit'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One page of entries, or the filtered log as a file. | AuditList (application/json), string (text/csv), string (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet), string (application/pdf) |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — The audit log is part of the Enterprise plan; the problem names the feature, the plan the account is on and required_plan. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/notifications

**List Organization Notifications**

The organization's notifications, of every channel, oldest first (at most 50).

A notification belongs to the organization and covers every project of it, including projects added later (project_ids absent), or the chosen projects (project_ids); it acts on the screens, sources, campaigns and cards of those projects exactly as a project notification does. status is failing while any covered project's deliveries are, and failing_since, last_success_at and last_failure_at fold the covered projects' (the earliest failure start, the latest success and failure).

A webhook's signing secret is never returned here. Requires an administrator of the organization; anyone else is not_found.


Operation: listOrganizationNotifications.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/notifications'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The organization's notifications. | OrganizationNotificationList (application/json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/organizations/{org_id}/notifications

**Create Organization Notification**

Creates a notification of the organization, with the members of POST /api/notifications and the projects it covers: project_ids absent or null covers every project of the organization (those it has and those it gets), a list covers exactly those, each a live project of this organization (invalid_request otherwise).

An email notification's recipients are owner (each covered project's owner), or user_ids of people of the organization (its admins and home members) or members of a covered project; each project still mails only the people who are members of it.

An organization has at most 50 notifications, not counting the defaults the system makes (notification_limit_reached). The same plan features gate the channels, rules and conditions as on the project routes, judged on the organization's plan. Each covered project gets notification.created, and the change is recorded in the organization's audit log.

A webhook's signing secret is returned only in this answer and in rotate-secret; one secret signs every covered project's deliveries. An exact retry with the same Idempotency-Key returns this same answer, secret included, for twenty-four hours.


Operation: createOrganizationNotification.

- Authentication: dashboardSession
- Request body: OrganizationNotificationWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| channel | required | string | Allowed: "email", "webhook", "slack", "teams", "pagerduty", "telegram" |
| description | optional | string | A name for people. maxLength: 200 |
| conditions | optional | NotificationConditions |  |
| screen_tags | optional | ScreenTags | Fleet selector tags, 0 to 16 unique exact tags, each matching the media tag grammar. On ScreenPatch the array replaces the whole set and an empty array clears it; a duplicate or malformed tag is invalid_request. Changing tags bumps the screen revision (If-Match guards it) and appends screen.updated with details.tags; it never changes the manifest revision. Tags are never on the runtime manifest and never authorization. |
| audience | optional | CampaignAudience | Which screens a campaign targets: exactly one of all (every rule matches), any (at least one matches), tag (the screen carries the tag), screen_id (that screen) or folder (the screens in that screen folder and every folder inside it, GET /api/screen-folders; the folder must exist). Rules nest at most 3 deep and an audience has at most 500 rules. {"all": []} is every screen of the project; elsewhere all and any list at least one rule. |
| min_severity | optional | NotificationSeverity | The severity of a notice, least severe first. An opened notice has its condition's severity: error for screen_offline and warning for every other condition, unless the notification's severity says otherwise. A resolved notice and a test are info. Allowed: "info", "warning", "error" |
| severity | optional | NotificationSeverityOverrides | Condition to severity for this notification, such as {"not_playing": "error"}. It counts for min_severity and is the severity Teams and PagerDuty show for the condition. A condition not listed keeps its default. On a patch an empty object, like null, clears every override. |
| active_hours | optional | NotificationActiveHours | When opened notices go out: the windows (screenrig.schedule/v1), read in timezone. A notice that falls due while they are closed waits for the next opening (outside hold, the default) and goes out then if its condition still holds, so a condition that clears first sends nothing, or is skipped (outside drop; last_error outside_hours). A resolved notice for an opened notice that was sent always goes out, inside the hours or not, and a test ignores them. crashing is held or dropped like any other condition. The hours judge a notice when it first goes out; a failed one is retried on the usual schedule whatever the hours. Adding, changing or removing the hours judges the notices held for the old ones again at once. |
| reach_drop | optional | NotificationReachDrop | The smallest drop in a campaign's reach a reach_dropped notice is sent for: screens is the fewest screens lost (default 1) and percent, when set, the smallest share of the screens the campaign reached when it was published; a drop must meet both. A smaller drop queues no notice, and a queued notice whose drop shrank below it by the time it is due is skipped (condition_cleared). Only with the reach_dropped condition; on a notification without it, invalid_request. |
| hold_minutes | optional | integer | How long a condition must last before it notifies. crashing is never held. default: 10 minimum: 0 maximum: 1440 |
| recipients | optional | NotificationRecipients | Email only. owner (whoever owns the project when a notice sends) or the user_id of a person who is a member of the project; a user_id that is not a member is invalid_request. Each is emailed at their verified address. One who has since left the project or has no verified address is skipped. |
| url | optional | string | Webhook only: https URL on a public Internet host, port 443 (the default) or 8443. No userinfo or fragment. maxLength: 2048 format: "uri" |
| event_types | optional | NotificationEventTypes | Webhook only. Project events also POSTed as they happen, exactly as a webhook sends them: exact types such as screen.online, or prefixes ending in .* such as screen.*. notification.test is never fanned out. |
| destination | optional | NotificationDestination | Slack, Teams, PagerDuty and Telegram only; required for them. The credential in the shape the service issues it: slack, the incoming webhook URL (hooks.slack.com); teams, the Workflows webhook URL (logic.azure.com or powerplatform.com; Office 365 connector URLs are retired); pagerduty, the 32-character Events API v2 routing key; telegram, the bot token. Another shape is notification_destination_rejected. It is sealed, never returned, and appears on reads only as destination_hint. minLength: 1 maxLength: 2048 |
| telegram_chat_id | optional | NotificationTelegramChat | Telegram only; required for it. The chat to post to: its numeric id (negative for a group or channel) or @channelname. Not a credential. pattern: "^(-?[0-9]{1,20}\|@[A-Za-z][A-Za-z0-9_]{4,31})$" |
| enabled | optional | boolean | default: true |
| project_ids | optional | inline | The projects the notification covers; null or absent covers every project of the organization. |

#### Request template

~~~sh
curl --request POST --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/organizations/ORG_ID/notifications'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. A webhook's secret is shown only here and on rotate-secret. | Notification (application/json); headers: Cache-Control |
| 400 | invalid_request, webhook_url_rejected or notification_destination_rejected. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — as on POST /api/notifications, for the organization's plan. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | notification_limit_reached or idempotency_mismatch. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/notifications/{id}

**Get Organization Notification**

One notification of the organization, as GET /api/organizations/{org_id}/notifications shows it.


Operation: getOrganizationNotification.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| id | path | required |  |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/notifications/RESOURCE_ID'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One notification. | Notification (application/json); headers: ETag |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PATCH /api/organizations/{org_id}/notifications/{id}

**Update Organization Notification**

Changes any member PATCH /api/notifications/{id} changes, in the notification and in every covered project, and project_ids: absent leaves the projects, null covers every project, a list covers exactly those. A project that gains the notification starts at its current event (nothing earlier is sent); one that loses it has the notification deleted.

Each covered project gets notification.updated, and the change is recorded in the organization's audit log.

A change that makes the notification cover a project it does not cover now is gated like a create of the notification as it will be (its channel, rules and glass conditions, on the organization's plan); covering fewer projects is never gated, and switching a list to every project always covers more. If-Match guards the revision.


Operation: updateOrganizationNotification.

- Authentication: dashboardSession
- Request body: OrganizationNotificationPatch

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| id | path | required |  |
| If-Match | header | optional | Optional positive resource revision. Omit to write the current resource; a supplied stale revision is revision_conflict. |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| description | optional | string | An empty string clears the description. maxLength: 200 |
| conditions | optional | NotificationConditions |  |
| screen_tags | optional | ScreenTags | Fleet selector tags, 0 to 16 unique exact tags, each matching the media tag grammar. On ScreenPatch the array replaces the whole set and an empty array clears it; a duplicate or malformed tag is invalid_request. Changing tags bumps the screen revision (If-Match guards it) and appends screen.updated with details.tags; it never changes the manifest revision. Tags are never on the runtime manifest and never authorization. |
| audience | optional | inline | As on NotificationPatch. |
| min_severity | optional | inline | As on NotificationPatch. |
| severity | optional | inline | Replaces every override; null or an empty object clears them. |
| active_hours | optional | inline | Replaces the hours; null clears them. |
| reach_drop | optional | inline | Replaces the threshold; null clears it. |
| hold_minutes | optional | integer | minimum: 0 maximum: 1440 |
| recipients | optional | NotificationRecipients | Email only. owner (whoever owns the project when a notice sends) or the user_id of a person who is a member of the project; a user_id that is not a member is invalid_request. Each is emailed at their verified address. One who has since left the project or has no verified address is skipped. |
| url | optional | string | maxLength: 2048 format: "uri" |
| event_types | optional | NotificationEventTypes | Webhook only. Project events also POSTed as they happen, exactly as a webhook sends them: exact types such as screen.online, or prefixes ending in .* such as screen.*. notification.test is never fanned out. |
| destination | optional | NotificationDestination | Slack, Teams, PagerDuty and Telegram only; required for them. The credential in the shape the service issues it: slack, the incoming webhook URL (hooks.slack.com); teams, the Workflows webhook URL (logic.azure.com or powerplatform.com; Office 365 connector URLs are retired); pagerduty, the 32-character Events API v2 routing key; telegram, the bot token. Another shape is notification_destination_rejected. It is sealed, never returned, and appears on reads only as destination_hint. minLength: 1 maxLength: 2048 |
| telegram_chat_id | optional | NotificationTelegramChat | Telegram only; required for it. The chat to post to: its numeric id (negative for a group or channel) or @channelname. Not a credential. pattern: "^(-?[0-9]{1,20}\|@[A-Za-z][A-Za-z0-9_]{4,31})$" |
| enabled | optional | boolean |  |
| snoozed_until | optional | inline | Send nothing until this instant, at most 30 days ahead; null ends the snooze. |
| project_ids | optional | inline | Absent leaves the projects the notification covers, null covers every project of the organization, a list covers exactly those projects. |

#### Request template

~~~sh
curl --request PATCH --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/organizations/ORG_ID/notifications/RESOURCE_ID'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Updated notification. | Notification (application/json); headers: ETag |
| 400 | invalid_request, webhook_url_rejected or notification_destination_rejected. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — as on PATCH /api/notifications/{id}, for the organization's plan. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 412 | revision_conflict — If-Match is stale; the error's current_revision carries the current revision. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### DELETE /api/organizations/{org_id}/notifications/{id}

**Delete Organization Notification**

Deletes the notification from every project it covers, with notification.deleted in each, and records it in the organization's audit log. Pending notices are failed with webhook_deleted.


Operation: deleteOrganizationNotification.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| id | path | required |  |
| If-Match | header | optional | Optional positive resource revision. Omit to write the current resource; a supplied stale revision is revision_conflict. |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request template

~~~sh
curl --request DELETE --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/notifications/RESOURCE_ID'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 204 | Deleted. | no body; headers: Cache-Control |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 412 | revision_conflict — If-Match is stale; the error's current_revision carries the current revision. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/organizations/{org_id}/notifications/{id}/rotate-secret

**Rotate Organization Notification Secret**

Webhook notifications only (any other channel is invalid_request). Replaces the signing secret in every covered project at once, so one receiver keeps verifying one secret, and returns the notification with the new one in secret.

As on POST /api/notifications/{id}/rotate-secret, at most one attempt per project already in flight may still arrive signed with the old secret. An exact retry with the same Idempotency-Key returns the same new secret for twenty-four hours.


Operation: rotateOrganizationNotificationSecret.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| id | path | required |  |
| If-Match | header | optional | Optional positive resource revision. Omit to write the current resource; a supplied stale revision is revision_conflict. |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request template

~~~sh
curl --request POST --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/notifications/RESOURCE_ID/rotate-secret'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The notification and its new secret. | Notification (application/json); headers: Cache-Control |
| 400 | invalid_request — the notification is not a webhook. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 412 | revision_conflict — If-Match is stale; the error's current_revision carries the current revision. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/organizations/{org_id}/notifications/{id}/test

**Test Organization Notification**

Sends one notification.test through the first project the notification covers (by project id), as POST /api/notifications/{id}/test sends one through its project: one delivery, whose outcome the deliveries list shows, attempted once.

Every covered project has the same receiver, destination and secret, so one test proves the channel; the email of a test goes to the recipients of that project. A notification that covers no project is invalid_request.


Operation: testOrganizationNotification.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request template

~~~sh
curl --request POST --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/notifications/RESOURCE_ID/test'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 202 | The queued test delivery. | NotificationDelivery (application/json); headers: RateLimit-Policy, RateLimit |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/notifications/{id}/deliveries

**List Organization Notification Deliveries**

The delivery logs of every project the notification covers, as one list, newest first by when each row was queued. Each row names its project (project_id), and its id opens GET .../deliveries/{delivery_id}/attempts. Pass next_cursor back as before for the next page.


Operation: listOrganizationNotificationDeliveries.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| id | path | required |  |
| before | query | optional | next_cursor of the previous deliveries page. |
| limit | query | optional |  |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/notifications/RESOURCE_ID/deliveries'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One page of deliveries. | NotificationDeliveryList (application/json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/notifications/{id}/deliveries/{delivery_id}/attempts

**List Organization Notification Delivery Attempts**

One delivery's attempts, newest first, the most recent 20. A delivery that is not one of this notification's is not_found.


Operation: listOrganizationNotificationDeliveryAttempts.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| id | path | required |  |
| delivery_id | path | required | The id of one of the webhook's deliveries. |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/notifications/RESOURCE_ID/deliveries/DELIVERY_ID/attempts'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The delivery's kept attempts. | NotificationDeliveryAttemptList (application/json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### DELETE /api/organizations/{org_id}/notifications/{id}/unsubscribed/{user_id}

**Resubscribe Organization Notification User**

Mails an email notification's person again after they stopped it with the unsubscribe link in its mail, in every project the notification covers (the link stops them in all of them). Idempotent: a person who is not stopped changes nothing, and the answer is the notification as it is.

A change bumps the revision and appends notification.updated with details change resubscribed, user_id and via api to each covered project.


Operation: resubscribeOrganizationNotificationUser.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| id | path | required |  |
| user_id | path | required | The user_id of a person in the notification's unsubscribed list. |

#### Request template

~~~sh
curl --request DELETE --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/notifications/RESOURCE_ID/unsubscribed/USER_ID'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The notification, without the person in unsubscribed. | Notification (application/json); headers: ETag |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/reports

**List Report Kinds In Project**

Lists the report kinds GET /api/reports/{kind} builds, with the filters each takes, and the formats a report is written in. Needs the reports capability.


Operation: listReportKindsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/reports'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The report kinds. | ReportCatalog (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/report-subscriptions

**List Report Subscriptions In Project**

Lists the project's scheduled reports, oldest first (at most 50), each with its next run and the outcome of its last. Requires the project capability.


Operation: listReportSubscriptionsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/report-subscriptions'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's report subscriptions. | ReportSubscriptionList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/report-subscriptions

**Create Report Subscription In Project**

Schedules a report to be emailed, as a PDF, XLSX or CSV attachment, daily, weekly or monthly at an hour in a time zone.

Each run covers the period before it in UTC days: yesterday, the seven days before, or last month. recipients are members of the project, named by user_id or by the email address of the member; anyone else is refused, so a subscription is never a way to mail an outsider, and a member who leaves stops receiving it.

A failed run is retried and shows as last_status failed with last_error. At most 50 per organization across its projects (report_subscription_limit_reached).


Operation: createReportSubscriptionInProject.

- Authentication: projectBearer
- Request body: ReportSubscriptionWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | string | minLength: 1 maxLength: 100 |
| kind | required | string | Allowed: "fleet_health", "uptime", "publishing", "alerts", "alert_log", "proof_of_play" |
| filters | optional | object | The report's filters other than the days (GET /api/reports lists them). |
| schedule | required | ReportSchedule |  |
| format | required | string | Allowed: "pdf", "xlsx", "csv" |
| recipients | required | array | user_id or email address of project members. |
| enabled | optional | boolean | default: true |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/report-subscriptions'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. | ReportSubscription (application/json); headers: ETag |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — Reports are part of Plus. The problem names the feature, the project's plan and required_plan. | Problem (application/problem+json) |
| 409 | report_subscription_limit_reached. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/cards

**List Cards In Project**


Operation: listCardsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| project | query | optional |  |
| after | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/cards'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Card resource | object (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/cards

**Create Project Card**


Operation: createProjectCard.

- Authentication: projectBearer
- Request body: CardCreate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| type | required | string |  |
| major | optional | integer | minimum: 1 |
| params | required | object |  |
| data | optional | object |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/cards'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Card resource | CardInstance (application/json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/organizations/{org_id}/billing/portal-sessions

**Create Organization Portal**

Requires a designated admin of a live organization, exact same-origin cookie authentication and JWT CSRF. Actor and return URL are server-owned. Response is private and no-store. Open to an organization delegated to a reseller too, for its card and past invoices.


Operation: createOrganizationPortal.

- Authentication: dashboardSession
- Request body: EmptyPaymentRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |

#### Request template

~~~sh
curl --request POST --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/organizations/ORG_ID/billing/portal-sessions'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Billing management result. | BillingPortalSession (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PUT /api/organizations/{org_id}/billing/auto-top-up

**Set Organization Auto Top Up**

Requires a designated admin of a live organization, exact same-origin cookie authentication and JWT CSRF. The actor is server-owned. Disabling preserves saved amounts and monthly spend; amount fields are ignored when disabled. Response is private and no-store. Changing settings clears a recoverable pause only after any failed payable invoice is safely voided.

Enabling it for an organization delegated to a reseller answers 409 billed_by_reseller; disabling stays open.


Operation: setOrganizationAutoTopUp.

- Authentication: dashboardSession
- Request body: AutoTopUpSettings

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |

#### Request template

~~~sh
curl --request PUT --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/organizations/ORG_ID/billing/auto-top-up'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Billing management result. | AutoTopUpView (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/billing/payments

**Get Organization Payments**

Payment projections for a live organization. Requires its designated admin cookie; staff visibility alone grants no purchase access. Unconfigured payments return available false. Never grants credit from a browser return.


Operation: getOrganizationPayments.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/billing/payments'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Payment availability and recent purchases. | OrganizationPayments (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/organizations/{org_id}/billing/credit-checkouts

**Create Organization Checkout**

Start a prepaid purchase for a live organization designated admin. Requires same-origin cookie authentication and, for JWT sessions, a matching CSRF header. Organization, customer, actor and return URLs are server-owned. Credit applies only after the billing service confirms payment. Retry ambiguous responses with the same Idempotency-Key and amount.

An organization delegated to a reseller answers 409 billed_by_reseller.


Operation: createOrganizationCheckout.

- Authentication: dashboardSession
- Request body: object

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| amount_cents | required | integer | minimum: 10000 maximum: 1000000 format: "int64" |

#### Request template

~~~sh
curl --request POST --cookie cookies.txt --cookie-jar cookies.txt --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/organizations/ORG_ID/billing/credit-checkouts'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Purchase created or replayed. Checkout URL is absent after completion. Responses are private and no-store. | CreditPurchase (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/billing/balance

**Get Organization Billing Balance**

The organization wallet. Requires a designated organization admin, an admin of the reseller the organization is delegated to, or a staff session. Grants no project membership.


Operation: getOrganizationBillingBalance.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/billing/balance'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Organization billing balance. | BillingBalance (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/billing/statement

**Get Organization Billing Statement**

Journal entries attributed to this organization at charge time, with project and payer identities. Moves do not transfer historical access. Entries predating recorded organization context remain in their project statements. Read by the same people as the balance (getOrganizationBillingBalance).


Operation: getOrganizationBillingStatement.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| cursor | query | optional |  |
| limit | query | optional |  |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/billing/statement'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Organization billing statement. | BillingStatement (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations/{org_id}/billing/usage

**Get Organization Billing Usage**

The organization's usage pool and each project's share for a UTC month. Covers usage paid by this organization; other payer costs are in the organization statement. Read by the same people as the balance (getOrganizationBillingBalance).


Operation: getOrganizationBillingUsage.

- Authentication: dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |
| cursor | query | optional |  |
| limit | query | optional |  |
| period | query | optional | UTC month in YYYY-MM form; defaults to the current month. |

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/organizations/ORG_ID/billing/usage'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Organization billing usage. | OrganizationBillingUsage (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/organizations

**Agent Organizations List**

Visible organizations, including empty organizations for their admins. Names are not unique.


Operation: agentOrganizationsList.

- Authentication: agentIdentityBearer OR dashboardSession
- Request body: none

#### Request template

~~~sh
curl --request GET --cookie cookies.txt --cookie-jar cookies.txt 'https://api.screenrig.ai/api/organizations'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Organization result. | OrganizationList (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PATCH /api/organizations/{org_id}

**Agent Organizations Rename**

Designated admins and agents whose verified responsible person is an admin may rename an organization. No project permission or membership is granted.


Operation: agentOrganizationsRename.

- Authentication: agentIdentityBearer OR dashboardSession
- Request body: OrganizationRename

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| org_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |

#### Request template

~~~sh
curl --request PATCH --cookie cookies.txt --cookie-jar cookies.txt --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/organizations/ORG_ID'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Organization result. | OrganizationView (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PUT /api/project/review-policy

**Set Project Review Policy**

Turns Require review for agent changes on or off. While on, every agent and service client on the project is held to Propose access on every request, whatever its credential carries. The project owner or an admin of the project's organization only (forbidden otherwise); staff shadow sessions cannot change it.

If-Match carries review_policy_revision (0 allowed) and is required; a stale value is revision_conflict. Turning it off requires proof, the fresh passkey or password proof agent connection approval uses; turning it on does not. Open reviews stay decidable either way. Appends project.review_policy_changed.


Operation: setProjectReviewPolicy.

- Authentication: dashboardSession
- Request body: ReviewPolicyUpdate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| If-Match | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| agent_changes_require_review | required | boolean |  |
| proof | optional | DashboardProof |  |

#### Request template

~~~sh
curl --request PUT --cookie cookies.txt --cookie-jar cookies.txt --header 'If-Match: "REVISION"' --header 'Content-Type: application/json' --data '@request.json' 'https://screenrig.ai/api/project/review-policy'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The policy now in force. | ReviewPolicy (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/project/deletion-preview

**Preview Project Deletion Agent In Project**

Returns current confirmation details, deletion blockers and archived devices that will be disconnected. allowed is false, with a reason, when the caller may not delete the project (only its owner or an administrator of its organization may; for an agent, its responsible person) or a blocker remains.


Operation: previewProjectDeletionAgentInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| project_id | query | optional | Expected project ID; a changed selection is refused. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project/deletion-preview'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectDeletionPreview (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PUT /api/projects/{project_id}/project/owner

**Transfer Project Owner Agent In Project**

The owner transfers ownership to a verified existing project member; this grants no new membership. The new owner's membership leaves any screen_scope and upload_scope, so the new owner reaches the whole project.


Operation: transferProjectOwnerAgentInProject.

- Authentication: projectBearer
- Request body: ProjectOwnerTransfer

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Expected project ID. The server refuses the action if the current credential or cookie session targets another project; browser clients send the page's project ID. minLength: 1 maxLength: 200 |
| user_id | required | string | minLength: 1 maxLength: 200 |

#### Request template

~~~sh
curl --request PUT --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project/owner'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectContext (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/project/move

**Move Project Agent In Project**

Owner-authorized move transfers screen occupancy and new usage to the destination payer atomically; historical charges keep their payer.


Operation: moveProjectAgentInProject.

- Authentication: projectBearer
- Request body: ProjectMove

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Expected project ID. The server refuses the action if the current credential or cookie session targets another project; browser clients send the page's project ID. minLength: 1 maxLength: 200 |
| organization_id | required | string | minLength: 1 maxLength: 200 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project/move'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectContext (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/project/moves

**Allowed Project Moves Agent In Project**

Lists destinations the owner administers or whose verified domain matches their verified email.


Operation: allowedProjectMovesAgentInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| project_id | query | optional | Expected project ID; a changed selection is refused. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project/moves'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | ProjectMoveDestinations (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/invitations

**List Invitations In Project**

Lists invitation lifecycle records for the current project, excluding sign-in resets. Recipient addresses are visible to the owning project, which supplied them. Tokens and credential URLs are never listed.


Operation: listInvitationsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| kind | query | optional |  |
| status | query | optional |  |
| cursor | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/invitations'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Lists invitation lifecycle records for the current project, excluding sign-in resets. | InvitationList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/invitations

**Create Invitations In Project**

Creates member or ad-buyer invitations. Email delivery never returns a credential. Link delivery is member-only and returns one URL, including on exact idempotent replay within 24 hours. Email invitations expire after seven days; links expire after 24 hours. Outstanding email invitations of the same kind, address, and project are reused.

A member limited to screens does not invite people (outside_screen_scope).

A project_member email invitation to a person who would be an external guest of the project's organization (it is not their home organization) is subject to that organization's guest policy, and guest_not_allowed (403) answers when the policy (allow, domains or none) does not admit the address. Accepting the invitation checks again.

Staff and the organization's own admins are exempt. A screen_scope on the invitation is screen scope, which the Enterprise plan includes; a project whose plan lacks it is refused with feature_not_in_plan (402).


Operation: createInvitationsInProject.

- Authentication: projectBearer
- Request body: InvitationCreate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| kind | required | string | Allowed: "project_member", "ad_buyer" |
| delivery | optional | string | Allowed: "email", "link" default: "email" |
| emails | optional | array |  |
| advertising | optional | InvitationAdvertising | Seller-owned scope: at least one screen or slot. Screens only, slots only, or both are accepted. |
| upload_scope | optional | UploadScope | What an uploader may use (docs/uploaders.md): the playlists it changes pages of and the media folders it uploads into. Folders are exactly the listed ones, never their subfolders. An id that names no live object of the project when written is invalid_request at its position; a later deletion leaves a dead id, which grants nothing. The scope is read on each request, so a change applies to the next one. |
| screen_scope | optional | ScreenScope | What a scoped member or agent may act on (docs/screen-scope.md): the screens in the listed screen folders and in every folder inside them, and the listed screens. At least one id in all. An id that names no live folder or screen of the project when written is invalid_request at its position; a later deletion leaves a dead id, which grants nothing. The scope is read on each request, so a change applies to the next one. Tags are never scope. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/invitations'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Creates member or ad-buyer invitations. | InvitationCreated (application/json); headers: RateLimit-Policy, RateLimit |
| 402 | feature_not_in_plan — a screen scope needs Enterprise, and an upload scope (an uploader) needs Plus; the problem names the feature, the plan the account is on and required_plan. Narrowing an upload scope and removing either scope are always allowed. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |

### GET /api/projects/{project_id}/project

**Get Project In Project**

The calling project. Does not debit the 1-credit API meter, so a zero-remaining project can still read remaining and recover. After a successful project principal, authenticated /api JSON and problem responses include ScreenRig-Credits-Remaining, ScreenRig-Credits-Reset, and ScreenRig-Credits-Included as whole credits.

SSE, runtime, content, and unauthenticated responses omit them.


Operation: getProjectInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project | Project (application/json); headers: ScreenRig-Credits-Remaining, ScreenRig-Credits-Reset, ScreenRig-Credits-Included, ScreenRig-Credits-Low |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PATCH /api/projects/{project_id}/project

**Update Project In Project**

Renames the current project. Names contain 1–60 printable characters and no URL-looking text.


Operation: updateProjectInProject.

- Authentication: projectBearer
- Request body: ProjectPatch

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Expected project ID. The server refuses the action if the current credential or cookie session targets another project; browser clients send the page's project ID. minLength: 1 maxLength: 200 |
| name | required | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |

#### Request template

~~~sh
curl --request PATCH --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Renames the current project. | Project (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### DELETE /api/projects/{project_id}/project

**Delete Project Agent In Project**

Only the project's owner or an administrator of its organization may delete it; any other caller is refused with forbidden. A person must also be a member of the project.

An agent needs the target membership project capability and deletes for its responsible person, who must be the owner or an organization administrator; an agent without a responsible person cannot delete, except the agent that enrolled the project while no person has accepted its enrollment.

Also requires the current name and revision, zero non-archived screens, no credit, reservations or debt in its own wallet and no live advertising. A negative balance run up under free or launch terms does not block; deletion writes it off with a project_delete_writeoff ledger entry. Queues member notices and uses the irreversible tombstone.


Operation: deleteProjectAgentInProject.

- Authentication: projectBearer
- Request body: ProjectDeleteConfirmation

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Expected project ID. The server refuses the action if the current credential or cookie session targets another project; browser clients send the page's project ID. minLength: 1 maxLength: 200 |
| empty_only | optional | boolean | Require an empty Screens project in the deletion transaction, including content, other agents or members, work and correspondence. The CLI additionally requires its saved enrollment provenance before using this cleanup guard. Omission keeps explicit member deletion. |
| name | required | ProjectName | Trimmed printable project name. Rejects controls, URL-looking text, @, www., and ://. minLength: 1 maxLength: 60 |
| revision | required | integer | minimum: 1 |

#### Request template

~~~sh
curl --request DELETE --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project lifecycle result | DeletedProject (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/applications

**List Applications In Project**

Lists applications newest first, at most 500 per page. Names are labels and need not be unique; address an application by id. next_cursor is the position of the last returned row while more rows remain; pass it back as after, with the same filters, for the next page. next_cursor is null on the last page.


Operation: listApplicationsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| after | query | optional | next_cursor of the previous page of the same list, with the same filters. Omitted starts at the newest row. A value that is not such a cursor is invalid_request. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/applications'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Applications | ApplicationList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/applications

**Upload Application In Project**

Streams one CLI-produced tar.gz directly into bounded durable staging. Metadata is carried in headers so the archive is never base64-wrapped or materialized as JSON by the server.

The proxy in front of the API ends a request it has not answered within about 100 s, so a large archive on a slow uplink fails here; POST /api/applications/uploads sends the archive straight to the object store instead.


Operation: uploadApplicationInProject.

- Authentication: projectBearer
- Request body: string

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |
| ScreenRig-Archive-SHA256 | header | required |  |
| ScreenRig-Expanded-Bytes | header | required |  |
| ScreenRig-File-Count | header | required |  |
| ScreenRig-SDK-Version | header | optional |  |
| ScreenRig-Application-Name | header | optional | ASCII application name; mutually exclusive with ScreenRig-Application-Name*. Leading and trailing whitespace is trimmed. Controls and duplicate header values are rejected. The decoded name is limited to 120 Unicode scalar values. |
| ScreenRig-Application-Name* | header | optional | Unicode application name encoded as RFC 8187 UTF-8'' followed by percent-encoded UTF-8 bytes (empty language, exact UTF-8 charset). Mutually exclusive with ScreenRig-Application-Name. Invalid escapes, invalid UTF-8, controls, and duplicate header values are rejected. Leading and trailing whitespace is trimmed; the decoded name is limited to 120 Unicode scalar values. Neither name header may rename a release update. This avoids raw Unicode in HTTP header values. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'ScreenRig-Archive-SHA256: VALUE' --header 'ScreenRig-Expanded-Bytes: VALUE' --header 'ScreenRig-File-Count: VALUE' --header 'Content-Type: application/gzip' --data-binary '@request.bin' 'https://api.screenrig.ai/api/projects/PROJECT_ID/applications'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 202 | Upload accepted | OperationAccepted (application/json); headers: RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/applications/uploads

**Create Application Upload In Project**

Declares a direct upload of one CLI-produced tar.gz that creates a new application. The answer is a signed authorization for exactly one object PUT of the archive, so the bytes go straight to the object store and never through the API, whose proxy ends a request it has not answered within about 100 s.

PUT the archive to upload_url, then commit with POST /api/applications/uploads/{id}/commit and wait on the operation. The application, its release and the receiving operation exist from this answer on, as after POST /api/applications. An upload not committed within 24 hours fails, and an application that never published a release is removed.

The same Idempotency-Key and body answer the same upload with a freshly signed URL. GET /api/capabilities sets features.application_direct_uploads where these routes are served.


Operation: createApplicationUploadInProject.

- Authentication: projectBearer
- Request body: ApplicationUploadDeclaration

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | optional | string | Name of a new application, trimmed; at most 120 Unicode characters and no controls. Omitted or empty selects a generated name. A release declaration (POST /api/applications/{id}/uploads) must not carry one. maxLength: 120 |
| sha256 | required | string | SHA-256 of the archive bytes. pattern: "^[0-9a-f]{64}$" |
| compressed_bytes | required | integer | Size of the archive in bytes. minimum: 1 maximum: 104857600 |
| expanded_bytes | required | integer | minimum: 1 maximum: 262144000 |
| file_count | required | integer | minimum: 1 maximum: 5000 |
| sdk_version | optional | string | maxLength: 64 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/applications/uploads'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Signed authorization for exactly one object PUT of the archive. Send the returned method and every returned header verbatim; the PUT must start before expires_at, two hours after the declaration. | ApplicationUploadSession (application/json); headers: Cache-Control, RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/playlists

**List Playlists V2 In Project**

Versioned playlist list that may expose adslot pages. The v1 list refuses a page holding an ad-bearing document with version_required instead of filtering it.

Pages like listPlaylists, newest first, at most 200 per page and fewer once a page holds 8 MiB of stored playlists. next_cursor is the position of the last returned row while more rows remain; pass it back as after, with the same filters, for the next page. next_cursor is null on the last page. library=true lists the organization Library's playlists instead (read-only; schedule them or create from their templates).


Operation: listPlaylistsV2InProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| library | query | optional | true lists the organization's Library project instead of this project: its items are read-only here and carry library true. An organization with no Library, or the Library project itself, lists nothing with true. |
| after | query | optional | next_cursor of the previous page of the same list, with the same filters. Omitted starts at the newest row. A value that is not such a cursor is invalid_request. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/playlists'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Playlists with the adslot page union. | PlaylistV2List (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/playlists

**Create Playlist V2 In Project**

Authoring entry for the adslot page union. At most 16 adslot pages, at least one ordinary page with no visibility, and the reuse of one slot definition on different pages remain server checks. Reference resolution and DNS stay server checks.

A write that carries an iframe page the Player web app cannot frame is stored all the same, and its answer carries warnings (PlaylistWarning) and the project's event log gets one playlist.iframe_framing_refused event per such page.

A write whose iframe src, fallback_src or application params read a screen token ({screen.field.NAME} and the others of the primitive schemas) that a screen showing the playlist lacks also answers with a screen_fact_missing warning naming the first 20 such screens; those screens' manifests leave that primitive out.

An iframe src or fallback_src that is a Google Slides, Docs or Sheets link (share, Publish to web or embed) or a Microsoft 365 OneDrive or SharePoint Embed link is stored rebuilt for the screen, where Slides loop and advance on the link's delayms held to the page's duration, Docs, Sheets, Word and Excel reload every 5 minutes.

A pasted <iframe> embed code in src or fallback_src is read for its src. A file that asks viewers to sign in is refused with document_not_shared (400), whose errors[0].field is that src or fallback_src and whose detail names the sharing setting to change.


Operation: createPlaylistV2InProject.

- Authentication: projectBearer
- Request body: PlaylistCreateRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Screenrig-Review-Note | header | optional | Optional proposer note on a review-class write, sent as percent-encoded UTF-8 (what JavaScript's encodeURIComponent writes). The server percent-decodes it (an invalid escape is 400 invalid_request); the decoded note is plain text, at most 280 characters after trimming, no control characters (else 400 invalid_request). Stored on the review when the write becomes one; ignored when it applies live. |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/playlists'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 202 | The write became a review and waits for a person's approval (docs/reviews.md), under Propose access, or at any access level when it reaches a screen in a protected screen folder, where a second approver decides it (docs/screen-scope.md). A person approves, rejects or requests changes in the dashboard at review.url. An Idempotency-Key replay answers the same body. | ReviewPending (application/json) |
| 201 | Playlist with its adslot pages. | PlaylistV2 (application/json) |
| 402 | feature_not_in_plan — Declaring fields, locking a primitive or changing a playlist that declares fields is a playlist template, and the project's plan does not include them; required_plan names the plan that does. Creating a playlist from a template is always allowed. A format of 2 or more displays (a video wall) is part of Plus; a region format and a playlist with no format are in every plan. The problem names the feature, the project's plan and required_plan. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/screens

**List Screens In Project**

Default list returns pairing_pending and active screens. Pass state=archived to list archived screens only. Pass tag to list only screens whose tags contain that exact tag; untagged screens are omitted when the filter is present.

Pass field, once per field, as NAME:VALUE to list only screens whose fields hold every value, and near as LAT,LON,KM to list only screens located within KM kilometres of the point; screens with no location are omitted, and every filter combines with state and tag.

Pass folder as a screen folder ID to list only the screens directly inside it (not those of its subfolders), or root for the screens outside every folder.

Screens are newest first, at most 500 per page. next_cursor is the position of the last returned row while more rows remain; pass it back as after, with the same filters, for the next page. next_cursor is null on the last page. A caller limited to screens (docs/screen-scope.md) lists only the screens in its scope.

A request that names no project leaves out each project where the caller is limited to screens; name the project to list it.


Operation: listScreensInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| state | query | optional | Omit for pairing_pending and active. Pass archived to list archived screens only. |
| tag | query | optional | Exact screen tag. Untagged screens are omitted when this filter is present. |
| field | query | optional | NAME:VALUE, split at the first colon: only screens whose field NAME holds exactly VALUE. Repeat the parameter for several fields; every one must match, and a name may be given once. A malformed or repeated one is invalid_request naming field. |
| near | query | optional | LAT,LON,KM in decimal degrees and kilometres, for example 47.6062,-122.3321,25: only screens whose location lies within KM kilometres of the point. Latitude is -90 to 90, longitude -180 to 180 and KM above 0 and at most 20038. Screens with no location are omitted. A malformed value is invalid_request naming near. |
| folder | query | optional | A screen folder ID keeps only the screens directly inside that folder, not those of its subfolders; root keeps only the screens outside every folder. Omitted lists every screen. |
| after | query | optional | next_cursor of the previous page of the same list, with the same filters. Omitted starts at the newest row. A value that is not such a cursor is invalid_request. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/screens'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Screens | ScreenList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/screen-folders

**List Screen Folders In Project**

Lists every screen folder in the project, at most 1000, ordered by name.

Folders organize screens and target them: a campaign audience leaf {"folder": ID} matches the screens in that folder and every folder inside it, a fleet action selector by:folder acts on the same screens, and a folder's default_playlist_id is what its screens without a playlist of their own show. parent_id is null at the project root; build the tree from parent_id. screens counts the live screens directly inside each folder.

A screen's folder is Screen.folder_id. A caller limited to screens (docs/screen-scope.md) lists the folders inside its scope and the folders above them, which are read-only so its tree has a root; folders above carry no screens count and sibling folders are hidden. A request that names no project leaves out each project where the caller is limited to screens.


Operation: listScreenFoldersInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/screen-folders'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Screen folders | ScreenFolderList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/screen-folders

**Create Screen Folder In Project**

Creates one screen folder at the project root (parent_id null or absent) or inside parent_id. Names are trimmed, hold 1 to 100 Unicode characters with no controls, are not "." or "..", and are unique among their siblings (resource_conflict).

An organization holds at most 1000 folders across its projects (quota_exceeded) nested at most 10 deep (invalid_request). default_playlist_id, when set, must name an approved playlist (playlist_not_approved). Emits screen.folder_created.

A caller limited to screens (docs/screen-scope.md) creates a folder only inside a folder of its scope; the project root or any other parent is outside_screen_scope. Creating a folder inside a protected folder is a protected_change review (202 pending_review) decided by a second approver.


Operation: createScreenFolderInProject.

- Authentication: projectBearer
- Request body: ScreenFolderCreate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | string | Trimmed; 1 to 100 Unicode characters, no controls, not . or .., unique among its siblings. minLength: 1 maxLength: 100 |
| parent_id | optional | inline | The folder to create it in; null or absent for the project root. |
| default_playlist_id | optional | string | An approved playlist for the folder's screens without their own. minLength: 1 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/screen-folders'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created folder. | ScreenFolder (application/json); headers: ETag |
| 202 | The write became a review and waits for a person's approval (docs/reviews.md), under Propose access, or at any access level when it reaches a screen in a protected screen folder, where a second approver decides it (docs/screen-scope.md). A person approves, rejects or requests changes in the dashboard at review.url. An Idempotency-Key replay answers the same body. | ReviewPending (application/json) |
| 403 | outside_screen_scope as on OutsideScreenScope, or approver_required - a member limited to screens who cannot approve reviews does not change what a screen plays or when (docs/screen-scope.md). | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/screens/move

**Move Screens In Project**

Moves screens (screen_ids) and folders (folder_ids) into one folder (folder_id), or to the project root when folder_id is null, in one all-or-nothing request of at most 1000 ids. Every id must exist (not_found). A folder cannot move into itself or a folder inside it, and the move must keep every folder within 10 levels (invalid_request).

Items already in the destination are left as they are; each moved screen and folder advances its revision. Moved screens re-resolve their playlist within about 15 seconds: levels names the campaigns whose folder audience they enter or leave and the folder default playlists they gain or lose. Emits screen.moved.

A caller limited to screens (docs/screen-scope.md) moves screens and folders inside its scope into a folder of its scope; any id or destination outside it, and the project root, refuse the whole move (outside_screen_scope), also when a screen leaves the scope between the check and the write.

A move into or out of a protected folder is a protected_change review (202 pending_review) decided by a second approver.


Operation: moveScreensInProject.

- Authentication: projectBearer
- Request body: ScreenMove

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| folder_id | required | inline | Destination folder; null for the project root. |
| screen_ids | optional | array |  |
| folder_ids | optional | array |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/screens/move'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | How many screens and folders moved, and the levels the moved screens gain or lose. | ScreenMoveResult (application/json) |
| 202 | The write became a review and waits for a person's approval (docs/reviews.md), under Propose access, or at any access level when it reaches a screen in a protected screen folder, where a second approver decides it (docs/screen-scope.md). A person approves, rejects or requests changes in the dashboard at review.url. An Idempotency-Key replay answers the same body. | ReviewPending (application/json) |
| 403 | outside_screen_scope as on OutsideScreenScope, or approver_required - a member limited to screens who cannot approve reviews does not change what a screen plays or when (docs/screen-scope.md). | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/schedule/calendar

**Get Schedule Calendar In Project**

Answers what a subject's screens play over [from, to) and why, for the dashboard's Schedule calendar: exactly one of screen_id, folder_id (the screens in the folder and every folder inside it), tag, or campaign_id (the screens in its audience).

Screens whose stacks have identical inputs (the same levels, default, timezone and display count) share one group with one timeline, so a large subject costs tens of timelines; a group lists up to 500 of its screens and an answer at most 200 groups (truncated).

Each group carries its stack, highest first (takeover, campaigns that reach the screens, their own schedule entries, then the default playlist or the nearest folder's), each level's spans (plays on every display it targets, partial with the displays it plays on, outranked, or conditional while it waits on interaction or a data condition only a Player observes), what each display shows (on_screen; the main region of a full page), and conflicts (two campaigns of one priority holding at once; a level that holds yet never plays). from and to are RFC 3339 instants, to after from, at most 93 days apart; they are truncated to the minute.

Rules are read in each group's timezone as the sweep and every Player read them; every level is judged playable. A caller limited to screens (docs/screen-scope.md) must send a screen_id or a folder_id inside its scope; a tag, a campaign_id or no subject is outside_screen_scope.


Operation: getScheduleCalendarInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| from | query | required |  |
| to | query | required |  |
| screen_id | query | optional |  |
| folder_id | query | optional |  |
| tag | query | optional |  |
| campaign_id | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/schedule/calendar?from=FROM&to=TO'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The subject's calendar. | ScheduleCalendar (application/json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/schedule/imports/template

**Get Schedule Import Template In Project**

Answers a sheet with the columns a schedule import reads and three example rows (a screen code, a screen folder and a tag), as CSV or, with format=xlsx, an XLSX workbook.


Operation: getScheduleImportTemplateInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| format | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/schedule/imports/template'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The template sheet. | string (text/csv), string (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/schedule/imports/preview

**Preview Schedule Import In Project**

Reads a sheet of schedule rows against the project and answers what committing it would do, writing nothing.

Each row names a target (a screen by id, by the value of a screen field such as a room code, or by name; folder:NAME; tag:NAME; several separated by ; or |), content (a media file name, or playlist:NAME or a playlist id), a start and an end (a date alone as the end closes that day), and optionally timezone, days (Mon-Fri), a daily window (daily_start, daily_end), priority 0 to 100, a name and display numbers.

Times are read on each screen's own clock; a timezone column converts them from another zone.

Every row is checked and reports its errors by column: unknown or ambiguous screen, folder or tag, missing or ambiguous file, a file that cannot fill a screen, invalid or reversed times, an end in the past, no timing at all, a screen without a time zone or a default playlist, two rows holding at once on a screen at the same priority, and more levels than a screen holds (32), or more campaigns than its organization holds across its projects (100; each shared level is one).

Rows that agree on content, times, priority and name become one level: one screen is an entry of that screen's own schedule; several screens, a folder or a tag are one shared level (a campaign), which outranks a screen's own levels.

The answer lists the levels with what a commit does to each (create, update, unchanged), the levels an earlier run of the same import made that the sheet no longer has (removals), the stacks of the screens as they would be (calendar), and a fingerprint to send to the commit. The sheet is JSON rows, CSV or XLSX, at most 5,000 rows.

Not available to a caller limited to screens (outside_screen_scope). Schedule import is part of the Enterprise plan; a project whose plan lacks it is refused with feature_not_in_plan (402). The template is open on every plan.


Operation: previewScheduleImportInProject.

- Authentication: projectBearer
- Request body: ScheduleImportRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| name | query | optional | The import's name; importing again under the same name updates the levels it made (default import). Sent with a file body. |
| timezone | query | optional | The IANA zone the times are written in when a row has no timezone. Without one, times are read on each screen's own clock. |
| source | query | optional | The file name, recorded with the import. |
| code_field | query | optional | Read a screen code only from this screen field (for example code or room_id); otherwise a code matches any field. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | optional | string | Identifies the import; the same name updates the levels it made (default import). minLength: 1 maxLength: 80 |
| source | optional | string | The file the rows came from. maxLength: 200 |
| timezone | optional | string | The IANA zone the times are written in when a row has no timezone. |
| code_field | optional | string | Read a screen code only from this screen field. |
| fingerprint | optional | string | The preview's fingerprint; required by the commit. |
| rows | required | array |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/schedule/imports/preview'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The preview. valid is false while any row has an error. | ScheduleImportPreview (application/json) |
| 402 | feature_not_in_plan — Schedule import is part of the Enterprise plan; the problem names the feature, the plan the account is on and required_plan. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/schedule/imports

**Commit Schedule Import In Project**

Commits a previewed import: the same sheet and the fingerprint its preview answered. Every level is created, updated or removed, the screens' schedules and the shared levels written in one transaction, and the import event schedule.imported appended: all rows apply or none does.

The one-file playlists the sheet's files need are made first (they change nothing on any screen). A sheet whose rows have errors is refused (schedule_import_invalid); a project that changed since the preview, or rows that read differently, is refused (schedule_import_stale), and the client previews again.

Importing the same sheet again under the same name changes nothing; a changed row updates its level and a removed row removes its level. Needs a person who can approve in the dashboard and a credential with write access for an agent.

A commit that would update or delete a reserved campaign answers reserved_item (403) and writes nothing, unless a person who can approve commits it.

A commit that reaches a protected screen is refused whole with protected_target, also when the import has no levels but removes import entries of a protected screen or deletes an import campaign whose audience reaches one.

In a project with protected screen folders a commit whose preview fails on a service, not on the sheet, answers dependency_unavailable and writes nothing: send the same request again after a short wait. A sheet, campaign or screen validation error answers as the commit would (invalid_request and the other problems above).

Not available to a caller limited to screens. Schedule import is part of the Enterprise plan; a project whose plan lacks it is refused with feature_not_in_plan (402). The template is open on every plan.


Operation: commitScheduleImportInProject.

- Authentication: projectBearer
- Request body: ScheduleImportRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |
| fingerprint | query | optional | The preview's fingerprint, for a CSV or XLSX body; a JSON body carries it as a member. |
| name | query | optional | The import's name; importing again under the same name updates the levels it made (default import). Sent with a file body. |
| timezone | query | optional | The IANA zone the times are written in when a row has no timezone. Without one, times are read on each screen's own clock. |
| source | query | optional | The file name, recorded with the import. |
| code_field | query | optional | Read a screen code only from this screen field (for example code or room_id); otherwise a code matches any field. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | optional | string | Identifies the import; the same name updates the levels it made (default import). minLength: 1 maxLength: 80 |
| source | optional | string | The file the rows came from. maxLength: 200 |
| timezone | optional | string | The IANA zone the times are written in when a row has no timezone. |
| code_field | optional | string | Read a screen code only from this screen field. |
| fingerprint | optional | string | The preview's fingerprint; required by the commit. |
| rows | required | array |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/schedule/imports'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | What the commit applied. | ScheduleImportCommitted (application/json) |
| 402 | feature_not_in_plan — Schedule import is part of the Enterprise plan; the problem names the feature, the plan the account is on and required_plan. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| 409 | protected_target - the change reaches a screen in a protected screen folder and has no review form (docs/screen-scope.md). Clear the folder's protection through a review, make the change, and protect the folder again. | Problem (application/problem+json) |
| 500 | dependency_unavailable - the check for protected screen folders could not run; send the same request again after a short wait. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/project/browser-links/claim

**Claim Browser Link In Project**

Claims one browser-link code for the authenticated project and atomically creates exactly one first pairing_pending screen plus its existing browser provisioning grant. Enrollment remains project-only. The response is safe metadata only and never contains a provisioning token, fragment URL, cookie, or proof.


Operation: claimBrowserLinkInProject.

- Authentication: projectBearer
- Request body: BrowserLinkClaimRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| code | required | string | pattern: "^[23456789ABCDEFGHJKMNPQRSTUVWXYZ]{3}-?[23456789ABCDEFGHJKMNPQRSTUVWXYZ]{3}$" |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project/browser-links/claim'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Exact replay-safe fragment-free browser-link claim. | BrowserLinkClaim (application/json); headers: Cache-Control, RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 403 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 410 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| 500 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/screens/pair

**Pair Screen In Project**

Claims one fleet-global six-character Player pairing session. Exact Idempotency-Key retries return the durable original result without consuming quota again.

The dashboard session claims into its current project with the same body, Idempotency-Key, budgets and problems, except that its not_found for an unknown or already-claimed code names no CLI command; it is a state change, so it follows the dashboard mutation rules, a shadow session without staff write mode is refused, and the events the claim appends name the dashboard user. folder_id names the folder the screen lands in.

A caller limited to screens (docs/screen-scope.md) must send a folder inside its scope and pairs only as a person who can approve (outside_screen_scope, approver_required).


Operation: pairScreenInProject.

- Authentication: projectBearer
- Request body: PairScreen

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| code | required | string | pattern: "^[23456789ABCDEFGHJKMNPQRSTUVWXYZ]{6}$" |
| label | optional | string | minLength: 1 maxLength: 120 |
| folder_id | optional | string | The screen folder the screen lands in; omitted lands it at the project root. A caller limited to screens must send a folder inside its scope (outside_screen_scope otherwise). A folder the project does not hold is invalid_request for a caller of the whole project. Part of the idempotent request: another folder_id under the same Idempotency-Key is idempotency_mismatch. minLength: 1 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/screens/pair'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Durable pairing_pending screen claim. | PairingClaim (application/json); headers: Cache-Control, RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 413 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| 500 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 403 | outside_screen_scope as on OutsideScreenScope, or approver_required - a member limited to screens who cannot approve reviews does not change what a screen plays or when (docs/screen-scope.md). | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/screens/provision

**Provision Screen In Project**

Optional browser-only new-screen provisioning. Always creates one new pairing_pending screen; at most five wait to pair at once (quota_exceeded). Exact retries redeliver the same fragment token for ten minutes. Six-character pairing remains the default and only recovery flow. folder_id names the folder the screen lands in.

A caller limited to screens (docs/screen-scope.md) must send a folder inside its scope (outside_screen_scope otherwise).


Operation: provisionScreenInProject.

- Authentication: projectBearer
- Request body: ProvisionScreen

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| label | optional | string | minLength: 1 maxLength: 120 |
| folder_id | optional | string | The screen folder the screen lands in, as on PairScreen. minLength: 1 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/screens/provision'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | New browser provisioning link. | ScreenProvisioning (application/json); headers: Cache-Control, RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 410 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 413 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| 500 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 403 | outside_screen_scope as on OutsideScreenScope, or approver_required - a member limited to screens who cannot approve reviews does not change what a screen plays or when (docs/screen-scope.md). | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/screens/actions

**Apply Screen Actions In Project**

Applies one action to a selected fleet with no screen-count cap. Select unique screen_ids or a tag matching active screens in creation order. The resolved selection is pinned before any mutation, stored in bounded chunks, and kept for twenty-four hours under Idempotency-Key.

Each step uses its single-screen mutation path, preserving revisions, events, runtime signals and per-screen limits. A response contains one bounded page of results; matched, succeeded and failed describe that page. HTTP 202 includes total, offset and next_cursor. Continue with the same selector, action and Idempotency-Key, passing next_cursor as after.

Honor Retry-After when present, including a page with zero results. The response echoes Idempotency-Key, including a generated key when the caller omitted it; retain this key for continuations. HTTP 200 finishes the selection. Answered pages replay exactly, including per-screen failures; interrupted steps replay without repeating their effects.

Large actions are paced against the existing 600-per-minute organization budgets for reload, toast, takeover, playlist schedules, reboot, display and display schedules; organization exhaustion pauses the action instead of refusing the fleet. Per-screen exhaustion fails that screen alone.

An assign, takeover or set_playlist_schedule step that binds a playlist with a format to a screen whose wall has another shape fails that screen alone with 409 playlist_format_mismatch. Assign and tag changes have no message budget.

An assign whose playlist has an iframe src or application params that read a screen token some screen of the page lacks adds a screen_fact_missing warning to that page (warnings, PlaylistWarning). Partial failure is a normal result. All pages and retries under one key are one metered action.

Requests use a bounded byte payload; use a tag for a selection larger than the transport payload. A malformed selector or action is refused before any screen is touched. selector or action is refused before any screen is touched.

A caller limited to screens (docs/screen-scope.md) selects by screen_ids or by folder, every screen inside its scope; a tag selector, a screen outside the scope or an unknown one refuses the whole action (outside_screen_scope) before anything is pinned or proposed.

Assign, takeover, takeover_clear, set_playlist_schedule and clear_playlist_schedule that reach a protected screen become a review decided by a second approver (202 pending_review) at every access level; set_tags, add_tags and remove_tags on a protected screen are protected_target (409).

A set_playlist_schedule action with a paused entry (a draft) is schedule change control, which the Enterprise plan includes; a project whose plan lacks it is refused with feature_not_in_plan (402).


Operation: applyScreenActionsInProject.

- Authentication: projectBearer
- Request body: ScreenActionRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Screenrig-Review-Note | header | optional | Optional proposer note on a review-class write, sent as percent-encoded UTF-8 (what JavaScript's encodeURIComponent writes). The server percent-decodes it (an invalid escape is 400 invalid_request); the decoded note is plain text, at most 280 characters after trimming, no control characters (else 400 invalid_request). Stored on the review when the write becomes one; ignored when it applies live. |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |
| after | query | optional | next_cursor from the preceding result page. Reuse the original Idempotency-Key and request body. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| selector | required | ScreenActionSelector |  |
| action | required | ScreenAction | One fleet action, discriminated by type. Each member carries only its own parameters; a member of another type is an unknown field and invalid_request. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/screens/actions'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Per-screen results. Partial success is a normal answer. | ScreenActionResult (application/json); headers: Idempotency-Key, Cache-Control, RateLimit-Policy, RateLimit |
| 202 | More screens remain (continue with next_cursor and the same Idempotency-Key and body, honoring Retry-After), or, with status pending_review, the action became a review under Propose access (docs/reviews.md), or at any access level because it reaches a screen in a protected screen folder (docs/screen-scope.md). Only assign, takeover, takeover_clear, set_playlist_schedule and clear_playlist_schedule become reviews; every other action is insufficient_access for Propose. | inline (application/json); headers: Idempotency-Key, Retry-After, Cache-Control, RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — set_reboot_schedule with enabled true (Scheduled reboots) and set_playlist_schedule with a displays lock (Video walls) are part of Plus; the clearing actions and one-off commands are open. payment_required — the project has no credit left. The problem names the feature, the project's plan and required_plan. | Problem (application/problem+json) |
| 403 | insufficient_access, outside_screen_scope when a caller limited to screens selects outside its scope, or approver_required for a member limited to screens who cannot approve. | Problem (application/problem+json) |
| 409 | playlist_format_mismatch and the other conflicts of the action, or protected_target when set_tags, add_tags, remove_tags, toast, display, display_clear, set_display_schedule or clear_display_schedule reaches a screen in a protected screen folder. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/tab-shares

**Start Tab Share In Project**

Shows browser tabs on screens in the named project; the request, answer, access and limits are those of POST /api/tab-shares.


Operation: startTabShareInProject.

- Authentication: projectBearer
- Request body: TabShareWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| screen_ids | optional | array | Screens by id. Duplicates collapse. |
| folder_ids | optional | array | Screen folders; each includes every folder inside it. |
| tags | optional | array | A screen carrying any of the tags. |
| tabs | required | array |  |
| cookies | optional | array | The sender's cookies for the pages, at most 64 KiB encoded. Secrets; see POST /api/tab-shares. |
| reload_s | optional | integer | 0 (never, the default) or 60 to 86400: every page's iframe reloads this often. |
| until | optional | string | Strictly in the future and at most 24 hours ahead; 24 hours from now when omitted. The end of the takeover on every target and the expiry of the temporary playlist. format: "date-time" |
| qr | optional | boolean | When true every page carries a QR code of its address in the bottom-right corner. Default false. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/tab-shares'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The share. At least one screen was taken over. | TabShare (application/json); headers: RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| 403 | outside_screen_scope as on OutsideScreenScope, or approver_required - a member limited to screens who cannot approve reviews does not change what a screen plays or when (docs/screen-scope.md). | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/campaigns

**List Campaigns In Project**

Lists the project's campaigns in precedence order (at most 100), the order resolution reads them. reserved is present on every campaign.

A caller limited to screens (docs/screen-scope.md) sees every campaign, with editable true only when the whole audience is inside its scope (a tag leaf and the root every-screen rule never are; an all rule needs one contained child, an any rule every child) and the caller may change what plays.


Operation: listCampaignsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/campaigns'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's campaigns. | CampaignList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/campaigns

**Create Campaign In Project**

createCampaign for the project named in the path. reserved true reserves the campaign, and only a person who can approve sets it (reserved_item for an agent, a service client or anyone else), appending campaign.reserved_changed.

A caller limited to screens (docs/screen-scope.md) creates a campaign only for an audience inside its scope (folder and screen_id leaves; no tag, no every screen), else outside_screen_scope, and only as a person who can approve (approver_required).

A campaign whose audience reaches a protected screen is a protected_change review decided by a second approver (202 pending_review) at every access level. reserved true is protected folders and paused true on a new campaign (a draft) is schedule change control, both part of the Enterprise plan; a project whose plan lacks it is refused with feature_not_in_plan (402).


Operation: createCampaignInProject.

- Authentication: projectBearer
- Request body: CampaignWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | string | minLength: 1 maxLength: 120 |
| playlist_id | required | string | minLength: 1 |
| audience | required | CampaignAudience | Which screens a campaign targets: exactly one of all (every rule matches), any (at least one matches), tag (the screen carries the tag), screen_id (that screen) or folder (the screens in that screen folder and every folder inside it, GET /api/screen-folders; the folder must exist). Rules nest at most 3 deep and an audience has at most 500 rules. {"all": []} is every screen of the project; elsewhere all and any list at least one rule. |
| from | optional | string | pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T([01][0-9]\|2[0-3]):[0-5][0-9]$" |
| until | optional | string | Later than from. pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T([01][0-9]\|2[0-3]):[0-5][0-9]$" |
| windows | optional | array |  |
| monthly | optional | array |  |
| dates | optional | array |  |
| except | optional | array | Civil dates, stored ascending and once each. An exception removes every occurrence its date starts, overnight tails included; it falls between from and until. |
| data | optional | PageDataCondition | A card-data predicate, evaluated before civil time. Missing or expired data never matches. The page also respects its ordinary time bounds. Requires host capability page.conditions; older hosts receive the page disabled. |
| interaction | optional | LevelInteraction | The level holds while someone is using the screen: from a touch, click or key until idle_s seconds pass with no input. Typical use: an interactive page above an attract loop. idle_s is stored and returned (60 when omitted). |
| covers | optional | string | Where the level plays: the full page (default, stored absent) or main, the page itself, for an ordinary playlist; header, footer or side, a strip drawn over the page, for a region playlist of that region only (PlaylistRegionFormat). A region playlist is refused with full or main, and a strip needs a region playlist that is laid out on a targeted display. Allowed: "full", "main", "header", "footer", "side" |
| displays | optional | array | The outputs of a video wall it plays on; absent plays on every output. |
| start | optional | string | on_time switches at the edge, interrupting the current item; next_item (default, stored absent) waits for it to end. Allowed: "next_item", "on_time" |
| priority | optional | integer | default: 0 minimum: 0 maximum: 100 |
| paused | optional | boolean | default: false |
| reserved | optional | boolean | Only a person who can approve reserves a campaign (reserved_item otherwise). default: false |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/campaigns'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. | Campaign (application/json); headers: ETag |
| 202 | The write became a review and waits for a person's approval (docs/reviews.md), under Propose access, or at any access level when it reaches a screen in a protected screen folder, where a second approver decides it (docs/screen-scope.md). A person approves, rejects or requests changes in the dashboard at review.url. An Idempotency-Key replay answers the same body. | ReviewPending (application/json) |
| 402 | feature_not_in_plan — Protected folders and schedule change control are part of the Enterprise plan; the problem names the feature, the plan the account is on and required_plan. A campaign that locks a playlist to display 2 or beyond of a video wall (displays) is part of Plus. The problem names the feature, the project's plan and required_plan. | Problem (application/problem+json) |
| 403 | outside_screen_scope or approver_required as on ScreenScopePlays, or reserved_item - a reserved campaign is changed, paused, deleted or un-reserved only by a person who can approve reviews, never by an agent or a service client (docs/screen-scope.md). | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/connections

**List Connections In Project**

Lists the project's connections, newest first, at most 200 a page; pass next_cursor back as after for the next. A secret, a key or a refresh token is never returned: secret shows whether one is held and a hint that is not secret. A read never carries a consent link.


Operation: listConnectionsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| after | query | optional | next_cursor of the previous page of the same list, with the same filters. Omitted starts at the newest row. A value that is not such a cursor is invalid_request. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/connections'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One page of connections. | ConnectionList (application/json); headers: Cache-Control |
| 400 | invalid_request, including a malformed cursor. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/connections

**Create Connection In Project**

Creates a connection to Microsoft or Google in this project: mode own_app with the customer's own credentials, or mode screenrig_app, which answers consent_url, a one-time link for the person who completes it (see createConnection). Appends connection.created. An exact retry with the same Idempotency-Key returns the same connection.


Operation: createConnectionInProject.

- Authentication: projectBearer
- Request body: ConnectionWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| provider | required | string | Allowed: "microsoft", "google" |
| mode | required | string | Allowed: "own_app", "screenrig_app" |
| name | required | string | minLength: 1 maxLength: 80 |
| config | optional | ConnectionConfigWrite | Microsoft, mode own_app: both members. Not accepted for a Google connection or for mode screenrig_app. |
| secret | optional | string | Mode own_app. Microsoft: the value of one of the app's client secrets (not its id; at most 1024 characters), with config.tenant_id and config.client_id. Google: the service account's JSON key file, whole (at most 16384 bytes). Sealed on arrival and never returned; the CLI reads it from a file or stdin, never an argument. Not accepted in mode screenrig_app. minLength: 1 maxLength: 16384 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/connections'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. A connection in mode screenrig_app carries consent_url and consent_expires_at. | ConnectionConsent (application/json); headers: ETag, Location, Cache-Control |
| 400 | invalid_request, naming the field that is not valid. | Problem (application/problem+json) |
| 409 | connection_app_not_configured, connection_limit_reached or idempotency_mismatch. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/bi-connections

**List BIConnections In Project**

Lists the project's BI connections, oldest first, 200 to a page; pass next_cursor back as cursor for the next. A secret is never returned: secret shows whether it is set and its last 4 characters.


Operation: listBIConnectionsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| cursor | query | optional | next_cursor of the previous page. |
| limit | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/bi-connections'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One page of connections. | BIConnectionList (application/json); headers: Cache-Control |
| 400 | invalid_request, including a malformed cursor. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/bi-connections

**Create BIConnection In Project**

Adds a connection to a BI system the project owns. For Grafana: its address (http or https on a public host, with an optional path prefix) and a service-account token with the Viewer role. The token is sealed and never returned.

Grafana renders dashboards for screens through its Image Renderer plugin or remote rendering service, so the customer installs one; run POST /api/bi-connections/{id}/test to check. For Power BI (kind powerbi): config.tenant_id and config.client_id of a Microsoft Entra app registration and its client secret value as secret.

A Power BI admin must turn on the Developer settings Embed content in apps and Service principals can call Fabric public APIs for the app (admin portal, Tenant settings), the app must be a Member or Admin of each workspace it shows, and reports need an Embedded, Fabric or Premium capacity for production use.

For Tableau (kind tableau): config.url (the https address of Tableau Server, or of your Tableau Cloud pod such as https://10ay.online.tableau.com), config.site (the site content URL, empty for the default site), config.client_id and config.secret_id of a connected app with direct trust, config.username (the Tableau user the screens views are shown as) and the secret value as secret.

Add the answer embed_origin to the connected app domain allowlist and enable the secret. Anyone holding a screen ticket can embed any view that user can see, so use a user that sees only screen dashboards. Tableau Cloud, or Tableau Server 2023.3 or later.

For a signed-in web page (kind web): the service account of a site that shows its dashboard behind a sign-in. config.login_mode is auto (config.login_url, config.username and the password as secret: the renderer opens the sign-in page and fills the form), steps (config.login_url and config.steps, a script of goto, fill, click, press, wait_for and wait_ms steps whose values may hold {{username}}, {{password}} and {{otp}}) or none (no sign-in: the connection shows pages with the cookies you give it). totp_secret is the base32 key, or the otpauth:// address, of the account's authenticator app: the server computes the one-time code (RFC 6238) and sends the renderer only the code. cookies is a cookie jar exported from a signed-in browser, for a site whose sign-in cannot be scripted (single sign-on that approves on a phone, a device check); the server keeps the jar and renews it from the cookies each capture returns, so a sign-in is rare.

The password, the seed and the cookies are sealed and never returned. Use a read-only service account. The password is typed only into config.login_url and the goto steps, so moving those to another origin needs secret again. After five refused sign-ins in a row the connection stops signing in until its credentials change.

A server with no page renderer answers bi_web_unavailable. Appends bi_connection.created. An exact retry with the same Idempotency-Key returns this same answer.


Operation: createBIConnectionInProject.

- Authentication: projectBearer
- Request body: BIConnectionWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | string | minLength: 1 maxLength: 80 |
| kind | required | string | Allowed: "grafana", "powerbi", "tableau", "web" |
| config | required | BIConnectionConfig |  |
| secret | optional | string | Required except for web, and for a Power BI connection that names connection_id. The service-account token (Grafana), the app's client secret value (Power BI), the connected app's secret value (Tableau) or the service account's password (web: required by login_mode auto, not used by none). Sealed on arrival; the CLI reads it from a file or stdin, never an argument. minLength: 1 maxLength: 4096 |
| totp_secret | optional | string | Web: the base32 key of the account's authenticator app (16 to 128 characters, spaces allowed), or its otpauth://totp address, which may set the algorithm (SHA1, SHA256, SHA512), the digits (6 to 8) and the period. Sealed; only the computed code leaves the server. maxLength: 1024 |
| cookies | optional | array | Web: a cookie jar exported from a signed-in browser. Sealed; renewed from every capture. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/bi-connections'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. | BIConnection (application/json); headers: ETag, Cache-Control |
| 400 | invalid_request, including an address that is not a public Internet host. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — the project's plan does not include BI dashboards; required_plan names the plan that does. | Problem (application/problem+json) |
| 409 | idempotency_mismatch. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/bi-views

**List BIViews In Project**

Lists the project's BI views, oldest first, 200 to a page; pass next_cursor back as cursor for the next. Each carries embed_url, the address an iframe primitive shows (content_fit fill); it is null where this server has no embed host.


Operation: listBIViewsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| cursor | query | optional | next_cursor of the previous page. |
| limit | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/bi-views'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One page of views. | BIViewList (application/json); headers: Cache-Control |
| 400 | invalid_request, including a malformed cursor. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/bi-views

**Create BIView In Project**

Adds a view of a connection's Grafana dashboard or one panel of it (url: a /d/{uid}/... address on the connection's host, or target), or of a Grafana public dashboard link (kind grafana and target.public_url, no connection).

Put the answer's embed_url in an iframe primitive: the server renders the dashboard as a PNG through Grafana's /render API, refreshed every refresh_s (30 to 86400, default 300) and shared by every screen of the same size; the screen never talks to Grafana. A public dashboard plays live from Grafana, which must allow embedding.

The embed page shows only a ticket the screen's manifest carries, so a view plays only on the screens of its own project.

For Power BI (kind powerbi): a report of a workspace the connection's app is a member of (url: an app.powerbi.com /groups/{workspace}/reports/{report} address, or target with workspace_id and report_id), with pages to show and rotate_s to rotate through them (pages listing two or more, or rotate_all_pages, need rotate_s of 10 to 3600; one report page needs none), and refresh_s of 0 (no refresh) or 60 to 86400 (default 300).

The page asks the server for a short-lived view-only embed token (GenerateToken, App owns data) and renews it before it expires; the screen never holds the client secret. A Power BI Publish to web link is a view without a connection (kind powerbi, target.public_url https://app.powerbi.com/view?r=...), framed as it is.

For Tableau (kind tableau): a view of the connection site (url: an address copied from Tableau, such as https://HOST/t/SITE/views/WORKBOOK/VIEW or Tableau Cloud https://HOST/#/site/SITE/views/WORKBOOK/VIEW, or Workbook/View; or target.view), optionally with target.rotate_sheets and rotate_s of 10 to 3600 to turn the workbook published sheets (rotate_s and rotate_sheets go together; rotate_sheets may be given as the whole target beside url), and refresh_s of 0 (no refresh) or 60 to 86400 (default 300).

The page asks the server for a five minute JWT with only the embed scope, signed with the connected app secret, and builds the Embedding API v3 tableau-viz from the Tableau host; the screen never holds the secret.

A Tableau Public viz is a view without a connection (kind tableau, target.public_url, rebuilt as https://public.tableau.com/views/WORKBOOK/VIEW) and takes rotate_sheets too. For a web page (kind web): a page of the web connection's site (url: an https address on a public host, with no target).

The browser on the edge opens it signed in, waits for the element wait_for when set and wait_ms milliseconds (up to 10000), and draws clip, a CSS selector, when set, else the whole page.

The image is drawn every refresh_s (60 to 86400, default 300) and shared by every screen of the same size, and only while a screen shows the view; when a capture fails the last image stays on glass. Set wait_for to an element that exists only when the page is signed in, so a sign-in page is never shown. Appends bi_view.created.


Operation: createBIViewInProject.

- Authentication: projectBearer
- Request body: BIViewWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| connection_id | optional | string |  |
| kind | optional | string | Required without a connection_id; otherwise it must match the connection. Allowed: "grafana", "powerbi", "tableau", "web" |
| name | required | string | minLength: 1 maxLength: 80 |
| url | optional | string | A /d/{uid}/... dashboard address on the connection's host. The server reads orgId, viewPanel (N or panel-N) or panelId, from, to, var-* and theme from it. Tableau: a view address on the connection's host (/views/W/V, /t/SITE/views/W/V, #/site/SITE/views/W/V) or Workbook/View. Web: the page's https address on a public host. maxLength: 2048 |
| target | optional | BITarget | What a view shows. Grafana with a connection: dashboard_uid (and optionally one panel). Power BI with a connection: workspace_id and report_id, with pages or rotate_all_pages. Tableau with a connection: view (Workbook/View), with rotate_sheets. Web with a connection: url. Without a connection: only public_url (and rotate_sheets for Tableau). |
| refresh_s | optional | integer | Grafana 30 to 86400; Power BI and Tableau 0 or 60 to 86400; web 60 to 86400. default: 300 minimum: 0 maximum: 86400 |
| rotate_s | optional | integer | Power BI and Tableau: 0 or 10 to 3600; required for a target that rotates pages or sheets. default: 0 minimum: 0 maximum: 3600 |
| fit | optional | string | Allowed: "contain", "cover", "fill" default: "contain" |
| wait_for | optional | string | Web: a CSS selector of an element that shows when the page is ready, and only when it is signed in. maxLength: 512 |
| wait_ms | optional | integer | Web: milliseconds to wait after the page is ready. minimum: 0 maximum: 10000 |
| clip | optional | string | Web: a CSS selector of the part of the page to draw. maxLength: 512 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/bi-views'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. | BIView (application/json); headers: ETag, Cache-Control |
| 400 | invalid_request, including a url that is not a dashboard address of the connection. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — the project's plan does not include BI dashboards; required_plan names the plan that does. | Problem (application/problem+json) |
| 404 | not_found, the connection is not in this project. | Problem (application/problem+json) |
| 409 | idempotency_mismatch. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/triggers

**List Triggers In Project**

Lists the project's CAP alert triggers, oldest first (at most 20), each with its ingest url, username and latest alert. The password is never returned here.


Operation: listTriggersInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/triggers'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's triggers. | TriggerList (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/triggers

**Create Trigger In Project**

Creates a CAP alert trigger: one inbound URL an emergency notification system (Everbridge, Rave, Alertus, IPAWS relays and other CAP 1.2 senders) posts alerts to, with HTTP Basic credentials.

The answer is the only place the password appears (with the rotate answer): store it in the sending system at once. target is a tag (every active screen with it when the alert arrives) or up to 500 screen_ids.

An alert takes every target screen with a default playlist over until the alert's <expires>, else for default_for_seconds (60 to 604800, default 3600); see POST /api/triggers/{id}/cap. An organization has at most 20 triggers across its projects (trigger_limit_reached). Appends trigger.created.

An exact retry with the same Idempotency-Key returns this same answer, password included, for twenty-four hours. A trigger whose selector reaches a protected screen is protected_target (409), and a failed read of screen protection is dependency_unavailable (500, retry the same request); a trigger is not available to a caller limited to screens.


Operation: createTriggerInProject.

- Authentication: projectBearer
- Request body: TriggerWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| kind | optional | string | Allowed: "cap", "overlay" default: "cap" |
| label | required | string | minLength: 1 maxLength: 80 |
| target | required | TriggerTarget | Exactly one of tag and screen_ids. |
| default_for_seconds | optional | integer | How long an alert without <expires> holds the screens: 60 to 604800, default 3600, for a CAP trigger; how long a post without duration holds its alert: 10 to 604800, default 600, for an overlay trigger. minimum: 10 maximum: 604800 |
| pull | optional | CapPull | CAP sources the platform reads for the trigger, with where the target screens are and which alerts count: the params of a cap-alert card placement (docs/feeds.md "CAP alerts"). Each source URL is fetched once per minute for every trigger and placement that names it. With no sources the platform's public feeds covering location are read (NWS, Canada NAAD, and MeteoAlarm for a location with area codes). The most severe matching alert takes the target screens over exactly as a pushed message would; when the sources show none (a cancel or an expiry) a pulled alert ends. A stale or failing source never ends an alert. location is required unless sources are given. |
| overlay | optional | TriggerOverlayWrite |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/triggers'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created, with its url, username and password. | TriggerWithPassword (application/json); headers: ETag, Cache-Control |
| 400 | invalid_request, including a malformed tag or both tag and screen_ids. | Problem (application/problem+json) |
| 409 | trigger_limit_reached, idempotency_mismatch, or protected_target when the selector reaches a screen in a protected screen folder. | Problem (application/problem+json) |
| 500 | dependency_unavailable - the check for protected screen folders could not run; send the same request again after a short wait. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/events

**List Events In Project**

Lists durable project events ascending by sequence.

Paging is contiguous: pass the returned next_cursor as after to continue exactly where the page stopped. next_cursor is the cursor of the last returned event while newer events already exist, and null at the end of the history. limit defaults to 50 and accepts 1-200; a value outside that range is refused with invalid_request and an errors[] member naming limit.

Offset paging is not supported. source=log lists the Player log instead: the condition reports Players send (runtime.reported) ascending by receipt time, filtered by screen_id and severity over a window from to (default the last 24 hours, at most 31 days).

Its cursors are opaque: lg1_ or ev1_ by where the server keeps the Player log, so pass back the one a page returned; a cursor of the other kind is refused with invalid_request naming the field. Player log records have no sequence, are not on GET /stream/project and are not delivered to webhooks.

A record is acknowledged when the Player log queue accepts it and is kept for the project's event retention. A poll with the last cursor and the default window follows the log. Player log records are not in the default (source=project) list. from and to are refused with invalid_request unless source is log.

The project list takes screen_id, playlist_id and application_id to read the events of one screen, playlist or application, all scoped to the project of the credential (an id names which events to read, never whose): screen_id keeps the events whose resource is that screen; playlist_id the events whose resource is that playlist plus the events that name it as details.playlist_id (a screen taking it); application_id the events whose resource is that application plus those whose resource carries it as application_id (its releases and key/value entries).

A value that is not an id of its kind is refused with invalid_request naming the field; playlist_id and application_id are refused with source=log, whose records belong to a screen only.

Both sources take resource_id, type and severity (a minimum), combined with each other, with the id filters and with paging; a Player log record is a runtime.reported record whose resource is its screen, so a resource_id that is not that screen or a type that excludes runtime.reported answers an empty page. before (or order=desc) pages newest first: next_cursor is then the cursor of the oldest returned item and passed back as before continues to older items.

Without from, a newest-first source=log page reaches past the default 24 hours to the oldest record the project's event retention keeps, at most 31 days below the page's upper edge per request. A newest-first source=log page may hold fewer than limit records, even none, with a next_cursor; null still ends it, at the end of the retained log.

A caller limited to screens (docs/screen-scope.md) reads a filtered feed: events of screens inside its scope, of the screen folders it can see (its own, the folders above and inside; screen.moved only when the destination is inside its scope), of campaigns, playlists, media, applications and cards, and playlist_change reviews.

Every other event is left out, so a page can hold fewer events than limit, even none, with a next_cursor; keep paging with it. source=log needs a screen_id inside its scope. Events about the project itself are not sent.


Operation: listEventsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| after | query | optional | A cursor from a previous response: ev1_ for project events (and the event streams). The Player log (source=log) pages with lg1_ or ev1_ cursors by where the server keeps it; treat them as opaque and pass back the one a page returned. A cursor of another kind is refused with invalid_request. |
| before | query | optional | Pages newest first, strictly older than this cursor: a next_cursor of a newest-first page, or any item cursor (ev1_ for project events; for source=log the opaque cursor its items carry). Empty or absent with order=desc starts at the newest item. |
| order | query | optional | asc pages oldest first with after; desc pages newest first with before. before implies desc; order=asc with before is refused. |
| limit | query | optional | Page size. A value outside 1-200 is refused with invalid_request and an errors[] member whose field is limit. |
| source | query | optional | project lists the sequenced project events. log lists the Player log: Player condition reports (runtime.reported), ascending by receipt time. |
| resource_id | query | optional | Only items whose resource.id is this id (a screen, playlist, media, webhook, agent or application id). With source=log it is the screen of the record. |
| type | query | optional | An exact event type such as playlist.updated, or a family prefix ending in a dot such as playlist. for every playlist.* type. |
| screen_id | query | optional | Only the events of this screen: with source=project those whose resource is the screen, with source=log its Player log records. Another id kind is refused with invalid_request. |
| playlist_id | query | optional | With source=project, only the events of this playlist: those whose resource is the playlist and those naming it as details.playlist_id. Refused with source=log. |
| application_id | query | optional | With source=project, only the events of this application: those whose resource is the application and those whose resource carries it as application_id (its releases and key/value entries). Refused with source=log. |
| severity | query | optional | Minimum severity: info is every item, warning keeps warnings and errors, error keeps errors. Both sources. |
| from | query | optional | With source=log, the start of the window (inclusive). Defaults to 24 hours before to; a newest-first page without from reaches back to the project's event retention instead. |
| to | query | optional | With source=log, the end of the window (exclusive). Defaults to now. The window is at most 31 days. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/events'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Durable events (source=project) or Player log records (source=log) | EventList (application/json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/project/capabilities

**Get Project Capabilities In Project**

The calling project's plan, independent advertiser/screens flags with their revision, the derived effective capability set, the plan features it has (plan_features) and its limits (limits). Never reflects a client claim; an unknown plan fails closed as forbidden.


Operation: getProjectCapabilitiesInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/project/capabilities'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project capabilities. | ProjectCapabilities (application/json); headers: Cache-Control |
| 403 | forbidden - the project plan is not in the catalog. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/billing/balance

**Get Billing Balance In Project**

The calling project's source-aware balance snapshot. All millicredit amounts are canonical decimal strings. It is a snapshot, not authority to pay; rails_available is false while card, tax and cash-out rail are unavailable.


Operation: getBillingBalanceInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/billing/balance'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Balance snapshot. | BillingBalance (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/billing/statement

**Get Billing Statement In Project**

Stable cursor pagination over immutable posted journal entries for the calling project. Holds are not posted entries. Corrections link to the original event instead of rewriting it. There is no arbitrary grant or payout mutation on this surface.


Operation: getBillingStatementInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| cursor | query | optional |  |
| limit | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/billing/statement'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Statement page. | BillingStatement (application/json) |
| 400 | invalid_request - limit is out of range. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/billing/usage

**Get Billing Usage In Project**

The calling project's use for one UTC month, with the bytes served and received (never charged), the storage charged above the free storage of the payer's plan (allowance.storage_bytes), and the API requests, billed project stream events and webhook delivery attempts counted against the free 10,000 a month. A project paying for itself reads its own pool.

A project another payer pays for shares that payer's pool with the payer's other projects; it reads its own share, and payer names the payer. The payer's pool total stays on the payer's billing reads.


Operation: getBillingUsageInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| period | query | optional | UTC month in YYYY-MM form; defaults to the current month. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/billing/usage'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Project usage. | ProjectBillingUsage (application/json); headers: Cache-Control |
| 400 | invalid_request - period is not a UTC month in YYYY-MM form. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/network

**Get Advertising Network In Project**

The calling seller's one network, including its resolved ScreenRig serving fee. Sellers cannot change that fee.


Operation: getAdvertisingNetworkInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/network'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Network. | AdvertisingNetwork (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/advertising/network

**Create Advertising Network In Project**

Creates the calling seller's one network if absent. Requires screens=true. Idempotent; repeating returns the same network.


Operation: createAdvertisingNetworkInProject.

- Authentication: projectBearer
- Request body: AdvertisingNetworkCreate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | string | maxLength: 120 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/network'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Network. | AdvertisingNetwork (application/json) |
| 403 | forbidden - the project lacks screen capability or has an unknown plan. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/advertising/network/rate

**Set Advertising Default Rate In Project**

Sets the seller's project default rate per 15 seconds of completed playback. A change that alters an accepted campaign's effective rate pauses that whole campaign with price_change_pending; a masked or no-op change does not. If-Match carries the network revision.


Operation: setAdvertisingDefaultRateInProject.

- Authentication: projectBearer
- Request body: AdvertisingRate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| If-Match | header | optional | Optional positive resource revision. Omit to write the current resource; a supplied stale revision is revision_conflict. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| rate_mcr_per_15s | required | McrString | Exact integer millicredit amount as a canonical decimal string. 1 credit is 1000 mcr. pattern: "^-?[0-9]+$" |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/network/rate'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Updated network. | AdvertisingNetwork (application/json) |
| 412 | revision_conflict - If-Match is stale. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/inventory

**List Advertising Inventory In Project**

The calling seller's opted-in inventory.


Operation: listAdvertisingInventoryInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/inventory'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Inventory. | AdvertisingInventoryList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/slots

**List Advertising Slots In Project**


Operation: listAdvertisingSlotsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/slots'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Slots. | AdvertisingSlotList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/advertising/slots

**Create Advertising Slot In Project**

Creates a reusable seller slot. Images are 5-30 seconds and videos at most the platform ceiling of 120 seconds. Ads are always muted.


Operation: createAdvertisingSlotInProject.

- Authentication: projectBearer
- Request body: AdvertisingSlotWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | string | minLength: 1 maxLength: 120 |
| enabled | optional | boolean |  |
| accepted_media | required | array |  |
| max_image_duration_ms | required | integer | minimum: 5000 maximum: 30000 |
| max_video_duration_ms | required | integer | minimum: 1000 maximum: 120000 |
| rate_override_mcr_per_15s | optional | inline |  |
| vast_tag_url | optional | inline | The seller's own VAST 2-4 ad tag, tried when no invited campaign fills this slot on the seller's own screens. HTTPS on port 443 or 8443 to a public host only. The backend fetches it, follows up to five Wrappers, ingests the chosen progressive MP4 or WebM MediaFile into the project's media, and fires Impression, creativeView and start on an accepted start, complete on an accepted completion, and Error on failure. Quartile trackers are not fired. Macros [CACHEBUSTING], [TIMESTAMP], [SCREENRIG_SCREEN_ID], [SCREENRIG_SLOT_ID] and [SCREENRIG_PAGE_ID] are expanded. The slot must accept video. Null, absent, or empty clears the tag. Buyers never see it. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/slots'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Slotslot. | AdvertisingSlot (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/memberships

**List Advertising Memberships In Project**


Operation: listAdvertisingMembershipsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/memberships'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Memberships. | AdvertisingMembershipList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/networks

**List Advertising Joined Networks In Project**

Networks the calling advertiser currently belongs to, with the scope and review policy it was granted. Screen count is not a guaranteed impression count.


Operation: listAdvertisingJoinedNetworksInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/networks'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Joined networks. | AdvertisingJoinedNetworkList (application/json) |
| 403 | forbidden - the project lacks the advertiser flag. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/creatives

**List Advertising Creatives In Project**


Operation: listAdvertisingCreativesInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/creatives'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Creatives. | AdvertisingCreativeList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/advertising/creatives

**Create Advertising Creative In Project**

Binds one ready owned media revision to an immutable creative. Does not copy bytes, re-upload, or re-charge generation. Unknown or non-ready media is rejected.


Operation: createAdvertisingCreativeInProject.

- Authentication: projectBearer
- Request body: AdvertisingCreativeCreate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| media_id | required | string |  |
| copy | optional | string | maxLength: 500 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/creatives'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Creative. | AdvertisingCreative (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/campaigns

**List Advertising Campaigns In Project**


Operation: listAdvertisingCampaignsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/campaigns'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Campaigns. | AdvertisingCampaignList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/advertising/campaigns

**Create Advertising Campaign In Project**

Creates a draft campaign. Drafts hold no money, reserve nothing, and authorize no delivery. Activation requires an explicitly accepted quote.


Operation: createAdvertisingCampaignInProject.

- Authentication: projectBearer
- Request body: AdvertisingCampaignDraft

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | string | minLength: 1 maxLength: 120 |
| category | optional | AdvertisingCategory | The advertiser category. Two ads of one category never play in the same ad break on a screen; an ad without a category is never separated. Allowed: "alcohol", "apparel", "automotive", "beauty", "cannabis", "consumer_electronics", "education", "entertainment", "financial_services", "food_and_beverage", "gambling", "government", "grocery", "health_and_pharma", "home_and_garden", "insurance", "jobs", "legal", "media", "political", "quick_service_restaurants", "real_estate", "restaurants", "retail", "sports_and_fitness", "technology", "telecom", "tobacco_and_vaping", "travel", "utilities" |
| daily_cap_mcr | required | McrString | Exact integer millicredit amount as a canonical decimal string. 1 credit is 1000 mcr. pattern: "^-?[0-9]+$" |
| lifetime_cap_mcr | required | McrString | Exact integer millicredit amount as a canonical decimal string. 1 credit is 1000 mcr. pattern: "^-?[0-9]+$" |
| image_duration_ms | optional | integer | Campaign-wide image dwell time. Videos keep their server-verified duration. default: 10000 minimum: 5000 maximum: 30000 |
| max_play_price_mcr | optional | inline | Positive maximum completed-play price. Omit for no additional ceiling. |
| flight_start | required | string | format: "date-time" |
| flight_end | required | string | format: "date-time" |
| networks | required | array |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/campaigns'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Draft campaign. | AdvertisingCampaign (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/reviews

**List Advertising Reviews In Project**

Creative versions submitted to the calling seller for review. A seller sees only exact submitted creative, never the buyer's library.


Operation: listAdvertisingReviewsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/reviews'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Reviews. | AdvertisingReviewList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/reports/spend

**Get Advertising Spend In Project**

Buyer-scoped spend and delivery counts for one campaign. Amounts are decimal mcr strings. It never exposes the seller's income, bank status, or project-wide usage.


Operation: getAdvertisingSpendInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| format | query | optional | json (default) returns the report as JSON. csv, xlsx and pdf return the same rows as a file attachment (every page of a paged report, up to 50000 rows). |
| campaign_id | query | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/reports/spend?campaign_id=CAMPAIGN_ID'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Spend report. | AdvertisingSpendReport (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/reports/delivery

**Get Advertising Delivery In Project**

Seller-scoped gross, serving fee and net for one window, with its own delivery evidence. It never exposes the buyer's wallet or other networks. Only authorized completed-play evidence establishes billable delivery. The window is at most 92 days; a wider window is invalid_request.

When it holds more than 5000 settlements the response sets truncated, totals and counts still cover all of them and rows lists only the earliest 5000.


Operation: getAdvertisingDeliveryInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| format | query | optional | json (default) returns the report as JSON. csv, xlsx and pdf return the same rows as a file attachment (every page of a paged report, up to 50000 rows). |
| from | query | required |  |
| to | query | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/advertising/reports/delivery?from=FROM&to=TO'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Delivery report. | AdvertisingDeliveryReport (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/playback

**List Playback Aggregates In Project**

Daily playback aggregates.

JSON returns at most 200 rows and is not rate limited. format=csv (or Accept text/csv without application/json) streams every matching row inside day_from to day_to (inclusive UTC days; defaults day_to today and day_from 30 days earlier; at most 366 days, else invalid_request) as CSV with a fixed header row (PlaybackAggregateCsv); only the CSV form spends the playback-export-org (30 per minute) and playback-export-ip (60 per minute) budgets.

Either form is one metered API request. Responses carry Vary Accept. A caller limited to screens (docs/screen-scope.md) must send screen_id and it must be a screen inside its scope (outside_screen_scope otherwise); the CSV forms follow the same rule.


Operation: listPlaybackAggregatesInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| screen_id | query | optional |  |
| media_id | query | optional |  |
| day | query | optional | UTC calendar day of the aggregate. |
| day_from | query | optional | First UTC day, inclusive. Excludes day. With day_to spans at most 366 days. CSV defaults it to 30 days before day_to. |
| day_to | query | optional | Last UTC day, inclusive. Excludes day. CSV defaults it to today (UTC). |
| format | query | optional | json (default), csv, xlsx or pdf. Omitted, an Accept header naming text/csv but not application/json selects csv. xlsx and pdf write the first 50000 rows of the same list. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/playback'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Daily playback aggregates for the calling project, images and videos alike. One row per screen, media, and UTC day. Newest days first. Identifiers filter the caller's own rows and are never a cross-project lookup. | PlaybackAggregateList (application/json), PlaybackAggregateCsv (text/csv); headers: Vary, RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/playback/plays

**List Playback Plays In Project**

Per-play records: one row per accepted playback.media_started (or legacy playback.video_started) and one row per accepted playback.application_started (release_id set, media fields empty) of the calling project, oldest first by received_at, ties by insertion. from (inclusive) and to (exclusive) bound received_at; to defaults to now, from to 24 hours before to, and the range is at most 31 days (invalid_request otherwise).

The newest 5 seconds are not returned yet: the effective to is at most now minus 5 seconds (settle lag), so a live tail never skips a row that commits behind its cursor. screen_id, media_id, playlist_id (the playlist the start was shown from), and tag (a screen tag the screen carried when the play was received) filter.

A playlist_id page may hold fewer than limit rows, even none, with a next_cursor when a long stretch of plays of other playlists was scanned; null still ends the range.

JSON is a page of at most limit rows (default 200, at most 1000) with next_cursor, null at the end of the range; pass it back as cursor with the same filters. order=desc (or before) pages newest first within the same range: next_cursor is then the cursor of the oldest returned row, null at the start of the range, and is passed back as before (exclusive) with the same filters; order=asc with before is refused with invalid_request naming order, and cursor with a newest-first page naming cursor.

A newest-first page may hold fewer than limit rows, even none, with a next_cursor; null still ends it.

A later page never skips a row that existed when the earlier page was read and does not repeat one, except that a start the Player re-sent after an unacknowledged report (the same play_id, received a moment later) can appear again on a later page; the CSV stream lists it once. format=csv (or Accept text/csv without application/json) streams every play from cursor to the end of the range as CSV (PlaybackPlayCsv), newest first below before with order=desc; limit is refused with CSV.

Metering is per request: every JSON page costs one API request credit, while one CSV stream of the whole range costs one. Every plays request (page or stream) spends the playback-export-org (30 per minute) and playback-export-ip (60 per minute) budgets.

A stream bounds each write (15 seconds) and ends when the client disconnects; if it fails after the 200 header the connection is aborted, so an export that reads to a clean end is complete. Responses carry Vary Accept. Plays are kept for the plan's event retention (30 days is the shipped floor; plan-based retention uses the plan's own 7- or 90-day window).

A caller limited to screens (docs/screen-scope.md) must send screen_id and it must be a screen inside its scope (outside_screen_scope otherwise); the CSV forms follow the same rule.


Operation: listPlaybackPlaysInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| from | query | optional | Inclusive lower bound on received_at. Defaults to 24 hours before to. |
| to | query | optional | Exclusive upper bound on received_at. Defaults to now. to - from is at most 31 days. |
| screen_id | query | optional |  |
| media_id | query | optional |  |
| playlist_id | query | optional | Only plays shown from this playlist. |
| tag | query | optional | A screen tag the screen carried when the play was received. |
| cursor | query | optional | next_cursor of the previous page, with the same filters. Opaque. |
| order | query | optional | asc pages oldest first with cursor; desc pages newest first with before. before implies desc; order=asc with before is refused. |
| before | query | optional | Pages newest first, strictly older than this position: the next_cursor of a newest-first page, with the same filters. Opaque. |
| limit | query | optional | JSON page size. Refused with CSV. |
| format | query | optional | json (default), csv, xlsx or pdf. Omitted, an Accept header naming text/csv but not application/json selects csv. xlsx and pdf write the first 50000 rows of the same list. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/playback/plays'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | A page of plays, or the CSV stream. | PlaybackPlayList (application/json), PlaybackPlayCsv (text/csv); headers: Cache-Control, Vary, RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/playback/totals

**Get Playback Totals In Project**

Play totals of the calling project over inclusive UTC days, read from the daily rollups: by=day returns one row per day with plays (screen_id or media_id narrows it to one screen or media), by=screen and by=media rank the busiest screens or media (top, at most 100), and by=screen_day returns the day series of the top screens (top at most 10). day_to defaults to today (UTC) and day_from to 29 days before it.

A day series spans at most 366 days (92 when narrowed to a screen or media); a ranking at most 92. Counts include every start the project has recorded, including those stored before play rollups existed.

Rollups follow ingestion by up to about 21 minutes. truncated is true when a ranking was built from slices of a window too large for one read: it covers the best keys of each slice. Not rate limited. One metered API request.

A caller limited to screens (docs/screen-scope.md) must send screen_id and it must be a screen inside its scope (outside_screen_scope otherwise); the CSV forms follow the same rule.


Operation: getPlaybackTotalsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| format | query | optional | json (default) returns the report as JSON. csv, xlsx and pdf return the same rows as a file attachment (every page of a paged report, up to 50000 rows). |
| by | query | optional | day: plays per day; screen, media: the busiest keys; screen_day: the day series of the top screens. |
| day_from | query | optional | First UTC day, inclusive. Defaults to 29 days before day_to. A day series spans at most 366 days (92 when narrowed to a screen or media), a ranking at most 92. |
| day_to | query | optional | Last UTC day, inclusive. Defaults to today (UTC). |
| screen_id | query | optional |  |
| media_id | query | optional |  |
| top | query | optional | How many screens or media a ranking returns (at most 10 for by=screen_day). Refused with by=day. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/playback/totals'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Totals of the requested grouping. | PlaybackTotals (application/json); headers: Cache-Control |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/playback/completeness

**Get Playback Completeness In Project**

How many of the plays the screens made reached the backend, and where the missing ones were. Players number their media starts, so a start that never arrived leaves a hole in the numbering, for example when a screen stays offline past its 24 hour report queue or the queue drops reports.

Over the inclusive UTC days day_from to day_to (day_to defaults to today, UTC, and day_from to 29 days before it; the window spans at most 92 days) the answer holds measured_plays, the numbered plays that arrived; missing_plays, the numbers the Players skipped; completeness, measured_plays over their sum, null when both are 0; days, the same three figures for each day that has any, oldest first, a hole counted on the UTC day of the play after it; and gaps, the holes newest first, each with its screen_id, its day, from and to (the start times of the plays on either side of it) and missing_plays. screen_id narrows days and gaps to one screen; limit (default 100, at most 500) caps gaps, and truncated is true when more exist.

A switched-off screen makes no plays and leaves no gap. Plays without a number (older Players, starts stored before play numbers) are not measured: they are in play_count of GET /api/playback/totals and in none of these figures. Not rate limited. One metered API request.

A caller limited to screens (docs/screen-scope.md) must send screen_id and it must be a screen inside its scope (outside_screen_scope otherwise); the CSV forms follow the same rule.


Operation: getPlaybackCompletenessInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| format | query | optional | json (default) returns the report as JSON. csv, xlsx and pdf return the same rows as a file attachment (every page of a paged report, up to 50000 rows). |
| day_from | query | optional | First UTC day, inclusive. Defaults to 29 days before day_to. With day_to spans at most 92 days. |
| day_to | query | optional | Last UTC day, inclusive. Defaults to today (UTC). |
| screen_id | query | optional |  |
| limit | query | optional | The most gaps to list, newest first. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/playback/completeness'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The completeness of the window and its gaps. | PlaybackCompleteness (application/json); headers: Cache-Control |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/health/trend

**Get Health Trend In Project**

The project's fleet health by UTC hour: how many screens reported, and how many of them were hot (at or above 80 °C), had a disconnected display, or were crashing (3 or more crashes in 24 hours). from (inclusive) and to (exclusive) default to the last 24 hours, at most 93 days; a page holds at most limit rows (default 100, at most 500) with next_cursor, null at the end.

Hours follow reports by up to about 5 minutes. Refused to a caller limited to screens (outside_screen_scope); GET /api/screens/{id}/health/history is the per-screen form.


Operation: getHealthTrendInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| format | query | optional | json (default) returns the report as JSON. csv, xlsx and pdf return the same rows as a file attachment (every page of a paged report, up to 50000 rows). |
| from | query | optional | Inclusive lower bound on the report time. Defaults to 24 hours before to. |
| to | query | optional | Exclusive upper bound on the report time. Defaults to now. |
| limit | query | optional | Page size. |
| cursor | query | optional | next_cursor of the previous page, with the same filters. Opaque. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/health/trend'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | A page of hourly fleet health. | HealthTrend (application/json); headers: Cache-Control |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/uptime/daily

**Get Project Uptime In Project**

Daily availability of the project's screens over inclusive UTC days (defaults and range as GET /api/screens/{id}/uptime). Each row sums the online and offline seconds of the screens with a recorded state that day; coverage is the share of those screens' day with a recorded state.

Intervals count when they end, so today's row covers intervals that ended so far; the live online count is in the dashboard summary. Refused to a caller limited to screens (outside_screen_scope); GET /api/screens/{id}/uptime is the per-screen form.


Operation: getProjectUptimeInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| format | query | optional | json (default) returns the report as JSON. csv, xlsx and pdf return the same rows as a file attachment (every page of a paged report, up to 50000 rows). |
| day_from | query | optional | First UTC day, inclusive. Defaults to 29 days before day_to. With day_to spans at most 366 days. |
| day_to | query | optional | Last UTC day, inclusive. Defaults to today (UTC). |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/uptime/daily'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Daily uptime rows, oldest first. | ProjectUptime (application/json); headers: Cache-Control |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/card-types

**List Card Types In Project**

Discover built-in cards from this environment's published catalogue, merged with the vendored floor. Refreshes and schema verification happen in the background; this route never calls the edge bucket. Each entry lists its current copy, its tier and the layouts that tier allows, and every served major.


Operation: listCardTypesInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/card-types'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Built-in card catalogue, ordered by id | CardCatalogue (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/card-data

**List Card Data In Project**

Lists the project's card data documents, at most 200 in ascending key order. next_cursor is the last returned key; pass it back as after for the next page, null when there are no more. Card data documents hold the dynamic properties of card placements (menu sections, ticker items, KPI tiles); the placement's data key names one.


Operation: listCardDataInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| after | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/card-data'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Card data documents | CardDataList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/media

**List Media In Project**

Lists ready media newest first, at most 1000 per page. folder keeps only the files directly inside one media folder (its ID), or with root only those at the library root; a well-formed folder ID that names no folder lists nothing. next_cursor is the position of the last returned row while more rows remain; pass it back as after, with the same filters, for the next page. next_cursor is null on the last page.


Operation: listMediaInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| tag | query | optional | Exact media tag. Untagged objects are omitted when this filter is present. |
| primitive | query | optional |  |
| folder | query | optional | A media folder ID keeps only the files directly inside that folder, not those of its subfolders; root keeps only the files at the library root. Omitted lists every file. |
| ids | query | optional | Comma-separated media ids, at most 200 distinct; more is invalid_request at ids. Keeps only those files: an id that names no ready file is absent, and the other filters and paging still apply. An empty value lists nothing. |
| library | query | optional | true lists the organization's Library project instead of this project: its items are read-only here and carry library true. An organization with no Library, or the Library project itself, lists nothing with true. |
| after | query | optional | next_cursor of the previous page of the same list, with the same filters. Omitted starts at the newest row. A value that is not such a cursor is invalid_request. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Media | MediaList (application/json) |

### POST /api/projects/{project_id}/media/generations

**Create Media Generation In Project**

Generate one still image from a prompt, store it as ready private project media, and bill its usage. The POST answers 202 at once with the Operation, with or without Prefer: respond-async, because a proxy in front of the API drops a request that sends no byte for 100 s and a high-quality still can take longer.

Poll GET /api/operations/{id} until it is succeeded, when its result is {media_id, usage: {quality, credits, usd}} (MediaGenerationUsage), or failed, when its error is the problem. A refusal decided before any vendor work (payment_required, reseller_generation_limit, rate_limited, not_ready, invalid_request) still answers the POST.

It never includes image bytes, b64_json, object keys, signed URLs, vendor URLs, vendor cost, vendor tokens, or the deployment model name. The prompt is customer bytes and is not echoed.

Before the prompt reaches the image vendor the server appends fixed framing requirements to it: the artwork must fill the frame edge to edge, and no television, monitor, screen, display, bezel, frame, border, mounting, stand, wall, room, device mockup, drop shadow, or photograph of an installed sign may be depicted, and any wording in the caller's prompt describing where the sign will hang or what device will show it is treated as placement metadata rather than subject matter.

Decorative rules and borders inside the design itself remain available. The caller's prompt is never rewritten or stripped and is stored unchanged. Idempotency-Key replays the same Operation, never a second billed attempt. Clients do not PUT or commit generated bytes.

The stored rendition is lossy WebP (quality 90) at the exact aspect size with a 1080 px short edge (16:9 is 1920x1080, 9:16 1080x1920, 1:1 1080x1080, 4:3 1440x1080, 3:4 1080x1440, 3:2 1620x1080, 2:3 1080x1620); the vendor canvas is centre-cropped and resampled to that size.

The filename is distinctive per generation, generated-16x9-1a2b3c4d.webp, with the suffix taken from the media id. Fetch the bytes with GET /api/media/{id}/content. Own-gen-then-upload remains valid.

On Standard, remaining that cannot cover twice the vendor cost of the still, rounded up to whole credits, after the intro floor (negative 1,000,000 credits until 2027-01-01, then zero) returns payment_required, except SCREENRIG__ENVIRONMENT=development, and admission before the vendor call requires remaining above that floor by the per-quality minimum: 200 credits for low, 800 for medium, 2000 for high.

Other plans are never refused for their balance.

An organization billed through a reseller is refused with reseller_generation_limit (402) once its reseller's delegated customers have been billed the month's image generation limit ($100 at list price while the reseller awaits review, $1,000 after, counted per UTC month), unless the organization's own credit covers the image; a generation already admitted finishes.

Empty selected-vendor configuration returns not_ready. Project bearer only. One in-flight generation per organization; further parallelism is rate_limited.


Operation: createMediaGenerationInProject.

- Authentication: projectBearer
- Request body: MediaGenerationRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |
| Prefer | header | optional | Optional. The route answers 202 with the Operation at once with or without it; poll the Operation. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| prompt | required | string | minLength: 1 maxLength: 4000 |
| aspect_ratio | optional | string | Allowed: "1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3" default: "16:9" |
| quality | optional | string | Allowed: "low", "medium", "high" default: "medium" |
| tag | optional | string | pattern: "^[A-Za-z0-9]{1,32}$" |
| references | optional | array |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media/generations'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 202 | The generation was accepted and runs on; poll GET /api/operations/{id} until the Operation is succeeded (result media_id and usage) or failed (error). The same Idempotency-Key answers with the same Operation. | Operation (application/json); headers: Preference-Applied, RateLimit-Policy, RateLimit |
| 402 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 500 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/media-folders

**List Media Folders In Project**

Lists every media folder in the project, at most 1000, ordered by name. Folders organize the media library, and a playlist selector by:folder plays the files directly inside one folder. A folder's parent_id is null at the library root.

Build the tree from parent_id; a file's folder is Media.folder_id. library=true lists the organization Library's folders instead (read-only; a by:folder selector may name one).


Operation: listMediaFoldersInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| library | query | optional | true lists the organization's Library project instead of this project: its items are read-only here and carry library true. An organization with no Library, or the Library project itself, lists nothing with true. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media-folders'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Media folders | MediaFolderList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/media-folders

**Create Media Folder In Project**

Creates one media folder at the library root (parent_id null or absent) or inside parent_id. Names are trimmed, hold 1 to 100 Unicode characters with no controls, are not "." or "..", and may repeat. An organization holds at most 1000 folders across its projects (quota_exceeded) nested at most 10 deep (invalid_request). Emits media.folder_created.


Operation: createMediaFolderInProject.

- Authentication: projectBearer
- Request body: MediaFolderCreate

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| name | required | string | Trimmed; 1 to 100 Unicode characters, no controls, not . or .. minLength: 1 maxLength: 100 |
| parent_id | optional | inline | The folder to create it in; null or absent for the library root. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media-folders'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created folder. | MediaFolder (application/json); headers: ETag |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/media/move

**Move Media In Project**

Moves files (media_ids) and folders (folder_ids) into one folder (folder_id), or to the library root when folder_id is null, in one all-or-nothing request of at most 1000 ids. Every id must exist (not_found). A folder cannot move into itself or a folder inside it, and the move must keep every folder within 10 levels (invalid_request).

Items already in the destination are left as they are; each moved file and folder advances its revision. Moving files changes what the by:folder selectors of their old and new folders play, from each screen's next selector resolve; moving a folder changes no folder's own files. Emits media.moved.

A Propose credential's move makes each moved file pending and queues its media review, and approved playlists' selectors resolve it by the folder it was approved with until a person approves the change (docs/reviews.md).


Operation: moveMediaInProject.

- Authentication: projectBearer
- Request body: MediaMove

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| folder_id | required | inline | Destination folder; null for the library root. |
| media_ids | optional | array |  |
| folder_ids | optional | array |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media/move'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | How many files and folders moved. | MediaMoveResult (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/media/uploads

**Create Media Upload In Project**


Operation: createMediaUploadInProject.

- Authentication: projectBearer
- Request body: MediaUploadDeclaration

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |
| project_id | query | optional | Required with dashboardSession: the project where the file was queued. Must match the session's selected project, otherwise resource_conflict. This binds upload admission across tabs. The returned upload id remains project-scoped for byte upload and commit. Omitted with a project bearer. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| filename | required | string | Name of the bytes being uploaded, as they will be sent: a bare file name of at most 255 UTF-8 bytes, without a directory, backslash or control characters. minLength: 1 maxLength: 255 |
| source_filename | optional | string | Caller's original file name before any client-side transcode. Bare file name only, at most 255 UTF-8 bytes. Stored verbatim on the ready media as source_filename and used to derive filename. minLength: 1 maxLength: 255 |
| content_type | required | string | Allowed: "image/png", "image/jpeg", "image/webp", "image/gif", "video/mp4", "video/webm", "audio/mpeg" |
| bytes | required | integer | minimum: 1 maximum: 1073741824 |
| sha256 | required | string | pattern: "^[a-f0-9]{64}$" |
| tag | optional | string | pattern: "^[A-Za-z0-9]{1,32}$" |
| folder_id | optional | string | The media folder the ready media lands in; omitted lands it at the library root. The folder must exist when the upload is declared (invalid_request at folder_id otherwise) and when it commits (the operation fails with invalid_request: declare it again). An uploader (docs/uploaders.md) must send one of its folders (outside_upload_scope otherwise). |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media/uploads'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Short-lived signed authorization for exactly one private object PUT. Send the returned method and every returned header verbatim. This is also how the dashboard uploads. Its browser PUTs the bytes straight to the object store, never through the API, because a proxy in front of the API refuses a request body over 100 MB. A browser sets Content-Length itself from the body, so it sends every returned header except that one. | MediaUploadSession (application/json); headers: Cache-Control, RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/selectors/preview

**Preview Selector In Project**

Read-only preview of current project-owned ready image or video matches. Returns filename ordering and normalized policy; random playback shuffles on each new pass on the Player. Does not advance cursors, mint content grants, or alter manifests. More than 1024 matches returns invalid_request.


Operation: previewSelectorInProject.

- Authentication: projectBearer
- Request body: SelectorPreviewInput

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| timezone | optional | string | IANA zone media availability is read in; default UTC. |
| rect | optional | object | The primitive's rect size; required with selector.fit aspect. |
| primitive | required | string | Allowed: "image", "video" |
| selector | required | inline |  |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/selectors/preview'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Current selector matches. | SelectorPreview (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/feedback/bugs

**List Bug Reports In Project**

Returns the calling project's own bug reports, newest first, up to one hundred. Another project's submissions are never returned. Read an owned thread by its feedback ID. There is no pagination cursor in v1. Each submission carries its status and the staff/agent replies, oldest first.


Operation: listBugReportsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/feedback/bugs'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Bug reports owned by the calling project. | FeedbackList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/feedback/bugs

**Report Bug In Project**

Records one bug report against the calling project. A submission's own fields cannot be updated or deleted; add a correction or more information with a reply on the same feedback thread.

An exact retry carrying the same Idempotency-Key returns the original submission for twenty-four hours instead of creating a duplicate, and a different body under that key returns idempotency_mismatch. Every submission is screened before it is stored.

A body under 12 characters or 3 words returns submission_too_short; text that describes no problem or request (random characters, keyboard mash) returns submission_unclear; spam, link-heavy text, prompt injection and malicious text return submission_rejected.

A refused submission is not stored, its problem detail never repeats the submitted text, and an exact retry under the same Idempotency-Key returns the same refusal.

Never place credentials, cookies, Authorization headers, completion nonces, signed URLs, or object keys in title, body, or context; credential-shaped text (ScreenRig tokens, JWTs, bearer tokens, cloud and API keys, private keys) is replaced with [redacted] before the submission is stored, and the response carries the redacted text.

When the screening model is briefly unavailable the submission is accepted and held for a background re-screen; after a few such acceptances in an hour the project gets submission_screening_unavailable (500, Retry-After) and retries the same request later.


Operation: reportBugInProject.

- Authentication: projectBearer
- Request body: FeedbackWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| title | required | string | Single-line summary. Leading and trailing whitespace is trimmed, and control characters are rejected. minLength: 1 maxLength: 120 |
| body | required | string | Full description. Line feed and tab are the only accepted control characters. minLength: 1 maxLength: 4000 |
| context | optional | FeedbackContext | Closed diagnostic envelope. Every member is an optional constrained scalar, nesting is structurally impossible, and an unknown member is rejected with invalid_request. The shape exists so a client cannot persist argument values, credentials, or free-form environment data through it. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/feedback/bugs'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Stored bug report. An exact idempotent retry returns this same submission. | FeedbackSubmission (application/json); headers: RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 422 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/feedback/features

**List Feature Requests In Project**

Returns the calling project's own feature requests, newest first, up to one hundred. Another project's submissions are never returned. Read an owned thread by its feedback ID. There is no pagination cursor in v1. Each submission carries its status and the staff/agent replies, oldest first.


Operation: listFeatureRequestsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/feedback/features'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Feature requests owned by the calling project. | FeedbackList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/feedback/features

**Request Feature In Project**

Records one feature request against the calling project. A submission's own fields cannot be updated or deleted; add a correction or more information with a reply on the same feedback thread.

An exact retry carrying the same Idempotency-Key returns the original submission for twenty-four hours instead of creating a duplicate, and a different body under that key returns idempotency_mismatch. Every submission is screened before it is stored.

A body under 12 characters or 3 words returns submission_too_short; text that describes no problem or request (random characters, keyboard mash) returns submission_unclear; spam, link-heavy text, prompt injection and malicious text return submission_rejected.

A refused submission is not stored, its problem detail never repeats the submitted text, and an exact retry under the same Idempotency-Key returns the same refusal.

Never place credentials, cookies, Authorization headers, completion nonces, signed URLs, or object keys in title, body, or context; credential-shaped text (ScreenRig tokens, JWTs, bearer tokens, cloud and API keys, private keys) is replaced with [redacted] before the submission is stored, and the response carries the redacted text.

When the screening model is briefly unavailable the submission is accepted and held for a background re-screen; after a few such acceptances in an hour the project gets submission_screening_unavailable (500, Retry-After) and retries the same request later.


Operation: requestFeatureInProject.

- Authentication: projectBearer
- Request body: FeedbackWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| title | required | string | Single-line summary. Leading and trailing whitespace is trimmed, and control characters are rejected. minLength: 1 maxLength: 120 |
| body | required | string | Full description. Line feed and tab are the only accepted control characters. minLength: 1 maxLength: 4000 |
| context | optional | FeedbackContext | Closed diagnostic envelope. Every member is an optional constrained scalar, nesting is structurally impossible, and an unknown member is rejected with invalid_request. The shape exists so a client cannot persist argument values, credentials, or free-form environment data through it. |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/feedback/features'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Stored feature request. An exact idempotent retry returns this same submission. | FeedbackSubmission (application/json); headers: RateLimit-Policy, RateLimit |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 422 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/service-clients

**List Service Clients In Project**

The project's service clients (spec: a customer's server, script or CI job owned by the project), newest first, live and revoked, with their public keys, secret ids and usage; never a secret or its digest. Needs the project capability.


Operation: listServiceClientsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/service-clients'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The service client. | ServiceClientList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/service-clients

**Create Service Client In Project**

Creates a service client owned by the project with a name, capabilities (omitted means all six; each must be one the manager holds, else 403), access (read, the default, or manage), and a public JWK key (OKP Ed25519, EC P-256 or RSA of at least 2048 bits) and/or secret true. The secret is returned once, here only. At most 50 live clients per organization.

Agents need identity scope, live identity Manage, their own Manage grant with the project capability on the project, and a live organization admin role; a person uses the dashboard session with project_id and a fresh proof (kind service_client_change, subject the client id, or the project id when creating) in the body. A service token is refused 403.

A member limited to screens does not connect agents, issue credentials or manage the project's people: outside_screen_scope (ask the project owner).


Operation: createServiceClientInProject.

- Authentication: agentIdentityBearer
- Request body: ServiceClientChange

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| project_id | optional | string | Dashboard session only. |
| proof | optional | DashboardProof |  |
| name | optional | string | minLength: 1 maxLength: 80 |
| capabilities | optional | array |  |
| access | optional | string | Manage needs a person who can approve reviews (403 approver_required). Allowed: "read", "propose", "manage" |
| key | optional | object | A public JWK: OKP Ed25519, EC P-256 or RSA of at least 2048 bits. |
| secret | optional | boolean | Generate a secret (creation only). |
| upload_scope | optional | UploadScope | What an uploader may use (docs/uploaders.md): the playlists it changes pages of and the media folders it uploads into. Folders are exactly the listed ones, never their subfolders. An id that names no live object of the project when written is invalid_request at its position; a later deletion leaves a dead id, which grants nothing. The scope is read on each request, so a change applies to the next one. |

#### Request template

~~~sh
curl --request POST --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/service-clients'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | The service client. | ServiceClientResult (application/json) |
| 402 | feature_not_in_plan — an upload scope makes an uploader, and the project's plan does not include uploaders; required_plan names the plan that does. Narrowing a scope and removing it are always allowed. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/media-sync-accounts

**List Media Sync Accounts In Project**

Lists the project's media sync accounts, newest first (200 per page; next_cursor continues). Each shows its tenant and client id, its state and its last test; the client secret is never returned, only whether one is set and its last four characters.


Operation: listMediaSyncAccountsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| cursor | query | optional | The next_cursor of the previous page. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media-sync-accounts'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's accounts. | MediaSyncAccountList (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/media-sync-accounts

**Create Media Sync Account In Project**

Adds an account that syncs read through. kind microsoft365 stores the credentials of the project's own Microsoft Entra app, so syncs can read a SharePoint or OneDrive for work folder: config.tenant_id is the tenant's directory id or primary domain (common, organizations and consumers are refused), config.client_id the app's application id, and secret the value of one of the app's client secrets; the app needs the Microsoft Graph application permission Sites.Selected (granted on each site) or Files.Read.All, with admin consent.

Instead of those three, config.connection_id names a Microsoft connection of the project (createConnection), in either mode, which signs in and holds the secret. kind canva takes only a name: the account is pending_consent and the answer carries authorize_url, a one-time link (valid for authorize_expires_at) that the person who owns the Canva account opens, signed in to Canva, to connect it through screenRIG's Canva integration.

The grant belongs to the project, so its syncs outlive the person who connected it.

A server without the Canva integration answers 409 canva_not_configured. kind dropbox works like canva: the account is pending_consent and authorize_url is a one-time link that the person who owns the Dropbox account opens, signed in to Dropbox, to allow read-only access (files.metadata.read, files.content.read, account_info.read, sharing.read).

It goes through screenRIG's Dropbox app, or through the customer's own Dropbox app when config.app_key names it (PKCE, so no app secret; the app must list https://API_ORIGIN/api/dropbox/callback as a redirect URI).

A server without screenRIG's Dropbox app answers 409 dropbox_not_configured to a dropbox account with no app_key. kind google_drive is ready at once and reads the folders shared with config.service_account_email: screenRIG's service account, or the customer's own when secret is that service account's JSON key.

A folder shared with screenRIG's service account syncs only when a member of the project with a verified email shared it or owns it. A server without screenRIG's service account answers 409 google_drive_not_configured to a google_drive account with no key. A secret is sealed at rest and never returned or logged. Appends media_sync_account.created.

An exact retry with the same Idempotency-Key returns the same account (a Canva or Dropbox retry gets a new link).


Operation: createMediaSyncAccountInProject.

- Authentication: projectBearer
- Request body: MediaSyncAccountWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| kind | required | string | Allowed: "microsoft365", "canva", "dropbox", "google_drive" |
| name | required | string | minLength: 1 maxLength: 80 |
| config | optional | object | For microsoft365, either tenant_id and client_id (both) with secret, or connection_id alone: the id of a Microsoft connection of this project. For dropbox, app_key optionally names the customer's own Dropbox app. Not accepted for canva or google_drive. |
| secret | optional | string | Required for microsoft365 with tenant_id and client_id: the value of one of the app's client secrets (not its id), at most 1024 characters. For google_drive, optional: the JSON key of the customer's own Google Cloud service account (type service_account), whose client_email becomes the account's service_account_email; without it the account reads through screenRIG's service account. Not accepted with connection_id, nor for canva or dropbox. minLength: 1 maxLength: 8192 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media-sync-accounts'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created, with its secret masked; a Canva or Dropbox account also carries authorize_url. | MediaSyncAccountConnect (application/json); headers: ETag, Cache-Control |
| 400 | invalid_request. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — the project's plan does not include media sync; required_plan names the plan that does. | Problem (application/problem+json) |
| 409 | canva_not_configured (this server has no Canva integration; kind canva only), dropbox_not_configured (no screenRIG Dropbox app and no config.app_key; kind dropbox only), google_drive_not_configured (no screenRIG service account and no key; kind google_drive only), resource_conflict or idempotency_mismatch. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/media-syncs

**List Media Syncs In Project**

Lists the project's media syncs, newest first (200 per page; next_cursor continues), optionally of one account_id or state. Each shows its source folder, the media folder it fills, its cadence, state, next run and the summary of its last run.


Operation: listMediaSyncsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| cursor | query | optional | The next_cursor of the previous page. |
| account_id | query | optional | Lists only the syncs that read through this account. |
| state | query | optional | Lists only the syncs in this state. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media-syncs'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's syncs. | MediaSyncList (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/media-syncs

**Create Media Sync In Project**

provider canva: reads the design (design is its id or its canva.com/design/ID/... address) through a connected Canva account, and stores the sync, due now.

Each run asks Canva for the design's last change and, when it moved, exports the pages (pages, default all; format png gives one image per page and mp4 one video) and imports them into the media folder, replacing the bytes of a page already imported under the same media id so the playlists that use it update; a page the design no longer has leaves play like a removed SharePoint file.

A design Canva no longer shows stops the sync in state error (run error design_unavailable) and keeps the media.

A design or page the account cannot see is 409 media_sync_source_refused. provider dropbox: url is the folder's address in Dropbox (https://www.dropbox.com/home/...), a shared folder link the connected account has added to its Dropbox, or a path starting with /; it is read once through a connected Dropbox account and the sync is stored, due now.

Folders in a team space are read from the team's root. Each run continues from Dropbox's saved position in the folder, so only changes are read.

A folder the account cannot open is 409 media_sync_source_refused; an account whose connection Dropbox no longer honors is 409 dropbox_reconnect_required. provider google_drive: url is the folder's Google Drive address (https://drive.google.com/drive/folders/ID) or its id, shared (Viewer is enough) with the account's service_account_email.

Each run lists the folder and compares each file's checksum. Google Docs, Sheets and Slides files are skipped as unsupported_type. A folder the address cannot open is 409 media_sync_source_refused.

Through screenRIG's service account the folder must be shared or owned by a member of the project with a verified email (403 media_sync_sharer_not_member), and every run checks it again (run error sharer_not_member stops the sync in state error). provider sharepoint: reads the folder's address through the account (a SharePoint site address such as https://TENANT.sharepoint.com/sites/SITE/LIBRARY/FOLDER, or a sharing link; the app must be able to see it: Sites.Selected needs a grant on the site, a sharing link needs Files.Read.All) and stores the sync, due now. folder_id is the media folder the files land in; without it the sync creates one named after the source folder.

Each run imports the images (png, jpeg, webp, gif), videos (mp4, webm) and audio (mp3) directly in the source folder, replaces the bytes of a file that changed under the same media id so playlists that use it update, and takes a file removed at the source out of play: its media is deleted, or, while screens still hold it, moved to a Removed subfolder and deleted later.

Subfolders are ignored; other files are listed as skipped with the reason. every is the seconds between runs, 300 to 86400, default 900. A folder the provider refuses is 409 media_sync_source_refused and one it cannot reach is 500 media_sync_source_unavailable; nothing is stored. Appends media_sync.created.


Operation: createMediaSyncInProject.

- Authentication: projectBearer
- Request body: MediaSyncWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| provider | required | string | Allowed: "sharepoint", "canva", "dropbox", "google_drive" |
| account_id | required | string | An account of the provider's kind: microsoft365 for sharepoint, canva, dropbox or google_drive. |
| url | optional | string | sharepoint (required): the folder's SharePoint address or sharing link, read once to find the folder and not stored. dropbox (required): the folder's Dropbox address, a shared folder link the account has added, or a path starting with /. google_drive (required): the folder's Google Drive address or its id. maxLength: 4096 |
| design | optional | string | canva (required): the design's id, or its canva.com/design/ID/... address. maxLength: 2048 |
| pages | optional | array | canva: the pages to sync; omit for every page. A design that later has fewer pages takes the missing ones out of play. |
| format | optional | string | canva: png gives one image per page; mp4 one video of the pages. Allowed: "png", "mp4" default: "png" |
| folder_id | optional | string | The media folder the files land in. Omit it to create one named after the source folder. |
| every | optional | integer | Seconds between runs; defaults to 900. minimum: 300 maximum: 86400 |
| name | optional | string | Defaults to the source folder's name. minLength: 1 maxLength: 80 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/media-syncs'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created, due now. | MediaSync (application/json); headers: ETag, Cache-Control |
| 400 | invalid_request. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — the project's plan does not include media sync; required_plan names the plan that does. | Problem (application/problem+json) |
| 403 | media_sync_sharer_not_member — a Google Drive folder read through screenRIG's service account was not shared or owned by a member of the project with a verified email. | Problem (application/problem+json) |
| 409 | media_sync_source_refused (the provider refused the app, its permission or the folder), dropbox_reconnect_required (the Dropbox account must be connected again), dropbox_not_configured, google_drive_not_configured, resource_conflict (the same source is already synced into that media folder) or idempotency_mismatch. | Problem (application/problem+json) |
| 500 | media_sync_source_unavailable — the provider could not be reached or answered with a failure. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/fleet-links

**List Fleet Links In Project**

Lists the project's fleet links, oldest first (at most 50). Below effective Manage each url is empty and url_redacted is true, since the URL creates screens.


Operation: listFleetLinksInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/fleet-links'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's fleet links. | FleetLinkList (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/fleet-links

**Create Fleet Link In Project**

Creates a fleet link: one Player URL (https://play.<domain>/player/#fleet=<secret>) that turns every device opening it into its own screen in this project, without a pairing code. Put it in a device manager, such as a Google Admin console kiosk app.

Each new screen gets the link's tags, fields and playlist, and a label from the device (its administrator asset ID, then location), else the next default label. A Player that already holds a screen never makes another: its key stays the screen's identity across reloads and relaunches.

After a powerwash or reinstall, a device that reports the serial or DUID of a screen a fleet link of this project created recovers that screen (screen.recovered) instead of creating a new one, once the old key has been silent for ten minutes.

The secret travels in the URL fragment, which browsers never send to a server. max_screens caps the screens the link creates and expires_at ends the link at that time, as disabling it does. An organization has at most 50 fleet links across its projects (fleet_link_limit_reached). Appends fleet_link.created.

An exact retry with the same Idempotency-Key returns this same answer for twenty-four hours.


Operation: createFleetLinkInProject.

- Authentication: projectBearer
- Request body: FleetLinkWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| label | required | string | minLength: 1 maxLength: 80 |
| enabled | optional | boolean | Defaults to true. |
| tags | optional | ScreenTags | Fleet selector tags, 0 to 16 unique exact tags, each matching the media tag grammar. On ScreenPatch the array replaces the whole set and an empty array clears it; a duplicate or malformed tag is invalid_request. Changing tags bumps the screen revision (If-Match guards it) and appends screen.updated with details.tags; it never changes the manifest revision. Tags are never on the runtime manifest and never authorization. |
| fields | optional | object | Fields each screen the link creates starts with; card placements read them through {screen.field.NAME}. |
| playlist_id | optional | string |  |
| max_screens | optional | integer | minimum: 1 maximum: 10000 |
| expires_at | optional | string | When the link expires (RFC 3339); a link past it creates and recovers no screens. Absent means it never expires. A time already past is accepted and expires the link at once. format: "date-time" |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/fleet-links'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created, with its url. | FleetLink (application/json); headers: Cache-Control |
| 400 | invalid_request, including an unknown playlist_id or a malformed tag or field. | Problem (application/problem+json) |
| 409 | fleet_link_limit_reached or idempotency_mismatch. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/webhooks

**List Webhooks In Project**

Lists the project's webhook notifications in the webhook shape, oldest first (at most 50); GET /api/notifications lists every channel. The signing secret is never returned here. Requires the project capability; a reports-only credential is refused 403 forbidden, because the response includes receiver URLs.


Operation: listWebhooksInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/webhooks'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's webhooks. | WebhookList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/webhooks

**Create Webhook In Project**

Creates a webhook that receives the project's own durable events as signed HTTPS POSTs. url must be https on a public Internet host: an IP literal or every current DNS answer of the name must be public (private, loopback, link-local, CGNAT, IPv6 outside 2000::/3, documentation, and other reserved ranges are refused with webhook_url_rejected).

The port must be 443 (the default) or 8443, else webhook_url_rejected. The host is resolved and checked again before every delivery and the connection is dialed to exactly the checked address, so a DNS answer that changes later cannot redirect deliveries. event_types lists exact event types (screen.online) or prefixes ending in .* (screen.*).

An organization has at most 50 notifications across every channel (webhook_limit_reached). The server generates the signing secret and returns it only in this answer and in rotate-secret; store it. An exact retry with the same Idempotency-Key returns this same answer, secret included, for twenty-four hours.

Delivery starts with the first event after notification.created.


Operation: createWebhookInProject.

- Authentication: projectBearer
- Request body: WebhookWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| url | required | string | https URL on a public Internet host, port 443 (the default) or 8443. No userinfo or fragment. maxLength: 2048 format: "uri" |
| event_types | required | WebhookEventTypes | Exact event types such as screen.online, or prefixes ending in .* such as screen.* (every type starting with screen.). notification.test is never fanned out by a subscription; it is sent only by POST /api/webhooks/{id}/test. |
| enabled | optional | boolean | default: true |
| description | optional | string | maxLength: 200 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/webhooks'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. secret is shown only here and on rotate-secret. | WebhookWithSecret (application/json); headers: Cache-Control |
| 400 | invalid_request or webhook_url_rejected. | Problem (application/problem+json) |
| 409 | webhook_limit_reached, idempotency_mismatch, or resource_conflict when the project belongs to no organization. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/alerts

**List Alerts In Project**

Lists the project's screen alerts: one per screen and condition while it lasts (open), and those that ended within the plan's event retention (resolved).

An alert opens when its condition's event arrives (screen.offline, a display_disconnected, temperature_high or on_battery health change, screen.storage_shortfall, a stuck screen.manifest_upgrade, an error-level screen.status_changed, a stalled, blank or frozen screen.glass_changed) and resolves on the clearing event; crashing is a notice, not an alert. counts.attention counts open alerts that are not acknowledged, snoozed or muted, the number the dashboard shows.

A screen restarting for a reboot the backend sent opens no screen_offline or display_disconnected alert until the reboot is done or ten minutes after it was sent; a condition that still holds then opens its alert at that moment. Open alerts come first, newest first; pass next_cursor back as before for the next page.


Operation: listAlertsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| format | query | optional | json (default) returns the report as JSON. csv, xlsx and pdf return the same rows as a file attachment (every page of a paged report, up to 50000 rows). |
| state | query | optional |  |
| screen_id | query | optional |  |
| before | query | optional | next_cursor of the previous page. |
| limit | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/alerts'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One page of alerts. | AlertList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/alert-mutes

**List Alert Mutes In Project**

Lists the project's mutes that have not ended, soonest end first (at most 50).


Operation: listAlertMutesInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/alert-mutes'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's current mutes. | AlertMuteList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/alert-mutes

**Create Alert Mute In Project**

Mutes screens for maintenance until an instant at most 30 days ahead: the screens named by screen_ids, and every screen carrying any of tags at the time a notice is due. Their alerts still open and resolve, marked muted, and never need attention; every notification's notices about them are skipped (muted).

An organization has at most 50 current mutes across its projects (alert_mute_limit_reached). Appends alert_mute.created.


Operation: createAlertMuteInProject.

- Authentication: projectBearer
- Request body: AlertMuteWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| screen_ids | optional | array |  |
| tags | optional | ScreenTags | Fleet selector tags, 0 to 16 unique exact tags, each matching the media tag grammar. On ScreenPatch the array replaces the whole set and an empty array clears it; a duplicate or malformed tag is invalid_request. Changing tags bumps the screen revision (If-Match guards it) and appends screen.updated with details.tags; it never changes the manifest revision. Tags are never on the runtime manifest and never authorization. |
| until | required | string | At most 30 days ahead. format: "date-time" |
| note | optional | string | maxLength: 200 |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/alert-mutes'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. | AlertMute (application/json) |
| 409 | alert_mute_limit_reached or idempotency_mismatch. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/notifications

**List Notifications In Project**

Lists the organization's notifications that cover the project, of every channel, oldest first (at most 50). status, failing_since, last_success_at and last_failure_at are this project's. A webhook's signing secret is never returned here.

Requires the project capability; a reports-only credential is refused 403 forbidden, because the response includes receiver URLs and recipients.


Operation: listNotificationsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/notifications'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | The project's notifications. | NotificationList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/notifications

**Create Notification In Project**

Creates a notification. channel email sends to recipients (owner, or user_ids of the project's members); channel webhook POSTs, signed, to url, which follows the webhook URL rules (https on a public Internet host, port 443 or 8443, else webhook_url_rejected). conditions lists the screen state changes it fires on; an email notification needs at least one, a webhook needs conditions or event_types. screen_tags limits conditions to screens carrying any of those tags; empty means every screen, or audience says which screens in the rules of a campaign audience. min_severity and severity filter and rate the notices, and active_hours holds or drops them outside its windows.

A condition notifies only after it has lasted hold_minutes (default 10), and a resolved notice follows when it clears after a notice went out. The notification covers this project alone; an organization has at most 50 notifications, not counting the defaults the system makes (notification_limit_reached).

A webhook's signing secret is generated by the server and returned only in this answer and in rotate-secret; store it. An exact retry with the same Idempotency-Key returns this same answer, secret included, for twenty-four hours. Notices start with the first event after notification.created.


Operation: createNotificationInProject.

- Authentication: projectBearer
- Request body: NotificationWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| channel | required | string | Allowed: "email", "webhook", "slack", "teams", "pagerduty", "telegram" |
| description | optional | string | A name for people. maxLength: 200 |
| conditions | optional | NotificationConditions |  |
| screen_tags | optional | ScreenTags | Fleet selector tags, 0 to 16 unique exact tags, each matching the media tag grammar. On ScreenPatch the array replaces the whole set and an empty array clears it; a duplicate or malformed tag is invalid_request. Changing tags bumps the screen revision (If-Match guards it) and appends screen.updated with details.tags; it never changes the manifest revision. Tags are never on the runtime manifest and never authorization. |
| audience | optional | CampaignAudience | Which screens a campaign targets: exactly one of all (every rule matches), any (at least one matches), tag (the screen carries the tag), screen_id (that screen) or folder (the screens in that screen folder and every folder inside it, GET /api/screen-folders; the folder must exist). Rules nest at most 3 deep and an audience has at most 500 rules. {"all": []} is every screen of the project; elsewhere all and any list at least one rule. |
| min_severity | optional | NotificationSeverity | The severity of a notice, least severe first. An opened notice has its condition's severity: error for screen_offline and warning for every other condition, unless the notification's severity says otherwise. A resolved notice and a test are info. Allowed: "info", "warning", "error" |
| severity | optional | NotificationSeverityOverrides | Condition to severity for this notification, such as {"not_playing": "error"}. It counts for min_severity and is the severity Teams and PagerDuty show for the condition. A condition not listed keeps its default. On a patch an empty object, like null, clears every override. |
| active_hours | optional | NotificationActiveHours | When opened notices go out: the windows (screenrig.schedule/v1), read in timezone. A notice that falls due while they are closed waits for the next opening (outside hold, the default) and goes out then if its condition still holds, so a condition that clears first sends nothing, or is skipped (outside drop; last_error outside_hours). A resolved notice for an opened notice that was sent always goes out, inside the hours or not, and a test ignores them. crashing is held or dropped like any other condition. The hours judge a notice when it first goes out; a failed one is retried on the usual schedule whatever the hours. Adding, changing or removing the hours judges the notices held for the old ones again at once. |
| reach_drop | optional | NotificationReachDrop | The smallest drop in a campaign's reach a reach_dropped notice is sent for: screens is the fewest screens lost (default 1) and percent, when set, the smallest share of the screens the campaign reached when it was published; a drop must meet both. A smaller drop queues no notice, and a queued notice whose drop shrank below it by the time it is due is skipped (condition_cleared). Only with the reach_dropped condition; on a notification without it, invalid_request. |
| hold_minutes | optional | integer | How long a condition must last before it notifies. crashing is never held. default: 10 minimum: 0 maximum: 1440 |
| recipients | optional | NotificationRecipients | Email only. owner (whoever owns the project when a notice sends) or the user_id of a person who is a member of the project; a user_id that is not a member is invalid_request. Each is emailed at their verified address. One who has since left the project or has no verified address is skipped. |
| url | optional | string | Webhook only: https URL on a public Internet host, port 443 (the default) or 8443. No userinfo or fragment. maxLength: 2048 format: "uri" |
| event_types | optional | NotificationEventTypes | Webhook only. Project events also POSTed as they happen, exactly as a webhook sends them: exact types such as screen.online, or prefixes ending in .* such as screen.*. notification.test is never fanned out. |
| destination | optional | NotificationDestination | Slack, Teams, PagerDuty and Telegram only; required for them. The credential in the shape the service issues it: slack, the incoming webhook URL (hooks.slack.com); teams, the Workflows webhook URL (logic.azure.com or powerplatform.com; Office 365 connector URLs are retired); pagerduty, the 32-character Events API v2 routing key; telegram, the bot token. Another shape is notification_destination_rejected. It is sealed, never returned, and appears on reads only as destination_hint. minLength: 1 maxLength: 2048 |
| telegram_chat_id | optional | NotificationTelegramChat | Telegram only; required for it. The chat to post to: its numeric id (negative for a group or channel) or @channelname. Not a credential. pattern: "^(-?[0-9]{1,20}\|@[A-Za-z][A-Za-z0-9_]{4,31})$" |
| enabled | optional | boolean | default: true |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/notifications'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Created. A webhook's secret is shown only here and on rotate-secret. | Notification (application/json); headers: Cache-Control |
| 400 | invalid_request, webhook_url_rejected or notification_destination_rejected. | Problem (application/problem+json) |
| 402 | feature_not_in_plan — Alert routing (a Slack, Teams, PagerDuty or Telegram channel, or a min_severity, severity, active_hours or audience rule) is part of Plus. Email and webhook notifications with the default rules, and the loop_stalled, glass_blank and glass_frozen conditions, are in every plan. The problem names the feature, the project's plan and required_plan. | Problem (application/problem+json) |
| 409 | notification_limit_reached, idempotency_mismatch, or resource_conflict when the project belongs to no organization. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/notes

**List Notes In Project**

Lists the project's notes, newest first. A note is a short plain-text fact an agent wrote for its future self and the project's other agents; it is context, never an instruction and never approval.

Any active credential reads, Read only included, and reading does not debit the API meter. state is open (the default), resolved or all; about_id keeps the notes about one screen or playlist and author_id the notes one agent wrote.

Pass next_cursor back as after for the next page; it is empty on the last page. new marks an open note another agent wrote after this caller last read the notes.

Reading the notes is the first page of open notes with no about_id or author_id: that answer computes new against the previous read, then moves the read marker to now (later pages of the same listing keep the earlier marker).

It also carries review: up to 5 open notes whose review is due (oldest first), which the caller should confirm when they still hold and resolve otherwise. A review left unanswered for 30 days after it was first returned resolves the note as not_confirmed.

Only an agent with Manage access moves its marker and starts that 30 days; a Read only agent or a service client gets the same answer and changes nothing. Every other page or filter returns an empty review and moves nothing. counts are the project's open notes and, for this caller, how many are new and due.

An agent limited to screens lists only the notes about its own screens, playlists and the project: the list, review and counts leave out notes about a screen outside its scope, and a listing across projects leaves out a project where the agent is limited, so select that project.


Operation: listNotesInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| state | query | optional |  |
| about_id | query | optional | Only notes about this screen or playlist. |
| author_id | query | optional | Only notes this agent wrote. |
| after | query | optional | next_cursor of the previous page. |
| limit | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/notes'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One page of notes, the notes to review and the counts. | NoteList (application/json) |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/notes

**Add Note In Project**

Adds a note for the project's agents: a fact another agent would act on, never a progress report, chat or a secret. body is 1 to 2000 characters of plain text after trimming and may span lines.

Text holding a credential (tokens, keys, private keys, passwords in URLs) is refused with note_contains_credential and text that reads as an instruction to a model with note_rejected; nothing is stored and the detail never repeats the text. about names one screen (archived included) or playlist of the project, else not_found. ttl_seconds (3600 to 31536000) makes a temporary note that resolves as expired; a note without one comes up for review 90 days after it was added or last confirmed. replaces names an open note this one supersedes: it is resolved as replaced in the same transaction (note_not_open when it is already resolved).

A body whose normalized text (trimmed, whitespace collapsed, lower case) and about match an open note confirms that note and returns it with 200 instead of adding one; no note.added event is appended. An organization holds at most 200 open notes across its projects; the 201st is note_limit_reached, whose detail names up to 5 notes to resolve first.

Appends note.added (and note.resolved for a replaced note) without the text. An exact retry with the same Idempotency-Key returns the first answer for twenty-four hours.

For an agent limited to screens (docs/screen-scope.md) an about that is a screen outside its scope or unknown to the project, or a replaces that names a note about a screen outside its scope, is outside_screen_scope (403) and nothing is stored.


Operation: addNoteInProject.

- Authentication: projectBearer
- Request body: NoteWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| body | required | string | Plain text, 1 to 2000 characters after trimming; line breaks are kept. minLength: 1 maxLength: 2000 |
| about | optional | inline |  |
| ttl_seconds | optional | integer | Expire the note this long after it is added. Without it the note comes up for review after 90 days. minimum: 3600 maximum: 31536000 |
| replaces | optional | string | An open note this one supersedes; it is resolved as replaced. pattern: "^(?:(?:stage\|qa\|development)_)?nte_[A-Za-z0-9_-]{16,24}$" |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/notes'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Added. | Note (application/json) |
| 200 | The text matched an open note, which is confirmed and returned. | Note (application/json) |
| 400 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 403 | outside_screen_scope - the caller is limited to screens and the screen, screen folder, selector or audience the request names or reaches is outside them (docs/screen-scope.md). An id outside the scope and an id that does not exist get the same answer; errors[0].screen_scope carries the caller's scope, and the write is refused whole, never narrowed. | Problem (application/problem+json) |
| 404 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | note_not_open, note_limit_reached or idempotency_mismatch. | Problem (application/problem+json) |
| 422 | note_contains_credential or note_rejected. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/support/status

**Support Status In Project**

Availability and hours for any active project. chat_allowed is true for every active project, including Standard. human_available is true when a person can join the chat on request (Education, Premium and Enterprise); Standard and Plus get documentation and AI support. staffed says whether staff are in their hours now. Luna answers right away on every plan.


Operation: supportStatusInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/support/status'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Support result. | SupportStatus (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/support/conversations

**List Support Conversations In Project**

Project conversations, newest first, at most 100. Staff unread counts are per authenticated person or agent.


Operation: listSupportConversationsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| before | query | optional | Conversation ID from the preceding page's next cursor. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/support/conversations'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Support result. | SupportConversationList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/support/conversations

**Create Support Conversation In Project**

Available on every plan; Luna answers first, right away. Where a person can join (human_available on the support status) a request for one pauses Luna for staff; on Standard and Plus it is ignored and Luna answers. Screened before storage or Slack.

An opening message under 8 characters or 3 words returns submission_too_short; one that is not a question or problem returns submission_unclear; spam, link-heavy text, prompt injection and unsafe text return submission_rejected. Rejected text is not retained or sent to Luna, and the problem detail never repeats it.

Credential-shaped text other than ScreenRig credentials (bearer tokens, cloud and API keys, private keys) is replaced with [redacted] before storage; ScreenRig credential material returns invalid_request. A new conversation starts one Slack thread; follow-ups stay in it. Requests with the same key replay for 24 hours, a refusal included.


Operation: createSupportConversationInProject.

- Authentication: projectBearer
- Request body: SupportWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| Idempotency-Key | header | required |  |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| body | required | string | minLength: 1 maxLength: 4000 |
| human_requested | optional | boolean | Ask for a person, which pauses Luna and waits for staff. It counts only where a person can join (human_available: Education, Premium and Enterprise); on Standard and Plus it is ignored and Luna answers. default: false |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Idempotency-Key: REQUEST_ID' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/support/conversations'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 201 | Support result. | SupportSubmission (application/json) |
| 422 | The message was refused by screening: submission_too_short or submission_unclear (the opening message only) or submission_rejected. The detail never repeats the text. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/reviews

**List Reviews In Project**

The project's reviews (docs/reviews.md): playlist and screen changes proposed under Propose access, and for an ad network the creatives buyers submitted (kind ad_creative).

Newest first, at most limit (default 50, at most 100) per page; pass next_cursor back as cursor with the same filters. state pending (the default), decided (every other state) or all; kind narrows to one kind.

A dashboard session reads every kind; a bearer reads the kinds its capabilities cover (playlists for playlist_change, screens for screen_change, advertising for ad_creative), and a kind it cannot read is an empty page. pending_count counts the pending reviews of every kind the caller sees.


Operation: listReviewsInProject.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| state | query | optional |  |
| kind | query | optional |  |
| limit | query | optional |  |
| cursor | query | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/reviews'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | One page of reviews. | ReviewList (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/card-data/{key}

**Project Scoped_get Card Data**

Returns the document and its revision. ETag is the revision, for If-Match on the next write.


Operation: projectScoped_getCardData.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| key | path | required | Card data key. May not start with feed:. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/card-data/KEY'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Card data document | CardDataEntry (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### PUT /api/projects/{project_id}/card-data/{key}

**Project Scoped_put Card Data**

Creates or replaces a card data document. When card placements in the project's playlists bind the key, the document must hold only each bound card's dynamic properties and validate against them; otherwise the write is refused with card_data_invalid naming the card and the property path, and nothing changes. A key no placement binds accepts any JSON object.

Every string that begins with media: is a media reference, media:<id>, for a card to draw (a photo); on any key it must name a ready, approved image in this project, at most 256 distinct media per document, or the write is refused with card_media_invalid naming the property path and media_id.

Screens whose content reads the key may then fetch those media from GET /media/{media_id}. Screens showing a placement that reads the key receive screen.card_data_changed and refetch it in place; the project log records kv.updated with a card_data_entry resource, which never reaches a Player.

Limits, quota, metering, revisions and idempotency are the application K/V store's.

Agent and webhook writes are also rate limited, per organization by plan (Standard and Plus 60 a minute, Education and Premium 600, Enterprise 3,000) and to 60 a minute per key on every plan; a write over a limit is refused with rate_limited and Retry-After, and is not metered.


Operation: projectScoped_putCardData.

- Authentication: projectBearer
- Request body: CardDataWrite

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| key | path | required | Card data key. May not start with feed:. |
| If-Match | header | optional | Optional positive resource revision. Omit to create or overwrite a key; a supplied stale revision is revision_conflict. |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| document | required | object | The card data document: a JSON object of at most 1 MiB. When a card placement binds the key it holds only that card's dynamic properties. A string that begins with media: is a media reference, media:<id>, naming a ready, approved image in this project. |
| expires_at | optional | string | Absolute expiry; omitted data never expires. format: "date-time" |
| ttl_seconds | optional | integer | TTL from the first committed write. Mutually exclusive with expires_at. A retry preserves the original expiry. Omitting both clears expiry. Size it to the writer's schedule: when a card's document expires with no write to renew it while a screen shows the card, the card goes blank and the card_data_stale notification condition tells the project, so a job that stopped is noticed. minimum: 1 maximum: 31536000 |

#### Request template

~~~sh
curl --request PUT --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/card-data/KEY'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Card data document | CardDataEntry (application/json) |
| 429 | Stable rate_limited problem. Retry only after the authoritative Retry-After interval. Fixed-window denials also include the applicable RateLimit-Policy and RateLimit members; concurrency and byte-budget denials omit those optional disclosures. | Problem (application/problem+json); headers: Retry-After, RateLimit-Policy, RateLimit |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### DELETE /api/projects/{project_id}/card-data/{key}

**Project Scoped_delete Card Data**

Deletes a card data document. A card bound to the key is hidden on glass until a document is written again.


Operation: projectScoped_deleteCardData.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| key | path | required | Card data key. May not start with feed:. |
| If-Match | header | optional | Optional positive resource revision. Omit to write the current resource; a supplied stale revision is revision_conflict. |
| Idempotency-Key | header | optional | Omit for a fresh operation. Supply and reuse a key to deduplicate retries; the CLI manages keys automatically. |

#### Request template

~~~sh
curl --request DELETE --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/card-data/KEY'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 204 | Deleted | no body |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/feeds/{family}

**Project Scoped_get Feed**

Reads a shared third-party feed document: the same document a built-in card placement with the same params reads (docs/feeds.md).

Query parameters normalize into the card's feed key, named by the ScreenRig-Feed-Key header: weather, airquality and alerts take lat and lon (rounded to a 0.25 degree cell); earthquakes lat, lon, radius_km (10 to 20000, default 500), min_magnitude (0 to 9, default 2.5) and hours (1 to 168, default 24); cyclones basins (comma separated at, ep, cp, wp; default all) and optionally lat, lon and radius_km (the storms near that place); tides station (a NOAA tide prediction station id such as 9414290) or lat and lon (the nearest station within 100 km, else invalid_request); launches no parameters (the next launches worldwide); rates base (an ECB reference currency, default EUR); crypto symbols (CoinGecko ids or tickers, comma separated, at most 20) and currency (default usd); headlines url (an http or https RSS or Atom URL); drugalerts (screenrig.drugalerts/v1) the recent recall reports of a country: country (us, ca or gb, required) and category (drug, the default, or food); respiratory (screenrig.respiratory/v1) the weekly flu, COVID-19 and RSV activity of a place: country (us or gb, required) and region (a US state, territory or DC by name or postal code, or England or a UKHSA region; the nation when empty).

The request never calls the provider. 200 is the document (schema, fetchedAt, attribution and the family's fields, as the cards contract defines them); its ETag is the sha256 of the bytes and If-None-Match answers 304; Cache-Control max-age runs to the next scheduled fetch. 202 means no document yet: the read registered demand, and the feed worker fetches the key within about a minute (Retry-After).

An API read keeps a key fetched for 24 hours. An organization registers at most 50 new keys a day across its projects (rate_limited past it). A family with no source on this deployment answers not_found.


Operation: projectScoped_getFeed.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| family | path | required | The feed family. |
| lat | query | optional |  |
| lon | query | optional |  |
| radius_km | query | optional |  |
| min_magnitude | query | optional | earthquakes: the least magnitude. |
| hours | query | optional | earthquakes: the window in hours. |
| basins | query | optional | cyclones: comma separated at, ep, cp, wp. |
| station | query | optional | tides: a NOAA tide prediction station id, such as 9414290. Without it, lat and lon pick the nearest station within 100 km. |
| base | query | optional |  |
| symbols | query | optional |  |
| currency | query | optional |  |
| url | query | optional |  |
| country | query | optional | drugalerts: us, ca or gb. respiratory: us or gb. Both require it. |
| category | query | optional | drugalerts: drug (default) or food. |
| region | query | optional | respiratory: a US state, territory or DC by name or postal code, or England or a UKHSA region; the nation when empty. |
| If-None-Match | header | optional |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/feeds/FAMILY'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Feed document | FeedDocument (application/json); headers: ETag, ScreenRig-Feed-Key |
| 202 | No document yet; the feed worker fetches the key on its next pass. | FeedPending (application/json); headers: Retry-After, ScreenRig-Feed-Key |
| 304 | The document matches If-None-Match. | no body |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/advertising/networks/{seller_project_id}/inventory

**Project Scoped_list Advertising Joined Inventory**

Opted-in inventory and reusable slots inside the caller's membership. Never exposes the seller's screenshots, playlists, or project-wide data.


Operation: projectScoped_listAdvertisingJoinedInventory.

- Authentication: projectBearer OR dashboardSession
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |
| seller_project_id | path | required |  |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --cookie cookies.txt --cookie-jar cookies.txt 'https://screenrig.ai/api/projects/PROJECT_ID/advertising/networks/SELLER_PROJECT_ID/inventory'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Permitted inventory and slots. | AdvertisingJoinedInventory (application/json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### GET /api/projects/{project_id}/agents/self

**Project Scoped_get Current Agent**

Returns the agent installation linked to the calling project bearer plus connection_ready. A project is ready when at least one member has a passkey or password. Identity status is diagnostic and does not debit the API meter.


Operation: projectScoped_getCurrentAgent.

- Authentication: projectBearer
- Request body: none

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request template

~~~sh
curl --request GET --header 'Authorization: Bearer [SCREENRIG_TOKEN]' 'https://api.screenrig.ai/api/projects/PROJECT_ID/agents/self'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 200 | Current agent and project connection readiness. | AgentSelfStatus (application/json); headers: Cache-Control |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |

### POST /api/projects/{project_id}/agents/self/disconnect

**Project Scoped_disconnect Current Agent**

Revokes only the calling project membership and its project token, preserving the global identity, other memberships, project and content. Repeating with the same cryptographically valid revoked bearer is a no-op success. Disconnecting the last active agent returns agent_lockout_risk unless allow_last_agent is explicit. Does not debit the API meter.

An agent access JWT, Read only included, disconnects the agent from its token's project; the membership ends, the project's consent leaves the agent's grants and that identity/project pair is cut off for older access tokens. Other projects in the token keep working. A retry with the same JWT after the disconnect is a no-op success.


Operation: projectScoped_disconnectCurrentAgent.

- Authentication: revocableAgentBearer
- Request body: AgentDisconnectRequest

#### Parameters

| Name | Send in | Required? | Description |
| --- | --- | --- | --- |
| project_id | path | required |  |
| ScreenRig-Project | header | optional | Deprecated for one release. Must be consistent with the JWT projects grants; adds Deprecation and Warning headers. No token exchange. |

#### Request body fields

| Field | Required? | Type | Description |
| --- | --- | --- | --- |
| allow_last_agent | optional | boolean | Explicitly accept loss of the last active agent bearer. A registered dashboard passkey remains independent administrative authority. default: false |

#### Request template

~~~sh
curl --request POST --header 'Authorization: Bearer [SCREENRIG_TOKEN]' --header 'Content-Type: application/json' --data '@request.json' 'https://api.screenrig.ai/api/projects/PROJECT_ID/agents/self/disconnect'
~~~

#### Responses

| Status | Meaning | Body and headers |
| --- | --- | --- |
| 204 | Agent disconnected. | no body; headers: Cache-Control |
| 401 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| 409 | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |
| default | Stable RFC 9457 problem response. A 421 server_draining carries Retry-After of 1 second, a 500 not_ready or dependency_unavailable carries 5, and a 500 manifest_degraded carries 60, unless the handler set a more specific value. | Problem (application/problem+json) |


## Sitemap

See the [public documentation sitemap](https://screenrig.ai/sitemap.md).
