openapi: 3.1.0
info:
  title: ScreenRig API
  version: 0.2.0
  description: Agent-operated digital signage control and runtime API.
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
servers:
  - url: https://api.screenrig.ai
    description: Control-plane origin
security: []
paths:
  /.health:
    get:
      operationId: getHealth
      responses:
        "200": { description: Alive, content: { application/json: { schema: { $ref: "#/components/schemas/HealthResponse" } } } }
  /.ready:
    get:
      operationId: getReadiness
      responses:
        "200": { description: Ready, content: { application/json: { schema: { $ref: "#/components/schemas/ReadyResponse" } } } }
        "503": { $ref: "#/components/responses/Problem" }
  /.version:
    get:
      operationId: getVersion
      responses:
        "200": { description: Version, content: { application/json: { schema: { $ref: "#/components/schemas/VersionResponse" } } } }
  /api/v1/capabilities:
    get:
      operationId: getCapabilities
      responses:
        "200": { description: Capability and archive limits, content: { application/json: { schema: { $ref: "#/components/schemas/Capabilities" } } } }
  /api/v1/accounts:
    post:
      operationId: createAccount
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/AccountCreate" } } } }
      responses:
        "201": { description: Account and one-time credential, content: { application/json: { schema: { $ref: "#/components/schemas/AccountCreated" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/account:
    get:
      operationId: getAccount
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: Account, content: { application/json: { schema: { $ref: "#/components/schemas/Account" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/account/credential-issuances/{id}/ack:
    post:
      operationId: acknowledgeCredentialIssuance
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/ResourceID" }]
      responses:
        "204": { description: Acknowledged }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/applications:
    get:
      operationId: listApplications
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: Applications, content: { application/json: { schema: { $ref: "#/components/schemas/ApplicationList" } } } }
        default: { $ref: "#/components/responses/Problem" }
    post:
      operationId: uploadApplication
      description: 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.
      security: [{ accountBearer: [] }]
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
        - { $ref: "#/components/parameters/ArchiveSHA256" }
        - { $ref: "#/components/parameters/ExpandedBytes" }
        - { $ref: "#/components/parameters/FileCount" }
        - { $ref: "#/components/parameters/SDKVersion" }
        - { $ref: "#/components/parameters/ApplicationName" }
      requestBody: { required: true, content: { application/gzip: { schema: { type: string, contentMediaType: application/gzip, contentEncoding: binary, maxLength: 104857600 } } } }
      responses:
        "202": { description: Upload accepted, content: { application/json: { schema: { $ref: "#/components/schemas/OperationAccepted" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/applications/{id}:
    get:
      operationId: getApplication
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/ResourceID" }]
      responses:
        "200": { description: Application, content: { application/json: { schema: { $ref: "#/components/schemas/Application" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/operations/{id}:
    get:
      operationId: getOperation
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/ResourceID" }]
      responses:
        "200": { description: Operation, content: { application/json: { schema: { $ref: "#/components/schemas/Operation" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/operations/{id}/cancel:
    post:
      operationId: cancelOperation
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/ResourceID" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "200": { description: Cancelled durable operation. A current worker fence can no longer finalize it., content: { application/json: { schema: { $ref: "#/components/schemas/Operation" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/playlists:
    get:
      operationId: listPlaylists
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: Playlists, content: { application/json: { schema: { $ref: "#/components/schemas/PlaylistList" } } } }
    post:
      operationId: createPlaylist
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PlaylistWrite" } } } }
      responses:
        "201": { description: Playlist, content: { application/json: { schema: { $ref: "#/components/schemas/Playlist" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/playlists/{id}:
    parameters: [{ $ref: "#/components/parameters/ResourceID" }]
    get:
      operationId: getPlaylist
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: Playlist, content: { application/json: { schema: { $ref: "#/components/schemas/Playlist" } } } }
        default: { $ref: "#/components/responses/Problem" }
    put:
      operationId: updatePlaylist
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PlaylistWrite" } } } }
      responses:
        "200": { description: Playlist, content: { application/json: { schema: { $ref: "#/components/schemas/Playlist" } } } }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deletePlaylist
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "204": { description: Deleted }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/screens:
    get:
      operationId: listScreens
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: Screens, content: { application/json: { schema: { $ref: "#/components/schemas/ScreenList" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/screens/pair:
    post:
      operationId: pairScreen
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PairScreen" } } } }
      responses:
        "201": { description: Paired screen claim, content: { application/json: { schema: { $ref: "#/components/schemas/PairingClaim" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/screens/{id}:
    parameters: [{ $ref: "#/components/parameters/ResourceID" }]
    get:
      operationId: getScreen
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: Screen, content: { application/json: { schema: { $ref: "#/components/schemas/Screen" } } } }
        default: { $ref: "#/components/responses/Problem" }
    patch:
      operationId: updateScreen
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ScreenPatch" } } } }
      responses:
        "200": { description: Screen, content: { application/json: { schema: { $ref: "#/components/schemas/Screen" } } } }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deleteScreen
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "204": { description: Deleted }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/screens/{id}/public-id/rotate:
    post:
      operationId: rotateScreenPublicId
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/ResourceID" }, { $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "200": { description: Public URL credential rotated; paired device credential preserved and all prior runtime/content grants revoked., content: { application/json: { schema: { $ref: "#/components/schemas/Screen" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/screens/{id}/credential/revoke:
    post:
      operationId: revokeScreenCredential
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/ResourceID" }, { $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "200": { description: Paired device credential revoked; public URL preserved and all prior runtime/content grants revoked., content: { application/json: { schema: { $ref: "#/components/schemas/Screen" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/events:
    get:
      operationId: listEvents
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/AfterCursor" }]
      responses:
        "200": { description: Durable events, content: { application/json: { schema: { $ref: "#/components/schemas/EventList" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/events/stream:
    get:
      operationId: streamEvents
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/AfterCursor" }, { $ref: "#/components/parameters/LastEventID" }]
      responses:
        "200": { description: Replayable SSE stream. The query cursor takes precedence over Last-Event-ID. Invalid, future, or pre-retention cursors receive stream.resync_required at the current head and close., content: { text/event-stream: { schema: { type: string } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/applications/{application_id}/kv:
    get:
      operationId: listKV
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/ApplicationID" }]
      responses:
        "200": { description: KV entries, content: { application/json: { schema: { $ref: "#/components/schemas/KVList" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/applications/{application_id}/kv/{key}:
    parameters: [{ $ref: "#/components/parameters/ApplicationID" }, { name: key, in: path, required: true, schema: { type: string } }]
    get:
      operationId: getKV
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: KV entry, content: { application/json: { schema: { $ref: "#/components/schemas/KVEntry" } } } }
        default: { $ref: "#/components/responses/Problem" }
    put:
      operationId: putKV
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/OptionalIfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/KVWrite" } } } }
      responses:
        "200": { description: KV entry, content: { application/json: { schema: { $ref: "#/components/schemas/KVEntry" } } } }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deleteKV
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "204": { description: Deleted }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/media:
    get:
      operationId: listMedia
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: Media, content: { application/json: { schema: { $ref: "#/components/schemas/MediaList" } } } }
  /api/v1/media/uploads:
    post:
      operationId: createMediaUpload
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/MediaUploadDeclaration" } } } }
      responses:
        "201": { description: Short-lived signed authorization for exactly one private object PUT. Send the returned method and every returned header verbatim., headers: { Cache-Control: { schema: { type: string, enum: ["private, no-store"] } } }, content: { application/json: { schema: { $ref: "#/components/schemas/MediaUploadSession" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/media/uploads/{id}/commit:
    post:
      operationId: commitMediaUpload
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/ResourceID" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/MediaCommit" } } } }
      responses:
        "202": { description: Exact declaration persisted for asynchronous verification and immutable private publication., content: { application/json: { schema: { $ref: "#/components/schemas/Operation" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /api/v1/media/{id}:
    parameters: [{ $ref: "#/components/parameters/ResourceID" }]
    get:
      operationId: getMedia
      security: [{ accountBearer: [] }]
      responses:
        "200": { description: Ready private media metadata. Bytes remain available only through a runtime manifest grant., headers: { ETag: { schema: { type: string } } }, content: { application/json: { schema: { $ref: "#/components/schemas/Media" } } } }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deleteMedia
      security: [{ accountBearer: [] }]
      parameters: [{ $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "204": { description: Tombstoned. Referenced desired or active grants cannot be deleted. }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/pairing-sessions:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    post:
      operationId: startPairingSession
      responses:
        "201": { description: Six-letter pairing code and HttpOnly pairing cookie, content: { application/json: { schema: { $ref: "#/components/schemas/PairingSession" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/pairing-events:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    get:
      operationId: streamPairingEvents
      security: [{ pairingCookie: [] }]
      responses:
        "200": { description: Pairing SSE; the pairing.claimed event data is PairingClaimedEvent., content: { text/event-stream: { schema: { type: string } } } }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/pairing-sessions/complete:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    post:
      operationId: completePairingSession
      security: [{ pairingCookie: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PairingComplete" } } } }
      responses:
        "200": { description: Paired-device cookie issued, content: { application/json: { schema: { $ref: "#/components/schemas/PairingCompletion" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/sessions:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    post:
      operationId: createAnonymousRuntimeSession
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/RuntimeSessionRequest" } } } }
      responses:
        "201": { description: Read-only runtime session cookie, content: { application/json: { schema: { $ref: "#/components/schemas/RuntimeSession" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/device-sessions:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    post:
      operationId: createDeviceRuntimeSession
      security: [{ pairedDeviceCookie: [] }]
      responses:
        "201": { description: Paired runtime session, content: { application/json: { schema: { $ref: "#/components/schemas/RuntimeSession" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/manifest:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    get:
      operationId: getRuntimeManifest
      security: [{ runtimeCookie: [] }]
      responses:
        "200": { description: Resolved player manifest, content: { application/json: { schema: { $ref: "#/components/schemas/RuntimeManifest" } } } }
        "304": { description: Manifest unchanged }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/events:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    get:
      operationId: streamRuntimeEvents
      security: [{ runtimeCookie: [] }]
      parameters:
        - { $ref: "#/components/parameters/AfterCursor" }
        - { $ref: "#/components/parameters/LastEventID" }
      responses:
        "200": { description: Screen-scoped durable SSE. The query cursor takes precedence over Last-Event-ID; each event carries an id cursor suitable for reconnect. Audience-only gaps emit payload-free stream.cursor frames. Invalid, future, or pre-retention cursors receive stream.resync_required at the current head and close., headers: { X-Accel-Buffering: { schema: { type: string, enum: [no] } } }, content: { text/event-stream: { schema: { type: string } } } }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/reports:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    post:
      operationId: createRuntimeReport
      security: [{ pairedRuntimeCookie: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/RuntimeReport" } } } }
      responses:
        "202": { description: Accepted. manifest.activated is committed before this response and is idempotent for the current active revision. }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/manifests/{manifest_revision}/releases/{release_id}/launch:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    post:
      operationId: launchApplicationRelease
      security: [{ runtimeCookie: [] }]
      parameters:
        - { name: manifest_revision, in: path, required: true, schema: { type: string } }
        - { name: release_id, in: path, required: true, schema: { type: string } }
      responses:
        "201": { description: Single-use 30-second exact-release-host launch ticket, headers: { Referrer-Policy: { schema: { type: string, enum: [no-referrer] } } }, content: { application/json: { schema: { $ref: "#/components/schemas/ReleaseLaunch" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /content/v1/manifests/{manifest_revision}/media/{media_id}:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    get:
      operationId: getProtectedMedia
      security: [{ runtimeCookie: [] }]
      parameters:
        - { name: manifest_revision, in: path, required: true, schema: { type: string } }
        - { name: media_id, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/Range" }
        - { $ref: "#/components/parameters/SecFetchSite" }
        - { $ref: "#/components/parameters/SecFetchMode" }
        - { $ref: "#/components/parameters/SecFetchDest" }
      responses:
        "200": { description: Session-authorized complete private media. Browser Fetch Metadata must describe trusted-player same-origin media/image/video use; authenticated native clients may omit it., headers: { Accept-Ranges: { schema: { type: string, enum: [bytes] } }, Content-Length: { schema: { type: integer } }, ETag: { schema: { type: string } }, Content-Disposition: { schema: { type: string } } } }
        "206": { description: One standards-correct byte range., headers: { Accept-Ranges: { schema: { type: string, enum: [bytes] } }, Content-Range: { schema: { type: string } }, Content-Length: { schema: { type: integer } }, ETag: { schema: { type: string } } } }
        "416": { description: Unsatisfiable, multiple, or malformed range., headers: { Content-Range: { schema: { type: string } }, Accept-Ranges: { schema: { type: string, enum: [bytes] } } }, content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } } }
        "429": { description: Per-client, screen, or account protected-media egress limit exceeded., headers: { Retry-After: { schema: { type: integer, minimum: 1 } } }, content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /{asset_path}:
    servers:
      - url: https://r-{release_host_label}.apps.screenrig.ai
        variables:
          release_host_label: { default: 0123456789abcdef0123456789abcdef01234567, description: Domain-separated 160-bit lowercase hexadecimal digest label derived deterministically from the immutable prefixed release ID. }
    get:
      operationId: consumeLaunchTicketOrGetReleaseAsset
      description: >-
        Exact-release-host operation. With a ticket query, atomically consumes the single-use 30-second ticket,
        binds Host/release/screen/content-generation/manifest/grant, sets only the exact-host release-grant cookie,
        and redirects to the same path without the ticket. Without a ticket, validates that cookie on every GET/HEAD.
        The asset_path parameter is greedy (x-screenrig-greedy-path) and is resolved only beneath the ready release root.
        Runtime/account/device cookies and account bearer tokens never authorize this operation.
      x-screenrig-greedy-path: true
      security:
        - releaseLaunchTicket: []
        - releaseGrantCookie: []
      parameters:
        - { name: asset_path, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/SecFetchSite" }
        - { $ref: "#/components/parameters/SecFetchMode" }
        - { $ref: "#/components/parameters/SecFetchDest" }
      responses:
        "200": { description: Exact-host grant-authorized immutable asset., headers: { Referrer-Policy: { schema: { type: string, enum: [no-referrer] } }, X-Content-Type-Options: { schema: { type: string, enum: [nosniff] } }, ETag: { schema: { type: string } } } }
        "302": { description: Ticket consumed once; release-grant cookie issued; ticket removed from Location., headers: { Location: { schema: { type: string } }, Set-Cookie: { schema: { type: string } }, Cache-Control: { schema: { type: string, enum: [no-store] } }, Referrer-Policy: { schema: { type: string, enum: [no-referrer] } } } }
        "403": { description: Wrong Host/release/screen/generation/grant, stale/replayed ticket, invalid cookie, contradictory Fetch Metadata, or disallowed method., content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } } }
        "404": { description: Asset absent without exposing release filesystem shape., content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } } }
  /runtime/v1/apps/{application_id}/kv:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    get:
      operationId: listRuntimeKV
      description: >-
        Lists metadata for the application placement named by the current manifest. Requires the HttpOnly runtime
        session and the parent-only ScreenRig-Placement-Capability handle issued on the application placement.
        Handles expire after five minutes and are refreshed by fetching the current runtime manifest; uploaded frames
        never receive the handle and must use the validated parent SDK bridge.
      security: [{ runtimeCookie: [], placementCapability: [] }]
      parameters: [{ $ref: "#/components/parameters/ApplicationID" }]
      responses:
        "200": { description: Manifest-authorized application KV, content: { application/json: { schema: { $ref: "#/components/schemas/KVList" } } } }
        default: { $ref: "#/components/responses/Problem" }
  /runtime/v1/apps/{application_id}/kv/{key}:
    servers: [{ url: https://play.screenrig.ai, description: Trusted player/runtime origin }]
    parameters: [{ $ref: "#/components/parameters/ApplicationID" }, { name: key, in: path, required: true, schema: { type: string } }]
    get:
      operationId: getRuntimeKV
      security: [{ runtimeCookie: [], placementCapability: [] }]
      responses:
        "200": { description: Manifest-authorized application KV value, content: { application/json: { schema: { $ref: "#/components/schemas/KVEntry" } } } }
        default: { $ref: "#/components/responses/Problem" }
    put:
      operationId: putRuntimeKV
      security: [{ pairedRuntimeCookie: [], placementCapability: [] }]
      parameters: [{ $ref: "#/components/parameters/OptionalIfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/KVWrite" } } } }
      responses:
        "200": { description: Paired-device KV mutation, content: { application/json: { schema: { $ref: "#/components/schemas/KVEntry" } } } }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deleteRuntimeKV
      security: [{ pairedRuntimeCookie: [], placementCapability: [] }]
      parameters: [{ $ref: "#/components/parameters/IfMatch" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "204": { description: Paired-device KV deletion }
        default: { $ref: "#/components/responses/Problem" }
components:
  parameters:
    ResourceID: { name: id, in: path, required: true, schema: { type: string } }
    ApplicationID: { name: application_id, in: path, required: true, schema: { type: string } }
    IdempotencyKey: { name: Idempotency-Key, in: header, required: true, schema: { type: string, minLength: 8, maxLength: 200 } }
    ArchiveSHA256: { name: ScreenRig-Archive-SHA256, in: header, required: true, schema: { type: string, pattern: "^[0-9a-f]{64}$" } }
    ExpandedBytes: { name: ScreenRig-Expanded-Bytes, in: header, required: true, schema: { type: integer, minimum: 1, maximum: 262144000 } }
    FileCount: { name: ScreenRig-File-Count, in: header, required: true, schema: { type: integer, minimum: 1, maximum: 5000 } }
    SDKVersion: { name: ScreenRig-SDK-Version, in: header, required: false, schema: { type: string, maxLength: 64 } }
    ApplicationName: { name: ScreenRig-Application-Name, in: header, required: false, schema: { type: string, maxLength: 120 } }
    IfMatch: { name: If-Match, in: header, required: true, schema: { type: string } }
    OptionalIfMatch: { name: If-Match, in: header, required: false, schema: { type: string }, description: Omit only when creating a new key; updates require the current quoted revision. }
    AfterCursor: { name: after, in: query, required: false, schema: { type: string } }
    LastEventID: { name: Last-Event-ID, in: header, required: false, schema: { type: string }, description: Durable cursor used when after is absent. }
    Range: { name: Range, in: header, required: false, schema: { type: string, pattern: "^bytes=[0-9]*-[0-9]*$" }, description: V1 accepts one byte range only. }
    SecFetchSite: { name: Sec-Fetch-Site, in: header, required: false, schema: { type: string, enum: [same-origin, same-site, cross-site, none] } }
    SecFetchMode: { name: Sec-Fetch-Mode, in: header, required: false, schema: { type: string } }
    SecFetchDest: { name: Sec-Fetch-Dest, in: header, required: false, schema: { type: string } }
  securitySchemes:
    accountBearer: { type: http, scheme: bearer, bearerFormat: sr_live }
    runtimeCookie: { type: apiKey, in: cookie, name: __Host-screenrig-runtime }
    pairedRuntimeCookie: { type: apiKey, in: cookie, name: __Host-screenrig-runtime, description: Signed runtime session with paired-device mutation profile. }
    pairedDeviceCookie: { type: apiKey, in: cookie, name: __Host-screenrig-device }
    pairingCookie: { type: apiKey, in: cookie, name: __Host-screenrig-pairing }
    placementCapability:
      type: apiKey
      in: header
      name: ScreenRig-Placement-Capability
      description: >-
        Parent-only signed five-minute placement capability bound to the runtime
        session, screen, application release, placement, manifest revision,
        content-access generation, and allowed KV operations. Refresh by fetching
        the current manifest.
    releaseGrantCookie: { type: apiKey, in: cookie, name: __Host-screenrig-release, description: Exact-host release/screen/generation/grant cookie; never valid on runtime or account routes. }
    releaseLaunchTicket: { type: apiKey, in: query, name: ticket, description: Random single-use 30-second ticket consumed only on its exact release host. }
    operatorBearer: { type: http, scheme: bearer }
  responses:
    Problem:
      description: Stable RFC 9457 problem response.
      content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } }
  schemas:
    HealthResponse: { type: object, additionalProperties: false, required: [status], properties: { status: { type: string, enum: [alive] } } }
    ReadyResponse: { type: object, additionalProperties: false, required: [status, degraded], properties: { status: { type: string, enum: [ready] }, degraded: { type: array, items: { type: string } } } }
    VersionResponse: { type: object, additionalProperties: false, required: [version, commit, api_version, protocol_version], properties: { version: { type: string }, commit: { type: string }, api_version: { type: string }, protocol_version: { type: string } } }
    Capabilities:
      type: object
      additionalProperties: false
      required: [api_version, protocol_version, application_compressed_bytes, application_expanded_bytes, application_file_count, application_file_bytes, application_path_depth, application_path_bytes, playlist_max_pages, playlist_max_items_per_page, transition_max_duration_ms, screens_per_account, account_content_bytes, features]
      properties:
        api_version: { type: string }
        protocol_version: { type: string }
        application_compressed_bytes: { type: integer, enum: [104857600] }
        application_expanded_bytes: { type: integer, enum: [262144000] }
        application_file_count: { type: integer, enum: [5000] }
        application_file_bytes: { type: integer, enum: [33554432] }
        application_path_depth: { type: integer, enum: [16] }
        application_path_bytes: { type: integer, enum: [255] }
        playlist_max_pages: { type: integer, enum: [100] }
        playlist_max_items_per_page: { type: integer, enum: [24] }
        transition_max_duration_ms: { type: integer, enum: [60000] }
        screens_per_account: { type: integer, enum: [50] }
        account_content_bytes: { type: integer, enum: [1073741824] }
        features: { type: object, additionalProperties: { type: boolean } }
    AccountCreate: { type: object, additionalProperties: false, required: [email], properties: { email: { type: string } } }
    Account: { type: object, additionalProperties: false, required: [id, email, revision, status, used_bytes, reserved_bytes, screen_count, content_limit_bytes, screen_limit], properties: { id: { type: string }, email: { type: string }, revision: { type: integer }, status: { type: string }, used_bytes: { type: integer }, reserved_bytes: { type: integer }, screen_count: { type: integer }, content_limit_bytes: { type: integer }, screen_limit: { type: integer } } }
    AccountCreated: { type: object, additionalProperties: false, required: [account, token, issuance_id], properties: { account: { $ref: "#/components/schemas/Account" }, token: { type: string }, issuance_id: { type: string } } }
    Application: { type: object, additionalProperties: true, required: [id, name, revision, state], properties: { id: { type: string }, name: { type: string }, revision: { type: integer }, state: { type: string }, release_id: { type: string } } }
    ApplicationList: { type: object, additionalProperties: false, required: [items], properties: { items: { type: array, items: { $ref: "#/components/schemas/Application" } } } }
    OperationAccepted: { type: object, additionalProperties: false, required: [id, operation_id], properties: { id: { type: string }, operation_id: { type: string } } }
    Operation: { type: object, additionalProperties: true, required: [id, kind, state, created_at, updated_at], properties: { id: { type: string }, kind: { type: string }, state: { type: string, enum: [receiving, queued, running, succeeded, failed, cancelled], description: "Receiving means the bounded upload body is still being durably staged. Terminal values are succeeded, failed, and cancelled." }, request_id: { type: string }, created_at: { type: string }, updated_at: { type: string }, result: { type: object, additionalProperties: true }, error: { type: object, additionalProperties: true } } }
    PlaylistWrite: { type: object, additionalProperties: false, required: [name, pages], properties: { name: { type: string }, pages: { type: array, items: { type: object, additionalProperties: true } } } }
    Playlist: { type: object, additionalProperties: false, required: [id, name, revision, pages], properties: { id: { type: string }, name: { type: string }, revision: { type: integer }, pages: { type: array, items: { type: object, additionalProperties: true } } } }
    PlaylistList: { type: object, additionalProperties: false, required: [items], properties: { items: { type: array, items: { $ref: "#/components/schemas/Playlist" } } } }
    PairScreen: { type: object, additionalProperties: false, required: [code], properties: { code: { type: string, minLength: 6, maxLength: 6 }, label: { type: string } } }
    ScreenPatch: { type: object, additionalProperties: false, minProperties: 1, properties: { name: { type: string, minLength: 1, maxLength: 120 }, playlist_id: { type: string } } }
    Screen: { type: object, additionalProperties: false, required: [id, public_id, label, revision, manifest_revision, content_access_generation, state, created_at, updated_at], properties: { id: { type: string }, public_id: { type: string }, label: { type: string }, playlist_id: { type: string }, revision: { type: integer }, manifest_revision: { type: integer }, content_access_generation: { type: integer }, state: { type: string, enum: [pairing_pending, active] }, created_at: { type: string, format: date-time }, updated_at: { type: string, format: date-time } } }
    ScreenList: { type: object, additionalProperties: false, required: [items], properties: { items: { type: array, items: { $ref: "#/components/schemas/Screen" } } } }
    Event: { type: object, additionalProperties: true, required: [cursor, sequence, type, severity, message, at], properties: { cursor: { type: string }, sequence: { type: integer }, type: { type: string }, severity: { type: string }, request_id: { type: string }, operation_id: { type: string }, message: { type: string }, resource: { type: object, additionalProperties: true }, details: { type: object, additionalProperties: true }, at: { type: string } } }
    EventList: { type: object, additionalProperties: false, required: [items, next_cursor], properties: { items: { type: array, items: { $ref: "#/components/schemas/Event" } }, next_cursor: { type: string } } }
    KVWrite: { type: object, additionalProperties: false, required: [value_base64, content_type], properties: { value_base64: { type: string, contentEncoding: base64, maxLength: 1398104 }, content_type: { type: string, minLength: 1, maxLength: 127 } } }
    KVEntry: { type: object, additionalProperties: false, required: [application_id, key, value_base64, content_type, bytes, sha256, revision], properties: { application_id: { type: string }, key: { type: string }, value_base64: { type: string, contentEncoding: base64 }, content_type: { type: string }, bytes: { type: integer }, sha256: { type: string, pattern: "^[a-f0-9]{64}$" }, revision: { type: integer } } }
    KVSummary: { type: object, additionalProperties: false, required: [application_id, key, content_type, bytes, sha256, revision], properties: { application_id: { type: string }, key: { type: string }, content_type: { type: string }, bytes: { type: integer }, sha256: { type: string, pattern: "^[a-f0-9]{64}$" }, revision: { type: integer }, created_at: { type: string }, updated_at: { type: string } } }
    KVList: { type: object, additionalProperties: false, required: [items], properties: { items: { type: array, maxItems: 200, items: { $ref: "#/components/schemas/KVSummary" } } } }
    MediaUploadDeclaration: { type: object, additionalProperties: false, required: [filename, content_type, bytes, sha256], properties: { filename: { type: string, minLength: 1, maxLength: 255 }, content_type: { type: string, enum: [image/png, image/jpeg, image/webp, image/gif, video/mp4, video/webm] }, bytes: { type: integer, minimum: 1, maximum: 1073741824 }, sha256: { type: string, pattern: "^[a-f0-9]{64}$" } } }
    MediaCommit: { type: object, additionalProperties: false, required: [content_type, bytes, sha256], properties: { content_type: { type: string, enum: [image/png, image/jpeg, image/webp, image/gif, video/mp4, video/webm] }, bytes: { type: integer, minimum: 1, maximum: 1073741824 }, sha256: { type: string, pattern: "^[a-f0-9]{64}$" } } }
    MediaUploadSession: { type: object, additionalProperties: false, required: [id, operation, upload_url, method, headers, expires_at], properties: { id: { type: string }, operation: { $ref: "#/components/schemas/Operation" }, upload_url: { type: string, format: uri }, method: { type: string, enum: [PUT] }, headers: { type: object, additionalProperties: { type: string }, description: Exact signed request headers; clients must send every field verbatim. }, expires_at: { type: string, format: date-time } } }
    Media: { type: object, additionalProperties: false, required: [id, filename, kind, content_type, operation_id, sha256, bytes, revision, state, created_at, updated_at], properties: { id: { type: string }, filename: { type: string }, kind: { type: string, enum: [image, video] }, content_type: { type: string }, operation_id: { type: string }, sha256: { type: string, pattern: "^[a-f0-9]{64}$" }, bytes: { type: integer }, duration_ms: { type: integer, minimum: 1, description: "Verified container duration; present for ready video media." }, revision: { type: integer }, state: { type: string, enum: [ready] }, created_at: { type: string, format: date-time }, updated_at: { type: string, format: date-time } } }
    MediaList: { type: object, additionalProperties: false, required: [items], properties: { items: { type: array, items: { $ref: "#/components/schemas/Media" } } } }
    PairingSession: { type: object, additionalProperties: false, required: [code, expires_at], properties: { code: { type: string }, expires_at: { type: string, format: date-time } } }
    PairingClaimedEvent: { type: object, additionalProperties: false, required: [type, completion_nonce], properties: { type: { type: string, enum: [pairing.claimed] }, completion_nonce: { type: string } } }
    PairingClaim: { type: object, additionalProperties: false, required: [screen, public_url], properties: { screen: { $ref: "#/components/schemas/Screen" }, public_url: { type: string } } }
    PairingComplete: { type: object, additionalProperties: false, required: [completion_nonce], properties: { completion_nonce: { type: string } } }
    PairingCompletion: { type: object, additionalProperties: false, required: [screen, public_url], properties: { screen: { $ref: "#/components/schemas/Screen" }, public_url: { type: string } } }
    RuntimeSessionRequest: { type: object, additionalProperties: false, required: [public_id], properties: { public_id: { type: string } } }
    RuntimeSession: { type: object, additionalProperties: false, required: [paired, capabilities, event_cursor, public_url], properties: { paired: { type: boolean }, capabilities: { type: array, items: { type: string, enum: [manifest.read, events.read, content.read, kv.read, kv.write, runtime.report, presence.write] } }, event_cursor: { type: string }, public_url: { type: string, format: uri } } }
    RuntimeManifest:
      type: object
      additionalProperties: false
      required: [schemaVersion, manifestRevision, grantId, contentGeneration, screenId, screenLabel, playlistRevision, generatedAt, pages]
      properties:
        schemaVersion: { type: integer, enum: [1] }
        manifestRevision: { type: string }
        grantId: { type: string }
        contentGeneration: { type: integer }
        screenId: { type: string }
        screenLabel: { type: string }
        playlistId: { type: string }
        playlistRevision: { type: integer }
        generatedAt: { type: string }
        pages: { type: array, items: { $ref: "#/components/schemas/RuntimePage" } }
    RuntimePage:
      type: object
      additionalProperties: false
      required: [id, grid, transition, advance, items]
      properties:
        id: { type: string }
        grid: { $ref: "#/components/schemas/RuntimeGrid" }
        transition: { $ref: "#/components/schemas/RuntimeTransition" }
        advance: { $ref: "#/components/schemas/RuntimeAdvance" }
        items: { type: array, items: { $ref: "#/components/schemas/RuntimePlacement" } }
    RuntimeGrid:
      type: object
      additionalProperties: false
      required: [style]
      properties:
        style: { type: object, additionalProperties: { type: string } }
        background: { type: string }
    RuntimeTransition:
      type: object
      additionalProperties: false
      required: [type, durationMs]
      properties:
        type: { type: string, enum: [crossfade] }
        durationMs: { type: integer }
    RuntimeAdvance:
      type: object
      additionalProperties: false
      required: [mode]
      properties:
        mode: { type: string, enum: [duration, application, video_end] }
        afterMs: { type: integer }
        maxMs: { type: integer }
    RuntimePlacement:
      oneOf:
        - { $ref: "#/components/schemas/RuntimeApplicationPlacement" }
        - { $ref: "#/components/schemas/RuntimePassivePlacement" }
    RuntimeApplicationPlacement:
      type: object
      additionalProperties: false
      required: [id, content, gridStyle, layer, capabilityHandle, capabilityExpiresAt]
      properties:
        id: { type: string }
        content: { $ref: "#/components/schemas/RuntimeApplicationContent" }
        gridStyle: { type: object, additionalProperties: { type: string } }
        layer: { type: integer }
        controller: { type: boolean }
        capabilityHandle: { type: string, minLength: 32, description: Opaque parent-only capability; never forward it into the uploaded iframe. }
        capabilityExpiresAt: { type: string, format: date-time, description: Five-minute expiry. Refresh the manifest before this time to obtain a replacement handle. }
    RuntimePassivePlacement:
      type: object
      additionalProperties: false
      required: [id, content, gridStyle, layer]
      properties:
        id: { type: string }
        content:
          oneOf:
            - { $ref: "#/components/schemas/RuntimeImageContent" }
            - { $ref: "#/components/schemas/RuntimeVideoContent" }
            - { $ref: "#/components/schemas/RuntimeIframeContent" }
        gridStyle: { type: object, additionalProperties: { type: string } }
        layer: { type: integer }
    RuntimeContent:
      oneOf:
        - { $ref: "#/components/schemas/RuntimeApplicationContent" }
        - { $ref: "#/components/schemas/RuntimeImageContent" }
        - { $ref: "#/components/schemas/RuntimeVideoContent" }
        - { $ref: "#/components/schemas/RuntimeIframeContent" }
      discriminator: { propertyName: type }
    RuntimeApplicationContent:
      type: object
      additionalProperties: false
      required: [type, applicationId, releaseId, launchUrl, origin, protocol, grantId]
      properties:
        type: { type: string, enum: [application] }
        applicationId: { type: string }
        releaseId: { type: string }
        launchUrl: { type: string }
        origin: { type: string }
        protocol: { type: string, enum: ["1"] }
        grantId: { type: string }
    RuntimeImageContent:
      type: object
      additionalProperties: false
      required: [type, mediaId, src, fit, allowUpscale, grantId, sha256, bytes, contentType]
      properties:
        type: { type: string, enum: [image] }
        mediaId: { type: string }
        src: { type: string }
        alt: { type: string }
        fit: { type: string, enum: [contain, cover, fill] }
        allowUpscale: { type: boolean }
        grantId: { type: string }
        sha256: { type: string, pattern: "^[a-f0-9]{64}$", description: Immutable content digest for integrity and optional native caching; not an authorization capability. }
        bytes: { type: integer, minimum: 1, maximum: 1073741824 }
        contentType: { type: string, enum: [image/png, image/jpeg, image/webp, image/gif] }
    RuntimeVideoContent:
      type: object
      additionalProperties: false
      required: [type, mediaId, src, fit, muted, loop, grantId, sha256, bytes, contentType]
      properties:
        type: { type: string, enum: [video] }
        mediaId: { type: string }
        src: { type: string }
        fit: { type: string, enum: [contain, cover, fill] }
        muted: { type: boolean }
        loop: { type: boolean }
        grantId: { type: string }
        sha256: { type: string, pattern: "^[a-f0-9]{64}$", description: Immutable content digest for integrity and optional native caching; not an authorization capability. }
        bytes: { type: integer, minimum: 1, maximum: 1073741824 }
        contentType: { type: string, enum: [video/mp4, video/webm] }
    RuntimeIframeContent:
      type: object
      additionalProperties: false
      required: [type, src, title]
      properties:
        type: { type: string, enum: [iframe] }
        src: { type: string }
        title: { type: string }
    RuntimeReport:
      oneOf:
        - { $ref: "#/components/schemas/ManifestActivatedReport" }
        - { $ref: "#/components/schemas/RuntimeConditionReport" }
    ManifestActivatedReport: { type: object, additionalProperties: false, required: [severity, code, context], properties: { severity: { type: string, enum: [info] }, code: { type: string, enum: [manifest.activated] }, context: { $ref: "#/components/schemas/ManifestActivatedContext" } } }
    ManifestActivatedContext: { type: object, additionalProperties: false, required: [manifest_revision], properties: { manifest_revision: { type: string, pattern: "^man_" } } }
    RuntimeConditionReport: { type: object, additionalProperties: false, required: [severity, code, context], properties: { severity: { type: string, enum: [info, warning, error] }, code: { type: string, pattern: "^[a-z0-9._-]{1,80}$", not: { const: manifest.activated } }, context: { type: object, additionalProperties: true } } }
    ReleaseLaunch: { type: object, additionalProperties: false, required: [launch_url, expires_at], properties: { launch_url: { type: string }, expires_at: { type: string } } }
    ProblemField: { type: object, additionalProperties: false, required: [field, code, detail], properties: { field: { type: string }, code: { type: string }, detail: { type: string } } }
    NextAction: { type: object, additionalProperties: false, required: [command, reason], properties: { command: { type: string }, reason: { type: string } } }
    Problem:
      type: object
      additionalProperties: true
      required: [type, title, status, detail, instance, code, request_id, errors]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        instance: { type: string }
        code: { type: string }
        request_id: { type: string }
        trace_id: { type: string }
        current_revision: { type: integer }
        errors: { type: array, items: { $ref: "#/components/schemas/ProblemField" } }
        next: { $ref: "#/components/schemas/NextAction" }
x-problem-registry: ./problems.yaml
x-problem-codes: [internal_error, invalid_request, unauthorized, forbidden, not_found, method_not_allowed, idempotency_mismatch, account_exists, resource_conflict, revision_conflict, invalid_range, quota_exceeded, rate_limited, dependency_unavailable, schema_incompatible, not_ready]
