openapi: 3.1.0
info:
  title: Bigscreen Public API
  version: '0'
  summary: Non-auth surface of apps/api — media, Beyond, channels, topics, info, analytics, reports, eye-tracking, Shopify.
  description: |
    The non-auth surface of [`apps/api/api.ts`](../../apps/api/api.ts).
    Everything the VR app and the website call to actually do work
    once a user is signed in: browse and play media, manage Beyond
    face-scan / order flows, fetch channels and topics, ingest
    analytics events, look up region / currency, file content
    reports, and serve the eye-tracking and Shopify webhook
    surfaces.

    Sister spec: [`auth-api`](./scalar/auth.html) on the same
    service hosts the `/auth/*`, OAuth, and account-management
    routes. Both are bound to port `3009` and share the `api`
    bearer key — the split is documentation-only.

    > Distinct from the cloud APIs ([cloud admin](./scalar/cloud.html),
    > [cloud public](./scalar/cloud-public.html)) and the IP-restricted
    > [`apps/admin_api`](./scalar/admin.html). Some component schemas are
    > shared with `auth-api` (e.g. `BigOrder`, `MediaProduct`).

    ## Auth at a glance

    Most requests need two headers:

    | Header | Value |
    |--------|-------|
    | `Authorization` | `Bearer <api app key>` |
    | `x-access-token` | A JWT issued by the auth API — required on routes marked `Auth: required` |

    Bypasses (no `Authorization` bearer needed):

    - `/eye-tracking/*` — uses its own JWT (`requiresEyeTrackingToken`).
    - `/shopify/webhook` — verified by Shopify HMAC; raw body required.

    ## Spec layout

    Sources are split per handler module under
    `docs/openapi/public-modules/*.yaml`, joined by `$ref` from this
    root file. The bundled artifact at
    `docs/openapi/public-api.bundled.yaml` is what the Scalar
    renderer reads — regenerate with
    `npx -y @redocly/cli@latest bundle docs/openapi/public-api.yaml -o docs/openapi/public-api.bundled.yaml`
    after any edit.
servers:
  - url: http://localhost:3009
    description: Local dev (started by `yarn dev` — port `3009` is hard-coded in `apps/api/api.ts`).
  - url: https://api.bigscreenvr.com
    description: Production.
security:
  - apiKey: []
    accessToken: []
tags:
  - name: Info
    description: |
      Public utility endpoints — username availability, server time,
      IP-based region / currency, room sanitization, sentiment scoring.
  - name: Beyond
    description: |
      Beyond headset support — face-scan request lifecycle, topology
      token presence checks, big-order retrieval and IPD update,
      Shopify Multipass URL.
  - name: Channels
    description: Public channel and channel-group reads (TV functions).
  - name: Topics
    description: Topic subscription / unsubscription for the messaging system.
  - name: Analytics
    description: Public analytics event ingestion.
  - name: Media
    description: |
      Media-product browsing, entitlements, playback manifests, orders
      (Stripe + Oculus IAP), receipts, and free / browser entitlement
      provisioning.
  - name: Reports
    description: |
      Content-reporting endpoints (account / room). Restricted to the
      `cloud-api` bearer — typically the public client never calls
      these directly; they are forwarded by `cloud_api` on behalf of
      end users.
  - name: EyeTracking
    description: |
      Beyond eye-tracking model and per-user file retrieval. Uses a
      dedicated JWT (`requiresEyeTrackingToken`) — no Bigscreen bearer
      / access token.
  - name: Shopify
    description: |
      Shopify-issued webhook callback. Signed via Shopify HMAC; raw
      body is required (this route bypasses the JSON parser).
paths:
  /info/account/{field}/{value}:
    parameters:
      - in: path
        name: field
        required: true
        schema:
          type: string
          enum:
            - username
            - userSessionId
        description: Lookup field — either `username` or `userSessionId`.
      - in: path
        name: value
        required: true
        schema:
          type: string
    get:
      tags:
        - Info
      operationId: findPublicAccountData
      summary: Public account lookup
      description: |
        Returns the public-facing snapshot of an account by `username`
        or `userSessionId`. Used by the VR app to resolve handles into
        display data without needing the full admin profile. Bearer
        key only — no access token required.
      security:
        - apiKey: []
      responses:
        '200':
          description: Public account data.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicAccountData'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: '`field` is not one of `username` / `userSessionId`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /info/username/{username}:
    parameters:
      - in: path
        name: username
        required: true
        schema:
          type: string
    get:
      tags:
        - Info
      operationId: checkUsername
      summary: Username availability + validity
      description: |
        Returns `200` when the username is well-formed AND not currently
        taken. Used by the sign-up form for live validation.
      responses:
        '200':
          description: Available.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: |
            Username is malformed (length / character class /
            banned word) or already taken. The response body is
            the validator's failure record or a `{ msg }` payload
            — clients should not depend on a specific shape.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /info/room/{roomId}/sanitize:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
    put:
      tags:
        - Info
      operationId: sanitizeRoomData
      summary: Sanitize room input
      description: |
        Applies the banned-words / NSFW filters to room metadata
        (title, description, etc.). Returns the cleaned shape — the
        client uses this before publishing a room.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Free-form room data — title, description, tags, etc.
              additionalProperties: true
      responses:
        '200':
          description: Sanitized room data.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
  /info/room/{roomId}/sentiment:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
    put:
      tags:
        - Info
      operationId: getSentimentForRoomData
      summary: Sentiment score for room input
      description: Returns a sentiment score for the supplied room metadata.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Sentiment score.
          content:
            application/json:
              schema:
                type: object
                properties:
                  score:
                    type: number
                    format: float
                  label:
                    type: string
                    description: e.g. `positive`, `neutral`, `negative`.
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
  /info/timestamp:
    get:
      tags:
        - Info
      operationId: getServerTimestamp
      summary: Server timestamp
      description: |
        Returns the server's current Unix-millisecond timestamp in the
        `x-timestamp` response header. The body is the literal string
        `OK`.
      responses:
        '200':
          description: Timestamp emitted in the `x-timestamp` response header.
          headers:
            x-timestamp:
              description: Server Unix-millisecond timestamp.
              schema:
                type: string
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
  /info/region:
    get:
      tags:
        - Info
      operationId: getRegionCode
      summary: Geo-IP region code (auth optional)
      description: |
        Returns the caller's region code derived from their IP. Access
        token is **optional** — present when the client wants the
        account's stored region preference factored in.
      security:
        - apiKey: []
      responses:
        '200':
          description: Region code.
          content:
            application/json:
              schema:
                type: object
                required:
                  - region
                properties:
                  region:
                    type: string
                    description: ISO 3166-1 alpha-2 country code.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /info/currency:
    get:
      tags:
        - Info
      operationId: getCurrencyData
      summary: Geo-IP currency code (auth optional)
      description: |
        Returns the currency code (and conversion metadata) appropriate
        for the caller's region. Access token is **optional**.
      security:
        - apiKey: []
      responses:
        '200':
          description: Currency payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrencyInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /beyond/scans/{scanRequestState}:
    parameters:
      - in: path
        name: scanRequestState
        required: true
        schema:
          type: string
        description: |
          Scan-request state filter. Optional in the underlying route
          (`/beyond/scans{/:scanRequestState}`) — when omitted, all
          states are returned.
    get:
      tags:
        - Beyond
      operationId: getScanRequests
      summary: Caller's pending scans
      description: |
        Lists the scan requests on the caller's account, optionally
        filtered by state. Returns the public projection
        (`PublicScanRequest`), not the admin shape.
      responses:
        '200':
          description: Scan requests.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PublicScanRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /beyond/topology/{topologyToken}:
    parameters:
      - in: path
        name: topologyToken
        required: true
        schema:
          type: string
    get:
      tags:
        - Beyond
      operationId: getTopologyTokenPresence
      summary: Topology-token presence check (auth optional)
      description: |
        Returns `200` plus a `redirectUrl` when the customer id behind
        this topology token exists on this server, `404` when it does
        not. Used by the iOS scanning app to confirm the right server
        and to deep-link the customer into the right next step.
      security:
        - apiKey: []
      responses:
        '200':
          description: Token recognised.
          content:
            application/json:
              schema:
                type: object
                required:
                  - redirectUrl
                properties:
                  redirectUrl:
                    type: string
                    format: uri
                    description: Where the iOS scanning app should send the customer next.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Token not on this server.
  /beyond/scan/{topologyToken}/process:
    parameters:
      - in: path
        name: topologyToken
        required: true
        schema:
          type: string
    get:
      tags:
        - Beyond
      operationId: processTopologyScan
      summary: Process a scan by topology token
      description: |
        Triggers downstream processing for the scan associated with
        the supplied topology token. Long-running on the server side;
        returns once the job is queued. **Returns plain text `OK`** —
        not JSON.
      responses:
        '200':
          description: Processing started. Body is the literal string `OK`.
          content:
            text/plain:
              schema:
                type: string
                example: OK
        '401':
          description: |
            Caller does not own the scan referenced by the token.
            *Note: handler returns 401 here even though 403 would
            be more correct semantically.*
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: '`ScanProcessorError` — invalid scan, preprocessing failed, etc.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  scan:
                    type: object
                    additionalProperties: true
  /beyond/multipass:
    get:
      tags:
        - Beyond
      operationId: getMultipassUrl
      summary: Shopify Multipass URL for the account
      description: |
        Returns a Shopify Multipass URL bound to the caller's account —
        used to deep-link the customer into their Shopify session
        (orders, returns) without a separate login.
      responses:
        '200':
          description: Multipass URL.
          content:
            application/json:
              schema:
                type: object
                required:
                  - url
                properties:
                  url:
                    type: string
                    format: uri
        '401':
          $ref: '#/components/responses/Unauthorized'
  /beyond/orders:
    get:
      tags:
        - Beyond
      operationId: getBigOrders
      summary: Caller's Beyond orders
      description: |
        Returns the `BigOrder`s associated with the caller's account
        (raw rows, not `getPublicData()` — historical inconsistency
        with `/beyond/order/{bigOrderId}`).
      responses:
        '200':
          description: Orders.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BigOrder'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /beyond/order/{bigOrderId}:
    parameters:
      - in: path
        name: bigOrderId
        required: true
        schema:
          type: string
    get:
      tags:
        - Beyond
      operationId: getBigOrder
      summary: One Beyond order
      description: |
        Returns a single `BigOrder` owned by the caller, via the
        customer-facing `getPublicData()` projection — the response
        is a *subset* of the full
        [`BigOrder`](../components/schemas/BigOrder.yaml)
        schema (admin-only fields like `currentLineItems`,
        `nextAction`, `priority`, `tags`, `history`,
        `creatorBigscreenAccountId`, `ownerBigscreenAccountId`,
        `shipmentHold`, `shippingServiceLevel` are stripped). The
        handler additionally attaches `ipdFromShopify`.
      responses:
        '200':
          description: Order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BigOrder'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own this order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /beyond/order/{bigOrderId}/ipd:
    parameters:
      - in: path
        name: bigOrderId
        required: true
        schema:
          type: string
    put:
      tags:
        - Beyond
      operationId: updateBigOrderFromCustomer
      summary: Set IPD on a Beyond order
      description: |
        Lets the customer set / update the IPD (interpupillary distance)
        on their Beyond order. Restricted to orders the caller owns.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - ipd
              properties:
                ipd:
                  type: number
                  description: IPD in millimetres.
      responses:
        '200':
          description: Updated order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BigOrder'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own this order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /beyond/scan/{scanRequestId}:
    parameters:
      - in: path
        name: scanRequestId
        required: true
        schema:
          type: string
    get:
      tags:
        - Beyond
      operationId: getScanRequest
      summary: One scan request
      description: |
        Returns a single scan belonging to the caller, via the
        `PublicScanRequest` projection (admin / fabricator fields
        stripped). For the full admin shape see
        [`ScanRequest`](../components/schemas/ScanRequest.yaml).
      responses:
        '200':
          description: Scan request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicScanRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own this scan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /beyond/scan/{scanRequestId}/rescan:
    parameters:
      - in: path
        name: scanRequestId
        required: true
        schema:
          type: string
    put:
      tags:
        - Beyond
      operationId: handleRescan
      summary: Customer-initiated rescan
      description: |
        Lets a logged-in customer mark a scan for re-scan. Updates
        the underlying `ScanRequest` state so the iOS app knows to
        re-collect.
      responses:
        '200':
          description: Rescan flag set; `PublicScanRequest`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicScanRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own this scan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /channel_groups:
    get:
      tags:
        - Channels
      operationId: getPublicChannelGroups
      summary: All public channel groups
      description: Returns every publicly-visible `ChannelGroup`.
      responses:
        '200':
          description: Channel groups.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ChannelGroup'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /channel_group/{name}:
    parameters:
      - in: path
        name: name
        required: true
        schema:
          type: string
    get:
      tags:
        - Channels
      operationId: getPublicChannelGroup
      summary: One channel group
      description: Returns a `ChannelGroup` by name, including its public channels.
      responses:
        '200':
          description: Channel group.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelGroup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /channel/{channelId}:
    parameters:
      - in: path
        name: channelId
        required: true
        schema:
          type: string
    get:
      tags:
        - Channels
      operationId: getPublicChannel
      summary: One channel (auth optional)
      description: |
        Returns a `Channel` by id. Access token is **optional** — when
        present, the response may include personalised fields
        (subscription state, entitlement gating).
      security:
        - apiKey: []
      responses:
        '200':
          description: Channel.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /topic/{topicName}:
    parameters:
      - in: path
        name: topicName
        required: true
        schema:
          type: string
    get:
      tags:
        - Topics
      operationId: getPublicTopic
      summary: One topic
      description: Returns the public-facing description of a `Topic`.
      responses:
        '200':
          description: Topic.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Topic'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /topic/{topicName}/subscribe:
    parameters:
      - in: path
        name: topicName
        required: true
        schema:
          type: string
    get:
      tags:
        - Topics
      operationId: subscribeToTopic
      summary: Subscribe to a topic
      description: |
        Subscribes the calling account to the named topic. Idempotent.
        Uses GET because the VR app dispatches it from a magic-link
        redirect.
      responses:
        '200':
          description: Subscribed.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /topic/{topicName}/unsubscribe:
    parameters:
      - in: path
        name: topicName
        required: true
        schema:
          type: string
    get:
      tags:
        - Topics
      operationId: unsubscribeFromTopic
      summary: Unsubscribe from a topic
      description: |
        Removes the calling account's subscription to the named topic.
        Idempotent — safe to retry.
      responses:
        '200':
          description: Unsubscribed.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /event:
    post:
      tags:
        - Analytics
      operationId: createEvent
      summary: Ingest an analytics event
      description: |
        Records a single analytics event. Access token is **optional** —
        anonymous events are accepted (with the bearer key still
        required for app identification).
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Event name — namespaced by convention (e.g. `app.media.start`).
                properties:
                  type: object
                  description: Event-specific payload.
                  additionalProperties: true
                timestamp:
                  type: integer
                  description: Optional Unix-millisecond timestamp; server-default when omitted.
      responses:
        '200':
          description: Accepted.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /token/purchase/{bigMediaId}:
    parameters:
      - in: path
        name: bigMediaId
        required: true
        schema:
          type: string
    get:
      tags:
        - Media
      operationId: sendPurchaseTokenByEmailLegacy
      summary: Email a purchase magic-link (legacy path)
      description: |
        Emails the caller a magic-link they can use to purchase the
        specified media product. Identical handler as the
        `/media/token/purchase/{bigMediaId}` route — kept at the
        top level for legacy clients.
      responses:
        '200':
          description: Email queued.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/category/{name}:
    parameters:
      - in: path
        name: name
        required: true
        schema:
          type: string
    get:
      tags:
        - Media
      operationId: getPublicCategory
      summary: One media category (auth optional)
      description: |
        Returns a public `MediaCategory` by name. Access token is
        optional — when present, the response may include
        personalised flags (entitlement gating, ordering).
      security:
        - apiKey: []
      responses:
        '200':
          description: Category.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaCategory'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/categories/{name}/{after}:
    parameters:
      - in: path
        name: name
        required: true
        schema:
          type: string
      - in: path
        name: after
        required: true
        schema:
          type: string
        description: |
          ISO date or cursor string. Optional in the underlying route
          (`/media/categories/:name{/:after}`) — when omitted, returns
          the unpaginated list.
    get:
      tags:
        - Media
      operationId: getPublicCategoryCollections
      summary: Category collections after a cursor (auth optional)
      description: |
        Returns the `ProductCollection`s in a category, optionally
        cursored at `after`. Access token is optional.
      security:
        - apiKey: []
      responses:
        '200':
          description: Collections.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ProductCollection'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/categories_as_events/{name}:
    parameters:
      - in: path
        name: name
        required: true
        schema:
          type: string
    get:
      tags:
        - Media
      operationId: getPublicCategoryCollectionsAsEvents
      summary: Category collections as events (auth optional)
      description: |
        HACK alternative shape — returns the current and future
        collections in the category formatted as event records (start /
        end times, titles). Used by the website's "What's on" widget.
      security:
        - apiKey: []
      responses:
        '200':
          description: Event-shaped collections.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/product/{bigMediaId}:
    parameters:
      - in: path
        name: bigMediaId
        required: true
        schema:
          type: string
    get:
      tags:
        - Media
      operationId: getPublicMediaProduct
      summary: One media product (auth optional)
      description: |
        Returns the public-facing `MediaProduct` by id. Access token
        is optional — when present, may include personalised price /
        availability for the caller's region.
      security:
        - apiKey: []
      responses:
        '200':
          description: Media product.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaProduct'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/location:
    get:
      tags:
        - Media
      operationId: getPublicMediaLocation
      summary: Media-pricing location (auth optional)
      description: |
        Returns the location info the server uses for media pricing
        and playback decisions (region code, currency, rights region).
        Access token is optional.
      security:
        - apiKey: []
      responses:
        '200':
          description: Location payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  region:
                    type: string
                  country:
                    type: string
                  currency:
                    type: string
                  rightsRegion:
                    type: string
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/entitlements/{bigMediaId}:
    parameters:
      - in: path
        name: bigMediaId
        required: true
        schema:
          type: string
        description: |
          Media product to scope to. Optional in the underlying route
          (`/media/entitlements{/:bigMediaId}`) — when omitted, returns
          every entitlement on the caller's account.
    get:
      tags:
        - Media
      operationId: getPublicEntitlements
      summary: Caller's entitlements (optionally scoped)
      description: |
        Returns the caller's `Entitlement`s — optionally filtered to
        a specific `bigMediaId`. Used by the VR app to decide
        playback / order paths.
      responses:
        '200':
          description: Entitlements.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Entitlement'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/checksum:
    get:
      tags:
        - Media
      operationId: getEntitlementsChecksum
      summary: Entitlements checksum
      description: |
        Returns a hash of the caller's entitlement set. The VR client
        polls this and only refetches the full entitlements list when
        the hash changes — keeps refresh traffic down.
      responses:
        '200':
          description: Checksum payload.
          content:
            application/json:
              schema:
                type: object
                required:
                  - checksum
                properties:
                  checksum:
                    type: string
                  computedAt:
                    type: integer
                    description: Unix milliseconds.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/check/{bigMediaId}:
    parameters:
      - in: path
        name: bigMediaId
        required: true
        schema:
          type: string
    get:
      tags:
        - Media
      operationId: checkMediaEntitlement
      summary: Caller's entitlement on one product
      description: |
        Returns the caller's `Entitlement`(s) on a specific media
        product — the same handler as `/media/entitlements/{id}`,
        scoped down.
      responses:
        '200':
          description: Entitlement(s).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Entitlement'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/play/{bigMediaId}:
    parameters:
      - in: path
        name: bigMediaId
        required: true
        schema:
          type: string
    get:
      tags:
        - Media
      operationId: playMedia
      summary: Playback manifest for a media product
      description: |
        Returns the playback manifest (DRM URLs, audio tracks,
        subtitles) for a media product. Requires a valid entitlement;
        returns `403` otherwise.
      responses:
        '200':
          description: Manifest.
          content:
            application/json:
              schema:
                type: object
                description: |
                  Manifest shape varies by product format
                  (HLS, DASH, MP4) — keys include `url`,
                  `audioTracks`, `subtitleTracks`, `drm`.
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: No entitlement for this product.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/start/{mediaRequest}:
    parameters:
      - in: path
        name: mediaRequest
        required: true
        schema:
          type: string
        description: |
          Comma-or-`|`-separated query string of fallback identifiers
          the server resolves in order, returning the first hit.
    get:
      tags:
        - Media
      operationId: startMedia
      summary: Resolve and start a media stream
      description: |
        Walks the supplied fallback list and returns playback details
        (URL + metadata) for the first product that resolves.
      responses:
        '200':
          description: Playback descriptor.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/token/purchase/{bigMediaId}:
    parameters:
      - in: path
        name: bigMediaId
        required: true
        schema:
          type: string
    get:
      tags:
        - Media
      operationId: sendPurchaseTokenByEmail
      summary: Email a purchase magic-link
      description: |
        Emails the caller a magic-link that lets the recipient (the
        same account) complete the purchase outside VR. Same handler
        as the legacy `/token/purchase/{bigMediaId}` route.
      responses:
        '200':
          description: Email queued.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/try-automatic-order:
    post:
      tags:
        - Media
      operationId: tryCreateAutomaticOrder
      summary: Auto-order a free media product
      description: |
        For products priced at $0.00 in the caller's location, creates
        the order automatically without going through Stripe. No-op
        (`200` with a no-op body) when the product has a non-zero
        price.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - bigMediaId
              properties:
                bigMediaId:
                  type: string
      responses:
        '200':
          description: Order created (or no-op).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ordered:
                    type: boolean
                  entitlement:
                    $ref: '#/components/schemas/Entitlement'
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/order:
    post:
      tags:
        - Media
      operationId: createOrder
      summary: Purchase a media product (Stripe)
      description: |
        Creates a Stripe-backed order for the supplied media product.
        **Account must be verified** — calls `requiresVerifiedAccount`,
        which returns `401` with code `RequiresVerifiedEmailAddress`
        for unverified accounts (semantically a 403, but historically
        modeled as 401).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - bigMediaId
              properties:
                bigMediaId:
                  type: string
                paymentMethodId:
                  type: string
                  description: Stripe `pm_...` id from the client SDK.
                couponCode:
                  type: string
      responses:
        '200':
          description: Order created.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          description: |
            Missing / invalid token, **or** account is not
            verified (code `RequiresVerifiedEmailAddress`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/entitlement/{entitlementClass}:
    parameters:
      - in: path
        name: entitlementClass
        required: true
        schema:
          type: string
    put:
      tags:
        - Media
      operationId: createFreeEntitlement
      summary: Provision a free entitlement
      description: |
        Creates a free entitlement of the supplied class on the
        caller's account when one does not already exist. Idempotent.
      responses:
        '200':
          description: Entitlement (created or already-present).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Entitlement'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/entitlement/{entitlementId}/activate:
    parameters:
      - in: path
        name: entitlementId
        required: true
        schema:
          type: string
    put:
      tags:
        - Media
      operationId: activateBrowserEntitlement
      summary: Activate a browser entitlement
      description: |
        Activates a browser-flavored entitlement — used by the website
        to convert a "free trial" entitlement into the active browser
        session.
      responses:
        '200':
          description: Activated entitlement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Entitlement'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/entitlement/browser:
    get:
      tags:
        - Media
      operationId: getBrowserEntitlements
      summary: Currently-active browser entitlement (deprecated)
      description: |
        **Deprecated** — prefer `/media/entitlement/features`. Returns
        the browser entitlement currently active on the caller's
        account.
      deprecated: true
      responses:
        '200':
          description: Browser entitlement (or empty).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Entitlement'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/entitlement/features:
    get:
      tags:
        - Media
      operationId: getFeatureFlagEntitlements
      summary: Feature-flag entitlements (biggies)
      description: |
        Returns the caller's feature-flag entitlements — the
        "biggies" — used for rollout-style gating.
      responses:
        '200':
          description: Feature-flag entitlements.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Entitlement'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/oculus/order:
    post:
      tags:
        - Media
      operationId: createOculusOrder
      summary: Convert an Oculus IAP token into an entitlement
      description: |
        Consumes an Oculus IAP token (returned by the Oculus billing
        SDK on the headset) and writes the corresponding `Entitlement`
        to the caller's account. **Verified account is NOT required**
        for IAP orders — verification happens through the IAP token
        itself.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - iapToken
              properties:
                iapToken:
                  type: string
                bigMediaId:
                  type: string
                  description: Optional — server-derived from the IAP token when omitted.
      responses:
        '200':
          description: Entitlement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Entitlement'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: IAP token already consumed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /media/oculus/update:
    post:
      tags:
        - Media
      operationId: updateOculusPurchases
      summary: Sync any unconsumed Oculus IAPs
      description: |
        Walks the caller's Oculus IAP receipts and writes
        `Entitlement`s for any that have not yet been ingested. Used
        by the VR app on launch to recover after a failed in-flight
        purchase.
      responses:
        '200':
          description: Sync result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  createdCount:
                    type: integer
                  entitlements:
                    type: array
                    items:
                      $ref: '#/components/schemas/Entitlement'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/receipts:
    get:
      tags:
        - Media
      operationId: getPublicReceipts
      summary: Caller's purchase receipts
      description: Returns the caller's public-facing purchase receipts.
      responses:
        '200':
          description: Receipts.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
  /media/info/{mediaRequest}:
    parameters:
      - in: path
        name: mediaRequest
        required: true
        schema:
          type: string
    get:
      tags:
        - Media
      operationId: getMediaInfo
      summary: Public info for a media request
      description: |
        Returns public metadata for the supplied media request
        (resolves the same fallback list as `/media/start`).
      responses:
        '200':
          description: Media info.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /media/analytics/engagement/{videoPosition}:
    parameters:
      - in: path
        name: videoPosition
        required: true
        schema:
          type: string
        description: Position marker — typically a percentage band or millisecond offset.
    get:
      tags:
        - Media
      operationId: sendMediaSessionEngagement
      summary: Record media-engagement event
      description: |
        Beacon-style endpoint the VR client calls to record continued
        engagement with a media session at the supplied position
        marker. Drives playback-completion analytics.
      responses:
        '200':
          description: Recorded.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /report/user:
    post:
      tags:
        - Reports
      operationId: reportAccount
      summary: Report a user account
      description: |
        Submits an account report. Restricted to the `cloud-api`
        bearer (`restrictRequestToApp("cloud-api")`) — `cloud_api`
        forwards reports filed by end users; the public client does
        not call this directly. Returns `{ message: "OK" }` on
        success.
      security:
        - apiKey: []
          accessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - targetSocialId
                - reason
              properties:
                targetSocialId:
                  type: string
                  description: Social id of the account being reported.
                reason:
                  type: string
                  description: |
                    Free-form report text. The reporting
                    client typically combines a coarse
                    category and the user's free input.
                metaData:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Report accepted.
          content:
            application/json:
              schema:
                type: object
                required:
                  - message
                properties:
                  message:
                    type: string
                    example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller bearer is not the `cloud-api` app.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Reported user not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /report/room:
    post:
      tags:
        - Reports
      operationId: reportRoom
      summary: Report a room
      description: |
        Submits a room report. Same restriction as `/report/user` —
        only the `cloud-api` bearer is admitted. Returns
        `{ message: "OK" }` on success.
      security:
        - apiKey: []
          accessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
                - roomSnapshot
              properties:
                reason:
                  type: string
                  description: Free-form report text.
                roomSnapshot:
                  type: object
                  description: |
                    Snapshot of the room at the time of
                    reporting — denormalised so the
                    report survives room deletion.
                  additionalProperties: true
                roomCreatorSocialId:
                  type: string
                  description: Social id of the room creator, when known.
                metaData:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Report accepted.
          content:
            application/json:
              schema:
                type: object
                required:
                  - message
                properties:
                  message:
                    type: string
                    example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller bearer is not the `cloud-api` app.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /eye-tracking/model:
    get:
      tags:
        - EyeTracking
      operationId: getEyeTrackingModel
      summary: Eye-tracking model file (302 redirect)
      description: |
        **Returns a `302` redirect** to a presigned S3 URL pointing
        to the active eye-tracking model. Standard `apiKey` /
        `accessToken` middleware does **not** run on `/eye-tracking/*` —
        this route is gated solely by the per-headset JWT.

        The `model_s3_uri` query string parameter is required.
      security:
        - eyeTrackingToken: []
      parameters:
        - in: query
          name: model_s3_uri
          required: true
          schema:
            type: string
            format: uri
          description: S3 URI the headset wants a presigned URL for.
      responses:
        '302':
          description: Redirect to the presigned S3 URL.
          headers:
            Location:
              description: Presigned S3 URL valid for a short window.
              schema:
                type: string
                format: uri
        '400':
          description: Missing `model_s3_uri` query parameter.
        '401':
          description: Missing or invalid `eyeTrackingToken`.
  /eye-tracking/user-files:
    get:
      tags:
        - EyeTracking
      operationId: getEyeTrackingUserFiles
      summary: Per-user eye-tracking calibration file keys
      description: |
        Returns the **list of S3 keys** for calibration / personalisation
        files associated with the headset's `eyeTrackingToken`. The
        client fetches the actual files via separate presigned-URL
        calls.

        Supports `If-None-Match` — returns `304` when the supplied
        ETag still matches.
      security:
        - eyeTrackingToken: []
      parameters:
        - in: header
          name: If-None-Match
          required: false
          schema:
            type: string
      responses:
        '200':
          description: File-key list.
          headers:
            ETag:
              description: Hash of the file-key list — usable on the next request.
              schema:
                type: string
            Cache-Control:
              schema:
                type: string
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
                  description: S3 key.
        '304':
          description: ETag matches — no body.
        '401':
          description: Missing or invalid `eyeTrackingToken`.
  /eye-tracking/user-id:
    get:
      tags:
        - EyeTracking
      operationId: getEyeTrackingUserId
      summary: User id linked to the eye-tracking token
      description: |
        Returns the **bare user-id string** (the JWT's `u` claim)
        that the `eyeTrackingToken` is currently bound to. Used by
        the headset to confirm pairing.
      security:
        - eyeTrackingToken: []
      responses:
        '200':
          description: User identifier (raw JSON string, not an object).
          content:
            application/json:
              schema:
                type: string
                description: Bare user-id string.
        '401':
          description: Missing or invalid `eyeTrackingToken`.
  /shopify/webhook:
    post:
      tags:
        - Shopify
      operationId: shopifyWebhook
      summary: Shopify webhook callback
      description: |
        Inbound webhook from Shopify. Verified by Shopify HMAC — the
        standard `apiKey` / `accessToken` middleware does **not** run
        on this path, and JSON parsing is bypassed (raw body required
        for HMAC verification).

        Drives downstream order ingestion for the factory pipeline.
        See [`docs/data-flows.md#5-shopify-webhook--order`](../../data-flows.md).
      security: []
      requestBody:
        required: true
        description: |
          Raw Shopify webhook body. The handler binds
          `bodyParser.raw({ type: "application/json" })` so the
          request body is received as a `Buffer` (the raw bytes
          are required for HMAC verification before any parsing).
          Logical payload shape varies by `X-Shopify-Topic` (e.g.
          `orders/create`, `orders/updated`, `orders/cancelled`).
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      parameters:
        - in: header
          name: X-Shopify-Topic
          required: true
          schema:
            type: string
          description: Webhook topic (e.g. `orders/create`).
        - in: header
          name: X-Shopify-Hmac-Sha256
          required: true
          schema:
            type: string
          description: Base64 HMAC-SHA256 of the raw body, signed with the webhook secret.
        - in: header
          name: X-Shopify-Shop-Domain
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Accepted.
        '401':
          description: HMAC verification failed.
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: ApiKey
      description: |
        Bearer key registered for the `api` application. Same key
        used by the auth API — splitting is documentation-only.
    accessToken:
      type: apiKey
      in: header
      name: x-access-token
      description: |
        JWT for the end-user account the request is acting as.
        Required on routes documented as `Auth: required`. Some
        routes are documented as `Auth: optional` — the JWT, if
        present, scopes / personalises the response, but its
        absence is not an error.
    eyeTrackingToken:
      type: http
      scheme: bearer
      bearerFormat: EyeTrackingJWT
      description: |
        Per-headset JWT issued by the Beyond pairing flow. Only
        accepted on `/eye-tracking/*` routes — the standard
        `apiKey` / `accessToken` middleware does not run on these
        routes.
  responses:
    Unauthorized:
      description: Missing or invalid `Authorization` / `x-access-token`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
        code:
          type: string
          description: Optional machine-readable error code.
        details:
          description: Optional structured details.
      example:
        error: Forbidden
        code: Forbidden
    PublicAccountData:
      type: object
      description: |
        Public-facing snapshot of a Bigscreen account — the shape returned
        from `/info/account/{field}/{value}` for handle / session-id lookups.
        Strips email, identities, and any moderation-only metadata.
      required:
        - bigscreenAccountId
        - username
      properties:
        bigscreenAccountId:
          type: string
        username:
          type: string
        smallImageUrl:
          type: string
          format: uri
        largeImageUrl:
          type: string
          format: uri
        isVerified:
          type: boolean
        badges:
          type: array
          items:
            type: string
      example:
        bigscreenAccountId: acct_01HXJ4Q12345
        username: alice
        smallImageUrl: https://cdn.bigscreenvr.com/avatars/alice_sm.png
        largeImageUrl: https://cdn.bigscreenvr.com/avatars/alice_lg.png
        isVerified: true
        badges:
          - beyond-early-supporter
    CurrencyInfo:
      type: object
      description: |
        Currency payload returned by `/info/currency`. Driven by the caller's
        region (see [`RegionInfo`](./RegionInfo.yaml)) and used by the media
        pricing flows on the website.
      required:
        - currencyCode
      properties:
        currencyCode:
          type: string
          description: ISO 4217 currency code.
        currencySymbol:
          type: string
        region:
          type: string
          description: Region the currency was derived from.
        conversionRate:
          type: number
          format: float
          description: Conversion rate from the base currency (USD) to this currency, when available.
      example:
        currencyCode: USD
        currencySymbol: $
        region: US
        conversionRate: 1
    PublicScanRequest:
      type: object
      description: |
        Public-facing scan request — `PublicScanRequest` from
        [`api/src/fabricator/FabricatorSchemas.ts`](../../../../api/src/fabricator/FabricatorSchemas.ts).
        Returned by `/beyond/scan/{scanRequestId}`,
        `/beyond/scans/{scanRequestState}`, and the rescan endpoint. Strips
        admin / fabricator-only fields (`ownerBigscreenAccountId`,
        `topologyAuthToken`, `preprocessorResult`, `jobIds`, etc.) — see
        [`ScanRequest`](./ScanRequest.yaml) for the full admin shape.
      required:
        - id
        - bigOrderId
      properties:
        id:
          type: string
        createdAt:
          type: integer
          description: Unix milliseconds.
        bigOrderId:
          type: string
        scanRequestState:
          type: string
          description: '`ScanRequestState` — defaults to `Unknown`.'
        name:
          type: string
          description: Customer-facing name for the scan (e.g. `"Alice's left eye"`).
        shopifyIpd:
          type: number
          format: float
          description: IPD as supplied at order time (Shopify line-item).
        preprocessorIpd:
          type: number
          format: float
          description: IPD computed by the scan preprocessor.
        ipd:
          type: number
          format: float
          description: |
            Final IPD applied to the order. Sourced from `shopifyIpd`,
            `preprocessorIpd`, or a customer override depending on flow.
      example:
        id: scan_01HZW01234
        createdAt: 1761127240000
        bigOrderId: ord_01HXJ4Q98765
        scanRequestState: Pending
        name: Alice's scan
        shopifyIpd: 63.5
        preprocessorIpd: 63.4
        ipd: 63.5
    BigProductType:
      type: string
      description: |
        Product taxonomy. Maps to Shopify variants for sellable items and to internal
        fabricator data for manufactured cushions. Adding a value requires a
        corresponding metafield in Shopify (see internal docs).
      enum:
        - Unknown
        - BigscreenBeyondV1
        - BeyondCushionV1
        - AudioStrapV1
        - BeyondFibreOpticCableV1
        - BeyondLinkBoxV1
        - BeyondSoftStrapV1
        - ReplacementBigscreenBeyondV1
        - ReplacementBeyondCushionV1
        - ReplacementBeyondFibreOpticCableV1
        - ReplacementBeyondLinkBoxV1
        - ReplacementBeyondSoftStrapV1
        - BeyondCyberboxV1
        - IPDDiscoveryUnitV1
        - CustomCushion
        - PrescriptionLenses
        - PrescriptionLensInserts
        - AudioStrap
        - Beyond2
        - Beyond2Upgrade
        - Beyond2Eye
        - Beyond2EyeUpgrade
        - Beyond2EyeVRChat
        - UniversalHalo
        - CoverShell
        - GenericCushion
        - Merchandise
        - Beyond2IPDTool
        - BigscreenRacingGloves
        - LinkBooster
        - Other
    BigLineItem:
      type: object
      description: |
        A line item belonging to a `BigOrder`. Mirrors `FabricatorSchemas.BigLineItem` —
        one row per Shopify line item, enriched with shipping group + product type for
        fabricator routing.
      required:
        - id
        - ownerBigscreenAccountId
        - bigOrderId
        - shopifyOrderId
        - shopifyOrderLineItemId
        - type
      properties:
        id:
          type: string
        createdAt:
          type: string
          format: date-time
        ownerBigscreenAccountId:
          type: string
          description: Customer account that owns the line item.
        creatorBigscreenAccountId:
          type: string
          description: Account that created the row.
        bigOrderId:
          type: string
        shopifyOrderId:
          type: string
        shopifyOrderLineItemId:
          type: string
        shopifyOrderLineItemSnapshot:
          type: object
          additionalProperties: true
          description: Snapshot of the Shopify line-item payload at creation time.
        shopifyProductId:
          type: integer
          format: int64
        shopifySku:
          type: object
          additionalProperties: true
          description: Parsed Bigscreen SKU descriptor — `FabricatorSchemas.ShopifySku`.
        type:
          description: Product taxonomy.
          $ref: '#/components/schemas/BigProductType'
        version:
          type: string
          example: V3
        shipmentPackageType:
          type: string
          description: Packaging slot — `FabricatorSchemas.BigShipmentPackageType`.
          example: Default
        shipmentGroupId:
          type: string
          description: Groups items that ship together; empty for un-grouped items.
        metaData:
          type: object
          additionalProperties: true
    BigOrder:
      type: object
      description: |
        Bigscreen's first-party order — mirrors a Shopify order and is enriched with
        fabricator state (line items, shipping summary, checklist result, IPD,
        state-machine position). Mirrors `FabricatorSchemas.BigOrder`.

        Created by `POST /admin/shop/order` once a Shopify order is fulfillable, then
        advanced through actions on `PUT /admin/shop/order/{bigOrderId}`. Most fields
        on this object are populated by the checklist pipeline rather than the caller.
      required:
        - id
        - ownerBigscreenAccountId
        - creatorBigscreenAccountId
        - shopifyOrderId
        - shopifyCustomerId
        - state
      properties:
        id:
          type: string
          description: BigOrder id (UUID).
        createdAt:
          type: string
          format: date-time
        ownerBigscreenAccountId:
          type: string
          description: Customer (the buyer's Bigscreen account).
        creatorBigscreenAccountId:
          type: string
          description: Account that triggered creation (often the same as owner for self-checkout).
        shopifyOrderId:
          type: string
          description: Numeric Shopify order id.
        shopifyCustomerId:
          type: string
          description: Numeric Shopify customer id.
        shopifyOrderName:
          type: string
          description: Shopify's human-readable order name (e.g. `#1234`).
        shopifyOrderSnapshot:
          type: object
          additionalProperties: true
          description: Snapshot of the Shopify order payload at last sync. Shape per Shopify Admin API.
        shopifyOrderStatus:
          type: string
          description: Cached `ShopifyOrder.financial_status` / `fulfillment_status` summary.
        lineItems:
          type: array
          deprecated: true
          items:
            $ref: '#/components/schemas/BigLineItem'
          description: Deprecated — use `currentLineItems`.
        lineItemsV1:
          type: array
          deprecated: true
          items:
            $ref: '#/components/schemas/BigLineItem'
          description: Backup of the deprecated V1 line items.
        lineItemsV2:
          type: array
          deprecated: true
          items:
            $ref: '#/components/schemas/BigLineItem'
        currentLineItems:
          type: array
          items:
            $ref: '#/components/schemas/BigLineItem'
          description: The current (unfulfilled) line items on the order.
        lineItemSummary:
          type: object
          additionalProperties: true
          description: Snapshot of the line-item layout at the last checklist run, including fulfilled and refunded items.
        origin:
          type: string
          description: Origin of the order — `FabricatorSchemas.BigOrderOrigin` (e.g. `Customer`, `TestData`, `Unknown`).
        state:
          type: string
          description: State-machine position — `FabricatorSchemas.BigOrderState`. Immutable except via `applyAction`.
        nextAction:
          type: string
          description: |
            Next action the checklist computed. Values in `FabricatorSchemas.BigOrderAction`
            (e.g. `SendScanRequest`, `AdminOverrideIPD`, `CreateJob`, `CreateShipment`,
            `ShipIt`, `Complete`, `Unknown`).
        priority:
          type: integer
          description: Operator-set priority (clamped server-side to `[-1, 100]`).
        purpose:
          type: string
          description: Free-text purpose tag used in test orders.
        name:
          type: string
          description: Operator-supplied label.
        level:
          type: string
          example: ShopifyOrder
        version:
          type: string
          example: V3
        checklistResult:
          type: object
          additionalProperties: true
          description: Last checklist result stored on the order.
        ipd:
          type: integer
          description: Final IPD (-1 if unset).
        ipdSource:
          type: string
          description: '`FabricatorSchemas.BigOrderIpdSource` — where the IPD value came from (`Customer`, `Admin`, `Preprocessor`, `Unknown`).'
        history:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Append-only audit trail.
        tags:
          type: array
          items:
            type: string
        shipmentHold:
          description: If set, blocks shipment creation. `FabricatorSchemas.BigOrderShipmentHold`.
        shippingServiceLevel:
          type: string
          description: Selected service level (e.g. `Standard`, `Express`).
      example:
        id: 01HXJ4S3M0Q3TRBQ3JV9P5W2VC
        createdAt: '2026-04-08T17:42:11.000Z'
        ownerBigscreenAccountId: acct_01HXJ4Q12345
        creatorBigscreenAccountId: acct_01HXJ4Q12345
        shopifyOrderId: '6107853586649'
        shopifyCustomerId: '7421003628825'
        shopifyOrderName: '#10042'
        shopifyOrderStatus: paid
        origin: Customer
        state: AwaitingScanRequest
        nextAction: SendScanRequest
        priority: 0
        purpose: ''
        name: ''
        level: ShopifyOrder
        version: V3
        ipd: -1
        ipdSource: Unknown
        tags: []
        history: []
        shippingServiceLevel: Standard
        currentLineItems: []
    Channel:
      type: object
      description: |
        Public channel — `LegacySchemas.PublicChannel` from
        [`api/src/LegacySchemas.ts`](../../../../api/src/LegacySchemas.ts).
        Returned by `/channel/{channelId}` and embedded inside
        `ChannelGroup.channels`. The full admin `Channel` (with `streams`,
        `queries`, `geo`, `ageRestriction`, etc.) is **not** what this surface
        returns — admin-only fields are stripped.
      required:
        - id
        - name
      properties:
        id:
          type: string
          description: Channel UUID.
        name:
          type: string
          description: URL-friendly slug.
        description:
          type: string
        title:
          type: string
          description: Display title.
        roomSettings:
          type: object
          description: Free-form per-channel room settings the VR app applies on join.
          additionalProperties: true
        texture:
          type: object
          description: '`Texture` instance — image asset metadata for the channel tile.'
          additionalProperties: true
      example:
        id: 01HXKV5G6PQRS8T9UVWXYZABCD
        name: live-events
        title: Live Events
        description: Live concerts, sports, and special broadcasts.
        roomSettings: {}
        texture: {}
    ChannelGroup:
      type: object
      description: |
        Public channel group — the response shape returned by
        `getPublicChannelGroup` / `getPublicChannelGroups` in
        [`api/src/Channels.ts`](../../../../api/src/Channels.ts). Wraps a
        set of `Channel`s with display metadata. The admin `ChannelGroup`
        (with `channelIds`, `createdAt`, etc.) is the storage shape; this
        is the resolved public projection.
      required:
        - name
        - channels
      properties:
        name:
          type: string
          description: URL-friendly slug.
        title:
          type: string
          description: Display title.
        imageUrl:
          type: string
          format: uri
          description: Optional thumbnail for the group tile.
        isPublished:
          type: boolean
          description: Whether the group is exposed to non-staff clients.
        channels:
          type: array
          items:
            $ref: '#/components/schemas/Channel'
        textures:
          type: array
          description: Generated atlas `Texture` records associated with the group.
          items:
            type: object
            additionalProperties: true
      example:
        name: featured-this-week
        title: Featured This Week
        imageUrl: https://dev-internal-files.bigscreencloud.com/channel-groups/featured.png
        isPublished: true
        channels:
          - id: 01HXKV5G6PQRS8T9UVWXYZABCD
            name: live-events
            title: Live Events
            description: Live concerts, sports, and special broadcasts.
            roomSettings: {}
            texture: {}
        textures: []
    Topic:
      type: object
      description: |
        Topic / interest tag in the Bigscreen app. Mirrors `Schemas.Topic`
        (LegacySchemas.ts). Drives subscription discovery; `subscriptions` holds the
        set of subscribed accounts internally and is summarized publicly via
        `subscriptionsCount`.
      required:
        - id
        - name
        - title
        - status
      properties:
        id:
          type: string
          description: 12-character random id.
        createdAt:
          type: string
          format: date-time
        name:
          type: string
          description: URL-friendly slug, generated from the title at creation time.
        title:
          type: string
        description:
          type: string
        status:
          type: string
          description: '`TopicStatus` — typically `ACTIVATED` or `DEACTIVATED`.'
        subscriptions:
          type: object
          additionalProperties: true
          description: Internal map of subscriptions keyed by id.
        subscriptionsCount:
          type: integer
      example:
        id: tpc_abc12345xyz
        createdAt: '2026-04-15T11:09:31.000Z'
        name: live-events
        title: Live Events
        description: Concerts, sports, premieres.
        status: ACTIVATED
        subscriptions: {}
        subscriptionsCount: 142
    MediaCategory:
      type: object
      description: |
        A media category — a top-level grouping in the Bigscreen TV / store UI that
        contains one or more `ProductCollection`s. Mirrors `Schemas.ProductCategory`
        (LegacySchemas.ts). Drives the home-screen rails and the unity thumbnail
        textures rendered on top of them.
      required:
        - id
        - name
        - title
        - entitlementClass
      properties:
        id:
          type: string
        createdAt:
          type: string
          format: date-time
        name:
          type: string
          deprecated: true
          description: Internal name. Obsolete — use ids instead.
        urlName:
          type: string
          description: URL slug; auto-updated when `title` changes.
        title:
          type: string
          description: Visible category name.
        description:
          type: string
        productCollectionIds:
          type: array
          items:
            type: string
          description: Collections that belong to this category.
        entitlementClass:
          type: string
          description: '`EntitlementClass` value the category surfaces (e.g. `TWO_DAY_RENTAL`, `OWNERSHIP`).'
        unityTextureConfig:
          type: object
          properties:
            imageWidth:
              type: integer
              default: 173
            imageHeight:
              type: integer
              default: 256
            canvasWidth:
              type: integer
              default: 512
            canvasHeight:
              type: integer
              default: 512
          description: Image and canvas sizes used when generating the unity thumbnail texture.
        isScheduled:
          type: boolean
          description: Whether the category honors the per-collection scheduling windows.
        defaultCollectionIndex:
          type:
            - integer
            - 'null'
          description: Index into `productCollectionIds` to surface by default.
      example:
        id: 01HZD12345ABC678DEF90GHJKM
        createdAt: '2026-04-22T10:14:00.000Z'
        name: rentals
        urlName: rentals
        title: Rentals
        description: Movies and shows available to rent.
        productCollectionIds:
          - 01HZE12345ABC678DEF90GHJKM
        entitlementClass: TWO_DAY_RENTAL
        unityTextureConfig:
          imageWidth: 173
          imageHeight: 256
          canvasWidth: 512
          canvasHeight: 512
        isScheduled: true
        defaultCollectionIndex: 0
    ProductCollection:
      type: object
      description: |
        A scheduled collection of `MediaProduct`s — the second-level grouping under
        `MediaCategory`. Mirrors `Schemas.ProductCollection` (LegacySchemas.ts). Has
        an availability window so curators can stage holiday rails, premieres, etc.
        in advance.
      required:
        - id
        - name
        - mediaProductIds
      properties:
        id:
          type: string
        createdAt:
          type: string
          format: date-time
        name:
          type: string
          description: Internal Bigscreen name.
        startTimestamp:
          type:
            - string
            - 'null'
          format: date-time
          description: Availability window opens.
        endTimestamp:
          type:
            - string
            - 'null'
          format: date-time
          description: Availability window closes.
        mediaProductIds:
          type: array
          items:
            type: string
          description: Products in this collection, ordered.
        isPublished:
          type: boolean
          description: Whether the collection is currently surfaced.
        textures:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Generated atlas textures the unity client renders.
      example:
        id: 01HZE12345ABC678DEF90GHJKM
        createdAt: '2026-04-22T10:14:00.000Z'
        name: spring-2026
        startTimestamp: '2026-04-01T00:00:00.000Z'
        endTimestamp: '2026-06-30T23:59:59.000Z'
        mediaProductIds:
          - 01HXKV8QRSTU9VWXYZABCDEFGH
        isPublished: true
        textures: []
    MediaProduct:
      type: object
      description: |
        A purchasable bundle of one or more `MediaItem`s. Mirrors
        `Schemas.MediaProduct` (LegacySchemas.ts). Carries marketing copy, pricing
        integrations (Stripe / Oculus IAP), entitlement class, and synchronization
        interval for rooms.
      required:
        - id
        - bigMediaId
        - status
        - title
        - mediaItemIds
        - entitlementClass
      properties:
        id:
          type: string
          description: Internal UUID.
        createdAt:
          type: string
          format: date-time
        bigMediaId:
          type: string
          description: Public identifier — anyone can use this to request the product.
        status:
          type: string
          description: '`ProductStatus` value — typically `DRAFT`, `PUBLISHED`, or `RETIRED`.'
        title:
          type: string
          description: Display title (also used as the Stripe product name).
        description:
          type: string
        url:
          type: string
          description: Human-readable URL slug.
        mediaItemIds:
          type: array
          items:
            type: string
          description: Media items bundled in this product.
        startingMediaItemId:
          type:
            - string
            - 'null'
          description: For movie nights — the media item to play first.
        featuredMediaItemId:
          type:
            - string
            - 'null'
          description: For movie nights — the featured "main feature" item.
        duration:
          type: integer
          description: Combined duration of all media items, in milliseconds.
        loops:
          type: boolean
          description: Whether the media loops on playback.
        is3D:
          type: boolean
          description: Indicates the main feature is stereoscopic.
        marketingData:
          type: object
          additionalProperties: true
          description: |
            Per-locale marketing payload. Keyed by locale (e.g. `en-US`); values are
            `ProductMarketing` records (titles, descriptions, image / texture URLs).
        locationCodes:
          type: array
          items:
            type: string
        deviceCode:
          type: string
        stripeProductData:
          type: object
          additionalProperties: true
          description: Cached Stripe product object (https://stripe.com/docs/api/products/object).
        oculusProductData:
          type: object
          additionalProperties: true
          description: Cached Oculus IAP product info.
        entitlementClass:
          type: string
          description: Maps to what's shown on the purchase page (e.g. `TWO_DAY_RENTAL`, `OWNERSHIP`).
        synchronizationInterval:
          type: integer
          description: |
            Interval (minutes, 0–30) used to generate showing time slots. `0` means the
            sync timestamp is set when the first person joins the room.
        roomSettings:
          type: object
          additionalProperties: true
      example:
        id: 01HXKV8QRSTU9VWXYZABCDEFGH
        createdAt: '2026-04-15T11:09:31.000Z'
        bigMediaId: bbb-2008
        status: PUBLISHED
        title: Big Buck Bunny
        description: Open-source animated short.
        url: big-buck-bunny
        mediaItemIds:
          - 01HXKV7N8PQRSTU9VWXYZABCDE
        startingMediaItemId: null
        featuredMediaItemId: 01HXKV7N8PQRSTU9VWXYZABCDE
        duration: 596000
        loops: false
        is3D: false
        locationCodes:
          - us
          - de
          - fr
          - jp
          - ca
        deviceCode: Unrestricted
        entitlementClass: TWO_DAY_RENTAL
        synchronizationInterval: 15
    Entitlement:
      type: object
      description: |
        A media entitlement — entitles a Bigscreen account to consume a specific media
        product. Mirrors `Schemas.Entitlement` (api/src/Schemas.ts). Created when a
        purchase, IAP, coupon redemption, or admin grant succeeds; transitions through
        a status state machine (`IDLE` → `ACTIVATED` → expired) and may carry session
        history.
      required:
        - id
        - bigscreenAccountId
        - mediaProductId
        - bigMediaId
        - entitlementClass
        - status
      properties:
        id:
          type: string
        createdAt:
          type: string
          format: date-time
        bigscreenAccountId:
          type: string
          description: Owner — the customer the entitlement belongs to.
        ip:
          type: string
          description: Customer IP captured at order time.
        receiptId:
          type: string
          description: Internal receipt id minted at purchase confirmation.
        receiptSnapshot:
          type: object
          additionalProperties: true
          description: Snapshot of the receipt at the time of purchase (auditing).
        mediaProductId:
          type: string
          description: Internal media product id. Immutable.
        bigMediaId:
          type: string
          description: Public media product id. Immutable.
        productSnapshot:
          type: object
          additionalProperties: true
          description: Snapshot of the product at purchase time.
        entitlementClass:
          type: string
          description: |
            Governs what happens when first used. Common values include
            `TWO_DAY_RENTAL`, `OWNERSHIP`, `FREE`, `PROMOTIONAL`. See `EntitlementClass`.
        status:
          type: string
          description: |
            State-machine position. `IDLE` (not yet activated), `ACTIVATED`,
            `EXPIRED`, `REFUNDED`, etc.
        activationTimestamp:
          type:
            - string
            - 'null'
          format: date-time
          description: When the entitlement was first activated. Null while `IDLE`. Set once.
        expirationTimestamp:
          type:
            - string
            - 'null'
          format: date-time
          description: When the entitlement expires. Set when activated for time-limited classes.
        sessions:
          type: array
          items:
            type: object
            additionalProperties: true
            description: Per-session record (device id, started at, ended at, etc.).
        maxAllowedSessions:
          type: integer
          description: '-1 indicates unlimited.'
        deviceHistory:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Devices that have used this entitlement; empty if never used.
        checksum:
          type: string
          description: MD5 over `bigscreenAccountId` + `receiptId` + media. Internal integrity check.
      example:
        id: ent_01HZB12345ABC678DEF90GHJKM
        createdAt: '2026-04-22T10:14:00.000Z'
        bigscreenAccountId: acct_01HXJ4Q12345
        ip: 203.0.113.10
        receiptId: rcpt_01HZB67890XYZ123
        mediaProductId: 01HXKV8QRSTU9VWXYZABCDEFGH
        bigMediaId: bbb-2008
        entitlementClass: TWO_DAY_RENTAL
        status: ACTIVATED
        activationTimestamp: '2026-04-22T11:00:00.000Z'
        expirationTimestamp: '2026-04-24T11:00:00.000Z'
        sessions: []
        maxAllowedSessions: -1
        deviceHistory: []
        checksum: a3b2c1d4e5f60718
