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.

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).

security:
    - apiKey: []
      accessToken: []

paths:
    /info/account/{field}/{value}:
        $ref: "./public-modules/info.yaml#/~1info~1account~1{field}~1{value}"
    /info/username/{username}:
        $ref: "./public-modules/info.yaml#/~1info~1username~1{username}"
    /info/room/{roomId}/sanitize:
        $ref: "./public-modules/info.yaml#/~1info~1room~1{roomId}~1sanitize"
    /info/room/{roomId}/sentiment:
        $ref: "./public-modules/info.yaml#/~1info~1room~1{roomId}~1sentiment"
    /info/timestamp:
        $ref: "./public-modules/info.yaml#/~1info~1timestamp"
    /info/region:
        $ref: "./public-modules/info.yaml#/~1info~1region"
    /info/currency:
        $ref: "./public-modules/info.yaml#/~1info~1currency"

    /beyond/scans/{scanRequestState}:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1scans~1{scanRequestState}"
    /beyond/topology/{topologyToken}:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1topology~1{topologyToken}"
    /beyond/scan/{topologyToken}/process:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1scan~1{topologyToken}~1process"
    /beyond/multipass:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1multipass"
    /beyond/orders:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1orders"
    /beyond/order/{bigOrderId}:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1order~1{bigOrderId}"
    /beyond/order/{bigOrderId}/ipd:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1order~1{bigOrderId}~1ipd"
    /beyond/scan/{scanRequestId}:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1scan~1{scanRequestId}"
    /beyond/scan/{scanRequestId}/rescan:
        $ref: "./public-modules/beyond.yaml#/~1beyond~1scan~1{scanRequestId}~1rescan"

    /channel_groups:
        $ref: "./public-modules/channels.yaml#/~1channel_groups"
    /channel_group/{name}:
        $ref: "./public-modules/channels.yaml#/~1channel_group~1{name}"
    /channel/{channelId}:
        $ref: "./public-modules/channels.yaml#/~1channel~1{channelId}"

    /topic/{topicName}:
        $ref: "./public-modules/topics.yaml#/~1topic~1{topicName}"
    /topic/{topicName}/subscribe:
        $ref: "./public-modules/topics.yaml#/~1topic~1{topicName}~1subscribe"
    /topic/{topicName}/unsubscribe:
        $ref: "./public-modules/topics.yaml#/~1topic~1{topicName}~1unsubscribe"

    /event:
        $ref: "./public-modules/analytics.yaml#/~1event"

    /token/purchase/{bigMediaId}:
        $ref: "./public-modules/media.yaml#/~1token~1purchase~1{bigMediaId}"
    /media/category/{name}:
        $ref: "./public-modules/media.yaml#/~1media~1category~1{name}"
    /media/categories/{name}/{after}:
        $ref: "./public-modules/media.yaml#/~1media~1categories~1{name}~1{after}"
    /media/categories_as_events/{name}:
        $ref: "./public-modules/media.yaml#/~1media~1categories_as_events~1{name}"
    /media/product/{bigMediaId}:
        $ref: "./public-modules/media.yaml#/~1media~1product~1{bigMediaId}"
    /media/location:
        $ref: "./public-modules/media.yaml#/~1media~1location"
    /media/entitlements/{bigMediaId}:
        $ref: "./public-modules/media.yaml#/~1media~1entitlements~1{bigMediaId}"
    /media/checksum:
        $ref: "./public-modules/media.yaml#/~1media~1checksum"
    /media/check/{bigMediaId}:
        $ref: "./public-modules/media.yaml#/~1media~1check~1{bigMediaId}"
    /media/play/{bigMediaId}:
        $ref: "./public-modules/media.yaml#/~1media~1play~1{bigMediaId}"
    /media/start/{mediaRequest}:
        $ref: "./public-modules/media.yaml#/~1media~1start~1{mediaRequest}"
    /media/token/purchase/{bigMediaId}:
        $ref: "./public-modules/media.yaml#/~1media~1token~1purchase~1{bigMediaId}"
    /media/try-automatic-order:
        $ref: "./public-modules/media.yaml#/~1media~1try-automatic-order"
    /media/order:
        $ref: "./public-modules/media.yaml#/~1media~1order"
    /media/entitlement/{entitlementClass}:
        $ref: "./public-modules/media.yaml#/~1media~1entitlement~1{entitlementClass}"
    /media/entitlement/{entitlementId}/activate:
        $ref: "./public-modules/media.yaml#/~1media~1entitlement~1{entitlementId}~1activate"
    /media/entitlement/browser:
        $ref: "./public-modules/media.yaml#/~1media~1entitlement~1browser"
    /media/entitlement/features:
        $ref: "./public-modules/media.yaml#/~1media~1entitlement~1features"
    /media/oculus/order:
        $ref: "./public-modules/media.yaml#/~1media~1oculus~1order"
    /media/oculus/update:
        $ref: "./public-modules/media.yaml#/~1media~1oculus~1update"
    /media/receipts:
        $ref: "./public-modules/media.yaml#/~1media~1receipts"
    /media/info/{mediaRequest}:
        $ref: "./public-modules/media.yaml#/~1media~1info~1{mediaRequest}"
    /media/analytics/engagement/{videoPosition}:
        $ref: "./public-modules/media.yaml#/~1media~1analytics~1engagement~1{videoPosition}"

    /report/user:
        $ref: "./public-modules/reports.yaml#/~1report~1user"
    /report/room:
        $ref: "./public-modules/reports.yaml#/~1report~1room"

    /eye-tracking/model:
        $ref: "./public-modules/eye-tracking.yaml#/~1eye-tracking~1model"
    /eye-tracking/user-files:
        $ref: "./public-modules/eye-tracking.yaml#/~1eye-tracking~1user-files"
    /eye-tracking/user-id:
        $ref: "./public-modules/eye-tracking.yaml#/~1eye-tracking~1user-id"

    /shopify/webhook:
        $ref: "./public-modules/shopify.yaml#/~1shopify~1webhook"

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"
