openapi: 3.0.3
info:
  title: Trillboards Partner API
  version: 1.1.0
  description: |
    API for partner integrations with Trillboards digital advertising platform.

    Use this API to integrate ads into your vending machines, kiosks, digital signage,
    and other display devices. Partners receive competitive revenue share on all impressions.

    ## Authentication

    Most endpoints require API key authentication using Bearer token:
    ```
    Authorization: Bearer trb_partner_xxxxx
    ```

    API keys are generated during partner registration and should be stored securely.

    ## Rate Limits

    All endpoints return rate limit headers:
    - `X-RateLimit-Limit`: Maximum requests allowed per window
    - `X-RateLimit-Remaining`: Requests remaining in current window
    - `X-RateLimit-Reset`: Unix timestamp when window resets
    - `RateLimit-Policy`: IETF draft-7 format policy string

    Rate limits by endpoint type:
    | Endpoint Type | Limit | Window | Key |
    |--------------|-------|--------|-----|
    | Registration | 5 | 1 hour | IP |
    | Authenticated API | 1000 | 1 minute | API key |
    | Ad serving (`/device/:deviceId/ads`) | 1 | 30 seconds | deviceId |
    | Impressions | 500 | 1 minute | deviceId |
    | Programmatic events | 300 | 1 minute | deviceId |
    | Batch impressions | 100 | 1 minute | API key |
    | Heartbeat | 60 | 1 minute | deviceId |
    | Venue intelligence | 100 | 1 minute | API key |
    | Audience intelligence | 100 | 1 minute | API key |
    | Bulk device operations | 20 | 1 minute | API key |

    Static direct-ad responses from `/device/:deviceId/ads` include `Cache-Control: private, max-age=300` and an `ETag`; `If-None-Match` revalidation can return HTTP 304. Dynamic programmatic waterfall responses include per-request VAST URLs, request IDs, and correlators, so they are returned with `Cache-Control: no-store, no-cache, must-revalidate, private` and do not return 304 even when `If-None-Match` matches. The response body still includes `cache_until` for backwards compatibility with SDK versions that parsed it directly.

    Exceeding rate limits returns HTTP 429 with a `Retry-After` header.

    ## Quick Start

    **Option A: New Partner Registration**
    1. Register as a partner via `POST /register` (returns API key)
    2. Register your devices
    3. Load the `embed_url` in your device's WebView or use `/device/{id}/ads` in custom UI
    4. Impressions are tracked automatically

    **Option B: Existing Earner Upgrade**
    If you already have a Trillboards earner account:
    1. Call `POST https://api.trillboards.com/v2/earner/upgrade-to-partner` with your user JWT
    2. Receive your Partner API key (one-time display - save it!)
    3. Your existing screens remain visible alongside new API-registered screens

    ## SDK

    Install the official TypeScript SDK for type-safe integration:
    ```
    npm install @trillboards/ads-sdk
    ```

    The SDK provides 4 entry points:
    - `@trillboards/ads-sdk` — Core browser SDK with ad player, events, and caching
    - `@trillboards/ads-sdk/react` — React hooks and components (TrillboardsProvider, TrillboardsAdSlot)
    - `@trillboards/ads-sdk/react-native` — React Native WebView wrapper
    - `@trillboards/ads-sdk/server` — Node.js PartnerClient with audience, analytics, auctions, creatives

    Full documentation: https://trillboards.com/developers/partner-sdk

  contact:
    name: Trillboards Developer Support
    email: developers@trillboards.com
    url: https://trillboards.com/developers
  license:
    name: Proprietary
    url: https://trillboards.com/terms-of-service

servers:
  - url: https://api.trillboards.com/v1/partner
    description: Production API
  - url: http://localhost:4004/v1/partner
    description: Local development

tags:
  - name: Partners
    description: Partner account management
  - name: Devices
    description: Device registration and management
  - name: Ads
    description: Ad serving endpoints
  - name: Impressions
    description: Impression tracking
  - name: Analytics
    description: Analytics and reporting
  - name: CMS
    description: Content management for partner screens
  - name: Webhooks
    description: Webhook subscriptions for real-time event notifications
  - name: Dashboard
    description: Dashboard overview and screen-level analytics
  - name: Earnings
    description: Earnings, transactions, and payout management
  - name: Team
    description: Team member management for partner organizations
  - name: VAST Integration
    description: |
      Direct VAST integration endpoints for partners who want to fetch ads directly from Google Ad Manager.

      This is the recommended approach for enterprise partners with existing video players.
      Partners cache the VAST config, build URLs client-side, and report impressions in batch.

      **Architecture:** Trillboards provides config + tracking only. Partners fetch VAST directly from Google.
  - name: Batch Tracking
    description: |
      High-volume impression tracking with cryptographic proof generation.

      Designed for partners with 10,000+ screens who need efficient batch reporting.
      Each impression receives an Ed25519 signature for third-party verification.
  - name: Fleet Management
    description: |
      Fleet-wide device management for partners with large screen networks.

      Push commands, update settings, and view analytics across your entire fleet.
      Supports filtering by venue type, country, and specific screen IDs.
  - name: Stripe Connect
    description: |
      Stripe Connect integration for partner payouts.

      Create a Stripe Connect account, complete onboarding, and receive payouts
      directly to your bank account. Minimum payout is $5.00.
  - name: Connect Dashboard
    description: |
      SDK partner dashboard API for earnings, screen health, and analytics.

      Provides pre-aggregated data optimized for embedding in partner dashboards.
  - name: Venue Intelligence
    description: |
      Real-world venue intelligence from CTV audience sensing.

      Access foot traffic patterns, atmosphere data, nearby venues, and network-wide trends.
      Data is aggregated from on-device face detection, audio analysis, and environmental sensors.
  - name: Audience Intelligence
    description: |
      Advanced audience analytics including live data, heatmaps, lookalike screens, and predictions.

      Lookalike and prediction endpoints require usage-based billing (`requireBilling('data_api')`).
    x-beta: true
  - name: Audience Segments
    description: |
      Custom audience segment targeting for programmatic campaigns.

      Create segments based on demographics, venue type, geography, and audience behavior.
      Match segments to screens and generate targeted VAST tags.

      Requires usage-based billing (`requireBilling('data_api')`).
    x-beta: true
  - name: Intent Catalog
    description: |
      Intent-based pricing catalog for audience purchase intent signals.

      Browse intent categories and get real-time intent pricing for screens
      based on live audience signals (speech, emotion, behavior).
    x-beta: true
  - name: Sandbox
    description: |
      Testing tools for partner API integration.

      Create sandbox screens, generate test events, and validate webhook integrations
      without affecting production data. Sandbox data expires after 24 hours.
    x-beta: true
  - name: Usage
    description: API usage tracking, rate limit status, and usage dashboards
  - name: Events
    description: |
      Server-Sent Events (SSE) for real-time event streaming.

      Subscribe to live events from your screens including audience updates,
      device status changes, and impression notifications.
      Maximum 5 concurrent SSE connections per partner.
  - name: Network Config
    description: White-label network configuration for content preferences, waterfall settings, and branding
  - name: Billing
    description: |
      Usage-based billing and credit management.

      Trillboards uses a pay-per-use model with generous free tiers. No subscriptions required.
      When free tier limits are exceeded, endpoints return 402 Payment Required until billing
      is set up via Stripe. Credits can be purchased at volume discounts.
  - name: Agent Registration
    description: |
      Self-service registration for AI agents and automated systems.

      Agents can register without human intervention, receive an API key and sandbox screens,
      and begin making API calls immediately within free tier limits.

paths:
  /register:
    post:
      tags:
        - Partners
      summary: Register a new partner
      description: |
        Register as a new partner to receive an API key.
        Partners are auto-activated for instant onboarding.

        **Important:** The API key is only shown once in the response. Store it securely!

        **Two key flavors are minted by this endpoint, selected via `partner_type`:**

        - `partner_type` ∈ {`vending_machine`, `kiosk`, `digital_signage`,
          `retail_display`, `other`} → response `api_key` carries the legacy
          `trb_partner_*` prefix. The hash is stored on the partner row and
          authenticates the full Partner Ads API surface.
        - `partner_type='ctv_publisher'` → response `api_key` carries the
          `tb_ctv_*` prefix. The key is minted via the unified
          `platform_api_keys` table with `owner_type='ctv_publisher'` and
          scopes the bearer to the CTV Measurement SDK ingestion surface
          (`POST /v1/partner/device/{deviceId}/heartbeat` and related
          measurement endpoints). The partner row itself carries no on-row
          `api_key_hash` for this flavor; rotation/revocation flows go
          through the platform_api_keys path. See `ctv-measurement-api.yaml`
          for the full CTV publisher surface.
      operationId: registerPartner
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - slug
                - contact_email
              properties:
                name:
                  type: string
                  description: Company or partner name
                  example: Acme Vending Co
                slug:
                  type: string
                  description: URL-safe identifier (lowercase, no spaces)
                  example: acmevending
                contact_email:
                  type: string
                  format: email
                  example: developer@example.com
                contact_name:
                  type: string
                  example: Jane Developer
                partner_type:
                  type: string
                  enum: [vending_machine, kiosk, digital_signage, retail_display, ctv_publisher, other]
                  default: other
                  description: |
                    Selects which API-key flavor is minted in the response:
                    - Any value except `ctv_publisher` → response `api_key`
                      has the legacy `trb_partner_*` prefix (Partner Ads API).
                    - `ctv_publisher` → response `api_key` has the `tb_ctv_*`
                      prefix and authenticates the CTV Measurement SDK surface
                      via `platform_api_keys.owner_type='ctv_publisher'`.
                  example: ctv_publisher
                revenue_share_percent:
                  type: number
                  description: Partner's revenue share percentage (negotiated per partner)
                  minimum: 0
                  maximum: 100
                allowed_domains:
                  type: array
                  items:
                    type: string
                  description: Whitelist of allowed referrer domains
      responses:
        '201':
          description: Partner registered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Partner registered successfully. API key shown once - save it securely.
                  data:
                    type: object
                    properties:
                      partner_id:
                        type: string
                        example: 507f1f77bcf86cd799439011
                      slug:
                        type: string
                        example: acmevending
                      api_key:
                        type: string
                        description: |
                          Store this securely - only shown once! The prefix
                          depends on the request `partner_type`:
                          - `trb_partner_*` for the legacy partner-ad flavors
                            (default — Partner Ads API).
                          - `tb_ctv_*` when `partner_type='ctv_publisher'`
                            (CTV Measurement SDK ingestion surface).
                        example: trb_partner_a1b2c3d4e5f6g7h8i9j0
                      status:
                        type: string
                        enum: [pending, active]
                        example: active
                      revenue_share_percent:
                        type: number
                        example: 80
                      portal_access:
                        type: object
                        description: Optional portal onboarding details for the partner owner account
                        properties:
                          user_id:
                            type: string
                          email:
                            type: string
                            format: email
                          new_account:
                            type: boolean
                          portal_url:
                            type: string
                            example: "https://trillboards.com/earner"
                          portal_auth_url:
                            type: string
                            example: "https://trillboards.com/earner/auth"
                          claim_required:
                            type: boolean
                          claim_request_endpoint:
                            type: string
                            example: "/v1/user/claim/request"
                          claim_verify_endpoint:
                            type: string
                            example: "/v1/user/claim/verify"
                          claim_complete_endpoint:
                            type: string
                            example: "/v1/user/claim/complete"
                          message:
                            type: string
                            example: "An earner portal account has been created. Check your email to set your password."
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          description: Partner slug already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /info:
    get:
      tags:
        - Partners
      summary: Get partner info
      description: Retrieve information about the authenticated partner
      operationId: getPartnerInfo
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Partner info retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Partner'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /device:
    post:
      tags:
        - Devices
      summary: Register a device
      description: |
        Register a new device or update an existing one.

        If a device with the same `device_id` already exists, it will be updated.

        The response includes an `embed_url` that should be loaded in your device's WebView.
      operationId: registerDevice
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - device_id
              properties:
                device_id:
                  type: string
                  description: Your internal device identifier
                  example: machine-001
                device_type:
                  type: string
                  enum: [vending_machine, kiosk, tablet, display, other]
                  default: other
                name:
                  type: string
                  description: Friendly name for the device
                  example: Mall of America - Floor 2
                description:
                  type: string
                  description: Screen or venue description for enrichment
                photo_url:
                  type: string
                  format: uri
                  description: Photo URL for enrichment context
                photos:
                  type: array
                  description: Optional additional photos for enrichment
                  items:
                    type: object
                    properties:
                      url:
                        type: string
                        format: uri
                      type:
                        type: string
                        description: photo type (context, close, angle, other)
                display:
                  type: object
                  description: |
                    Screen display specification. `width`/`height` are pixel
                    resolution; `width_in`/`height_in` are physical inches.
                    For accurate floor pricing we need physical inches — pass
                    `width_in` + `height_in` if you've measured the screen,
                    OR `ppi` so the server can derive inches from the pixel
                    resolution. Pixel-only inputs (no `ppi`, no `*_in`) will
                    NOT be recorded as physical size; they're still used for
                    orientation detection and DSP slot dimensions.
                  properties:
                    width:
                      type: integer
                      description: Display resolution width in pixels (e.g. 1920)
                      example: 1920
                    height:
                      type: integer
                      description: Display resolution height in pixels (e.g. 1080)
                      example: 1080
                    orientation:
                      type: string
                      enum: [landscape, portrait]
                      default: landscape
                    ppi:
                      type: number
                      description: |
                        Pixels per inch. Required if you want physical screen
                        size derived from `width`/`height` (server computes
                        widthInches = width / ppi). Typical: 40 (large TV),
                        96 (desktop monitor), 264 (tablet).
                      example: 40
                    width_in:
                      type: number
                      description: |
                        Physical screen width in INCHES. Overrides ppi-based
                        derivation. Use this when you've measured the actual
                        screen (most accurate for pricing).
                      example: 47.6
                    height_in:
                      type: number
                      description: Physical screen height in INCHES. Pairs with `width_in`.
                      example: 26.8
                location:
                  type: object
                  properties:
                    lat:
                      type: number
                      format: double
                      example: 44.8537
                    lng:
                      type: number
                      format: double
                      example: -93.2428
                    address:
                      type: string
                      example: 60 E Broadway, Bloomington, MN
                    venue_type:
                      type: string
                      enum: [mall, airport, retail, restaurant, office, gym, hospital, school, other]
                    venue_name:
                      type: string
                      example: Mall of America
                    country:
                      type: string
                      example: US
                    state:
                      type: string
                      example: MN
                    city:
                      type: string
                      example: Bloomington
                    zip:
                      type: string
                      example: 55425
                    timezone:
                      type: string
                      example: America/Chicago
                venue:
                  type: object
                  description: Venue classification details (optional). Subcategory is free text and may be auto-enriched by AI.
                  properties:
                    category:
                      type: string
                      enum: [retail, restaurant, bar, cafe, hotel, office, gym, hospital, school, mall, airport, gas_station, convenience_store, laundromat, salon, auto_shop, other]
                      example: retail
                    subcategory:
                      type: string
                      description: Free text subcategory (may be auto-enriched by AI)
                    environment_type:
                      type: string
                      enum: [indoor, outdoor, semi_outdoor, transit]
                      example: indoor
                    placement_type:
                      type: string
                      enum: [digital_signage, point_of_sale, waiting_area, window_display, kiosk, other]
                      example: digital_signage
                    business_name:
                      type: string
                    context_notes:
                      type: string
                placement:
                  type: object
                  description: Physical placement details (optional)
                  properties:
                    mounting_type:
                      type: string
                      enum: [wall, ceiling, tabletop, window, stand, freestanding, other, unknown]
                      example: wall
                    orientation:
                      type: string
                      enum: [landscape, portrait]
                      example: landscape
                    viewing_distance_ft:
                      type: number
                      example: 8
                    height_from_floor_ft:
                      type: number
                      example: 5
                custom_metadata:
                  type: object
                  description: Any additional metadata for your records
      responses:
        '201':
          description: Device registered
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: Device registered
                  data:
                    type: object
                    properties:
                      device_id:
                        type: string
                        description: Our internal device ID
                      external_device_id:
                        type: string
                        description: Your device ID
                      fingerprint:
                        type: string
                        description: Device fingerprint for SDK
                      status:
                        type: string
                      screen_id:
                        type: string
                        description: Linked screen ID
                      embed_url:
                        type: string
                        description: Load this URL in your WebView
                      enrichment_status:
                        type: string
                        description: Enrichment completion state
              example:
                success: true
                message: "Device registered"
                data:
                  device_id: "6507a1b2c3d4e5f6g7h8i9j0"
                  external_device_id: "machine-001"
                  fingerprint: "P_a1b2c3d4e5f6g7h8"
                  screen_id: "6507a1b2c3d4e5f6g7h8aa11"
                  status: "active"
                  embed_url: "https://screen.trillboards.com?fp=P_a1b2c3d4e5f6g7h8"
                  enrichment_status: "pending"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /device/{deviceId}:
    get:
      tags:
        - Devices
      summary: Get device info
      operationId: getDevice
      security:
        - BearerAuth: []
      parameters:
        - name: deviceId
          in: path
          required: true
          description: Device ID, fingerprint, or external device ID
          schema:
            type: string
      responses:
        '200':
          description: Device info
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Device'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Devices
      summary: Update device metadata
      description: |
        Partially update device metadata. Only provided fields are updated.
        Changes propagate to the linked screen record.
        If location changes, venue enrichment is re-triggered.
      operationId: updateDevice
      security:
        - BearerAuth: []
      parameters:
        - name: deviceId
          in: path
          required: true
          description: Device ID, fingerprint, or external device ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Friendly name for the device
                external_device_id:
                  type: string
                  description: Partner-owned device ID used for idempotent lookup and reconciliation
                description:
                  type: string
                  description: Screen or venue description
                photo_url:
                  type: string
                  format: uri
                photos:
                  type: array
                  items:
                    type: object
                    properties:
                      url:
                        type: string
                        format: uri
                      type:
                        type: string
                display:
                  type: object
                  description: |
                    Screen display specification. `width`/`height` are pixels;
                    pass `width_in`/`height_in` (inches) or `ppi` to record
                    physical screen size for floor pricing. See POST /device
                    for full unit semantics.
                  properties:
                    width:
                      type: integer
                      description: Display resolution width in pixels
                    height:
                      type: integer
                      description: Display resolution height in pixels
                    orientation:
                      type: string
                      enum: [landscape, portrait]
                    ppi:
                      type: number
                      description: Pixels per inch (required to derive physical size from width/height)
                    width_in:
                      type: number
                      description: Physical screen width in inches (overrides ppi-based derivation)
                    height_in:
                      type: number
                      description: Physical screen height in inches (pairs with width_in)
                location:
                  type: object
                  properties:
                    lat:
                      type: number
                      format: double
                    lng:
                      type: number
                      format: double
                    address:
                      type: string
                    country:
                      type: string
                    state:
                      type: string
                    city:
                      type: string
                    zip:
                      type: string
                    timezone:
                      type: string
                venue:
                  type: object
                  properties:
                    category:
                      type: string
                      enum: [retail, restaurant, bar, cafe, hotel, office, gym, hospital, school, mall, airport, gas_station, convenience_store, laundromat, salon, auto_shop, other]
                    subcategory:
                      type: string
                    environment_type:
                      type: string
                      enum: [indoor, outdoor, semi_outdoor, transit]
                    placement_type:
                      type: string
                      enum: [digital_signage, point_of_sale, waiting_area, window_display, kiosk, other]
                    business_name:
                      type: string
                    context_notes:
                      type: string
                placement:
                  type: object
                  properties:
                    mounting_type:
                      type: string
                      enum: [wall, ceiling, tabletop, window, stand, freestanding, other, unknown]
                    orientation:
                      type: string
                      enum: [landscape, portrait]
                    viewing_distance_ft:
                      type: number
                    height_from_floor_ft:
                      type: number
                custom_metadata:
                  type: object
                  description: Any additional metadata for your records
      responses:
        '200':
          description: Device updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: Device updated
                  data:
                    $ref: '#/components/schemas/Device'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags:
        - Devices
      summary: Delete a device
      description: Soft-delete a device. It will no longer receive ads.
      operationId: deleteDevice
      security:
        - BearerAuth: []
      parameters:
        - name: deviceId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Device deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: Device deleted
        '404':
          $ref: '#/components/responses/NotFound'

  /devices:
    get:
      tags:
        - Devices
      summary: List all devices
      operationId: listDevices
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, active, offline, suspended]
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
            maximum: 500
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Device list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      devices:
                        type: array
                        items:
                          $ref: '#/components/schemas/DeviceSummary'
                      total:
                        type: integer
                      limit:
                        type: integer
                      offset:
                        type: integer

  /devices/batch:
    post:
      tags:
        - Devices
      summary: Bulk register devices
      description: |
        Register up to 100 devices in a single request.

        Each device must include `device_id`. Optional `defaults` are applied to every device.
      operationId: registerDevicesBatch
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - devices
              properties:
                devices:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    required:
                      - device_id
                    properties:
                      device_id:
                        type: string
                      name:
                        type: string
                      device_type:
                        type: string
                      display:
                        type: object
                      location:
                        type: object
                      venue:
                        type: object
                      placement:
                        type: object
                defaults:
                  type: object
                  description: Default fields applied to each device
      responses:
        '200':
          description: Batch processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  summary:
                    type: object
                    properties:
                      total:
                        type: integer
                      created:
                        type: integer
                      updated:
                        type: integer
                      failed:
                        type: integer
                  devices:
                    type: array
                    items:
                      type: object
                      properties:
                        device_id:
                          type: string
                        trillboard_id:
                          type: string
                        screen_id:
                          type: string
                        fingerprint:
                          type: string
                        status:
                          type: string
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        device_id:
                          type: string
                        error:
                          type: string
                        status:
                          type: string

  /device/{deviceId}/ads:
    get:
      tags:
        - Ads
      summary: Get ads for device
      description: |
        **SDK Endpoint** - Returns ads for the specified device.

        This endpoint is called by the Trillboards Lite SDK. It supports ETag caching
        for static direct-ad responses to minimize bandwidth usage.

        If header bidding is enabled for the screen, the response includes
        `header_bidding_settings` with dynamic VAST waterfall URLs for programmatic
        playback. Programmatic waterfall responses are intentionally not HTTP-cacheable
        because each response carries fresh request IDs, VAST URLs, and ad-server
        correlators.

        No authentication required - device is identified by fingerprint or external device ID.
      operationId: getAdsForDevice
      parameters:
        - name: deviceId
          in: path
          required: true
          description: Device fingerprint or external device ID
          schema:
            type: string
        - name: If-None-Match
          in: header
          description: ETag from previous static direct-ad response. Dynamic programmatic waterfall responses ignore matching ETags and return 200 with a fresh body.
          schema:
            type: string
        - name: slot_w
          in: query
          description: Actual render slot width in pixels (player truth)
          schema:
            type: integer
        - name: slot_h
          in: query
          description: Actual render slot height in pixels (player truth)
          schema:
            type: integer
        - name: orientation
          in: query
          description: Slot orientation (portrait or landscape)
          schema:
            type: string
            enum: [portrait, landscape]
        - name: muted
          in: query
          description: Whether player is muted (true/false or 1/0)
          schema:
            type: boolean
        - name: autoplay
          in: query
          description: Whether autoplay is allowed (true/false or 1/0)
          schema:
            type: boolean
        - name: ua
          in: query
          description: |
            Runtime user agent string from the WebView/browser. Used for CTV device
            detection when stored device metadata is missing. Enables correct dth
            (device type hint) in VAST tags — e.g., Fire TV Silk WebView UA triggers
            dth=4 (CTV) instead of defaulting to dth=2 (desktop).
          schema:
            type: string
            example: "Mozilla/5.0 (Linux; Android 9; AFTKA Build/PS7629) AppleWebKit/537.36 Silk/3.23"
        - name: ipd
          in: query
          description: |
            Inventory Partner Domain for Google Ad Manager VAST requests. This must be
            a single root domain with no scheme, path, query, or port, and it must match
            an `inventorypartnerdomain=<domain>` declaration in the relevant ads.txt or
            app-ads.txt inventory-sharing relationship. Only send this when instructed by
            Trillboards; the ad server may also append `ipd=trillboards.com` for scoped
            revenue experiments.
          schema:
            type: string
            pattern: '^[A-Za-z0-9.-]+$'
            example: trillboards.com
      responses:
        '200':
          description: Ads list
          headers:
            ETag:
              description: Use in If-None-Match for static direct-ad caching. Dynamic programmatic waterfall responses still emit an ETag for diagnostics but do not short-circuit to 304.
              schema:
                type: string
                example: '"abc123def456"'
            Cache-Control:
              description: Static direct-ad responses use `private, max-age=300`; dynamic programmatic waterfall responses use `no-store, no-cache, must-revalidate, private`.
              schema:
                type: string
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      ads:
                        type: array
                        items:
                          $ref: '#/components/schemas/Ad'
                      settings:
                        $ref: '#/components/schemas/AdSettings'
                      header_bidding_settings:
                        $ref: '#/components/schemas/HeaderBiddingSettings'
                      cache_until:
                        type: string
                        format: date-time
                      screen_id:
                        type: string
                      screen_orientation:
                        type: string
                        enum: [portrait, landscape]
                      screen_dimensions:
                        type: object
                        properties:
                          width:
                            type: integer
                          height:
                            type: integer
                          orientation:
                            type: string
                            enum: [portrait, landscape]
                      device:
                        type: object
                        properties:
                          fingerprint:
                            type: string
                          display:
                            type: object
              example:
                success: true
                data:
                  ads:
                    - id: "507f1f77bcf86cd799439011"
                      title: "Summer Sale 50% Off"
                      type: "image"
                      url: "https://cdn.trillboards.com/ads/summer-sale.jpg"
                      duration: 15
                      impression_url: "https://api.trillboards.com/v1/partner/impression?adid=507f1f77bcf86cd799439011&impid={impid}&did={did}&sid={sid}"
                      pixel_url: "https://api.trillboards.com/openrtb/v1/pixel?adid=507f1f77bcf86cd799439011&impid={impid}&deviceId={fp}"
                    - id: "507f1f77bcf86cd799439012"
                      title: "New Product Launch"
                      type: "video"
                      url: "https://cdn.trillboards.com/ads/product-launch.mp4"
                      duration: 30
                      impression_url: "https://api.trillboards.com/v1/partner/impression?adid=507f1f77bcf86cd799439012&impid={impid}&did={did}&sid={sid}"
                      pixel_url: "https://api.trillboards.com/openrtb/v1/pixel?adid=507f1f77bcf86cd799439012&impid={impid}&deviceId={fp}"
                  settings:
                    ad_interval: 300
                    image_duration: 15
                    sound_enabled: false
                    max_ads_per_hour: 20
                    overlay_button_text: "TAP TO CONTINUE"
                  header_bidding_settings:
                    enabled: true
                    vast_tag_url: "https://pubads.g.doubleclick.net/gampad/ads?...&correlator=123"
                    variant_name: "simple"
                    allocation: 1
                    provider: "floodgates"
                    request_mode: "always"
                    force_request: true
                    min_interval_seconds: 60
                    prebid: null
                  cache_until: "2026-01-01T03:00:00.000Z"
                  screen_id: "6507a1b2c3d4e5f6g7h8aa11"
                  screen_orientation: "landscape"
                  screen_dimensions:
                    width: 1920
                    height: 1080
                    orientation: "landscape"
                  device:
                    fingerprint: "P_a1b2c3d4e5f6"
                    display:
                      width: 1920
                      height: 1080
        '304':
          description: Not Modified for static direct-ad responses only. Dynamic programmatic waterfall responses always return 200 with a fresh body.
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /impression:
    get:
      tags:
        - Impressions
      summary: Record impression (pixel)
      description: |
        Record a single impression. Can be called as a tracking pixel (1x1 image load).

        Both GET and POST methods are supported for flexibility.
        Duplicate `impid` + `adid` submissions are ignored (idempotent).

        **Optional Signature Verification**
        Provide signature headers to verify impressions:
        ```
        X-Trillboards-API-Key: trb_partner_xxxxx
        X-Trillboards-Timestamp: <unix_seconds>
        X-Trillboards-Signature: sha256=<hmac>
        ```
        Signature payload:
        ```
        {timestamp}.{adid}.{impid}.{did}.{sid}.{aid}
        ```
        Signature = HMAC-SHA256(api_key, payload)
      operationId: recordImpressionGet
      parameters:
        - name: X-Trillboards-API-Key
          in: header
          description: Partner API key for optional signature verification
          schema:
            type: string
        - name: X-Trillboards-Timestamp
          in: header
          description: Unix timestamp for signature verification
          schema:
            type: integer
        - name: X-Trillboards-Signature
          in: header
          description: HMAC signature for optional verification
          schema:
            type: string
        - name: adid
          in: query
          required: true
          description: Advertisement ID
          schema:
            type: string
        - name: impid
          in: query
          required: true
          description: Unique impression ID
          schema:
            type: string
        - name: did
          in: query
          description: Device ID, fingerprint, or external device ID
          schema:
            type: string
        - name: sid
          in: query
          description: Screen ID (optional, for analytics linkage)
          schema:
            type: string
        - name: aid
          in: query
          description: Allocation ID
          schema:
            type: string
        - name: duration
          in: query
          description: View duration in seconds
          schema:
            type: integer
        - name: completed
          in: query
          description: Whether ad was fully viewed
          schema:
            type: boolean
        - name: display_mode
          in: query
          description: Display mode context (fullscreen, l-bar, pip)
          schema:
            type: string
            enum: [fullscreen, l-bar, pip]
        - name: display_area_ratio
          in: query
          description: Screen area ratio used by ad (0.0 to 1.0)
          schema:
            type: number
        - name: lbar_size
          in: query
          description: L-bar size percentage (0-100)
          schema:
            type: number
        - name: lbar_position
          in: query
          description: L-bar position
          schema:
            type: string
            enum: [bottom, right]
        - name: lbar_slot_index
          in: query
          description: L-bar slot index (0-based)
          schema:
            type: integer
        - name: lbar_slot_count
          in: query
          description: Total L-bar slots in rotation
          schema:
            type: integer
        - name: sig
          in: query
          description: HMAC signature (optional, for signed impressions)
          schema:
            type: string
        - name: sig_ts
          in: query
          description: Signature timestamp in unix seconds
          schema:
            type: integer
      responses:
        '200':
          description: Impression recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  duplicate:
                    type: boolean
    post:
      tags:
        - Impressions
      summary: Record impression (POST)
      operationId: recordImpressionPost
      parameters:
        - name: X-Trillboards-API-Key
          in: header
          description: Partner API key for optional signature verification
          schema:
            type: string
        - name: X-Trillboards-Timestamp
          in: header
          description: Unix timestamp for signature verification
          schema:
            type: integer
        - name: X-Trillboards-Signature
          in: header
          description: HMAC signature for optional verification
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - adid
                - impid
              properties:
                adid:
                  type: string
                impid:
                  type: string
                did:
                  type: string
                sid:
                  type: string
                aid:
                  type: string
                duration:
                  type: integer
                completed:
                  type: boolean
                display_mode:
                  type: string
                  enum: [fullscreen, l-bar, pip]
                display_area_ratio:
                  type: number
                lbar_size:
                  type: number
                lbar_position:
                  type: string
                  enum: [bottom, right]
                lbar_slot_index:
                  type: integer
                lbar_slot_count:
                  type: integer
                sig:
                  type: string
                sig_ts:
                  type: integer
      responses:
        '200':
          description: Impression recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  duplicate:
                    type: boolean

  /openrtb/v1/complete:
    post:
      tags:
        - Impressions
      summary: Record ad completion (custom players)
      description: |
        Report that an ad played to completion. Designed for **custom player integrations**
        that don't use the IMA SDK (which auto-fires VAST tracking URLs).

        This writes a `complete` event to the event tracking pipeline, which the daily
        earnings aggregation cron picks up to calculate revenue. Without this call,
        impressions are recorded but earnings won't be attributed.

        ### When to use this endpoint

        Use this if your integration:
        - Calls `/openrtb/v1/impression` and `/openrtb/v1/heartbeat` directly
        - Does NOT use the IMA SDK or Google DAI
        - Plays VAST ads to completion but doesn't fire the VAST `<Tracking event="complete">` URL

        ### Deduplication

        Events are deduplicated by `(impressionId, screenId)` within a 60-second window.
        If no `impressionId` is provided, one is generated automatically.
      operationId: recordAdComplete
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - screenId
                - adId
              properties:
                screenId:
                  type: string
                  description: Screen mongo ID
                  example: "69a076ad2ccbba67a5ba1c1e"
                adId:
                  type: string
                  description: Advertisement or creative ID (from VAST response)
                  example: "ima_24idno"
                impressionId:
                  type: string
                  description: Impression ID for correlation with `/impression` call. Auto-generated if omitted.
                  example: "imp_abc123"
                duration:
                  type: number
                  description: Ad duration in seconds
                  example: 30
                deviceId:
                  type: string
                  description: Device identifier (falls back to request IP if omitted)
      responses:
        '200':
          description: Completion tracked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Ad completion tracked successfully"
                  impressionId:
                    type: string
                    description: The impression ID used (provided or auto-generated)
                    example: "imp_abc123"
              examples:
                success:
                  value:
                    success: true
                    message: "Ad completion tracked successfully"
                    impressionId: "imp_abc123"
                deduplicated:
                  value:
                    success: true
                    message: "Complete event deduplicated"
                    givt:
                      flags: ["duplicate"]
        '400':
          description: Missing required parameters
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: "Missing required parameters: screenId and adId are required"
        '500':
          $ref: '#/components/responses/InternalError'

  /impressions/batch:
    post:
      tags:
        - Impressions
      summary: Batch record impressions
      description: |
        Record multiple impressions at once. Useful for syncing offline impressions.

        Maximum 100 impressions per request.
        Signed impressions can include `sig` and `sig_ts` per impression (see `/impression` signature format).
      operationId: recordImpressionsBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - impressions
              properties:
                impressions:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    required:
                      - adid
                      - impid
                    properties:
                      adid:
                        type: string
                      impid:
                        type: string
                      did:
                        type: string
                      sid:
                        type: string
                      aid:
                        type: string
                      duration:
                        type: integer
                      completed:
                        type: boolean
                      display_mode:
                        type: string
                        enum: [fullscreen, l-bar, pip]
                      display_area_ratio:
                        type: number
                      lbar_size:
                        type: number
                      lbar_position:
                        type: string
                        enum: [bottom, right]
                      lbar_slot_index:
                        type: integer
                      lbar_slot_count:
                        type: integer
                      sig:
                        type: string
                      sig_ts:
                        type: integer
                      timestamp:
                        type: string
                        format: date-time
                      device_fingerprint:
                        type: string
      responses:
        '200':
          description: Batch processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      recorded:
                        type: integer
                      failed:
                        type: integer
                      skipped:
                        type: integer
                      errors:
                        type: array
                        items:
                          type: object
                          properties:
                            impid:
                              type: string
                            error:
                              type: string

  /device/{deviceId}/heartbeat:
    post:
      tags:
        - Devices
      summary: Device heartbeat
      description: |
        Mark device as online. Called periodically by SDK.

        Supports structured telemetry payloads (`telemetry`, `sdk`, `device`, `network`, `ad`)
        and legacy flat heartbeat keys for backward compatibility.

        The request body accepts EITHER the legacy flat/nested telemetry shape
        (`PartnerHeartbeatRequest`) OR the new agent-core sensing payload
        (`PartnerDeviceHeartbeatBody`) — the latter is honest about which
        sensing fields actually ship in production (BLE, WiFi, mDNS, SSDP,
        HTTP probes, skip-reason counts) and explicitly excludes stub fields
        with 0% production population (Phase 6 UWB / Auracast / Channel
        Sounding, MDM enrollment, native sensors, CSI).
      operationId: heartbeat
      parameters:
        - name: deviceId
          in: path
          required: true
          description: Device fingerprint or external device ID
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/PartnerHeartbeatRequest'
                - $ref: '#/components/schemas/PartnerDeviceHeartbeatBody'
      responses:
        '200':
          description: Heartbeat recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerHeartbeatResponse'
    get:
      tags:
        - Devices
      summary: Read live device heartbeat state
      description: |
        Read-side companion to `POST /device/{deviceId}/heartbeat`. Returns
        the device twin (Redis-cached, PG-fallback) so ad-serving partners
        can check device health before issuing ad requests. Field-level
        access control strips MDM compliance, vertex embeddings, identity-
        correction internals, partner identifiers, and Mongo `_id`. Pure
        read; no side effects.

        Authenticated via the partner API key (`Authorization: Bearer
        trb_partner_xxx` or `tb_prt_xxx`). Rate limit: 1000 req/min per
        API key.
      operationId: getDeviceHeartbeat
      security:
        - BearerAuth: []
      parameters:
        - name: deviceId
          in: path
          required: true
          description: Device fingerprint or external device ID
          schema:
            type: string
      responses:
        '200':
          description: Live device heartbeat state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerHeartbeatQueryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Device does not belong to this partner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /device/{deviceId}/programmatic-event:
    post:
      tags:
        - Ads
      summary: Record programmatic ad event (Server-side Analytics)
      description: |
        **SDK Analytics Endpoint** - Records programmatic ad lifecycle events for
        Trillboards' internal analytics and triggers partner webhooks when subscribed.

        **Important**: This endpoint is for **server-side event recording only**.
        It does NOT push events to your native Android, iOS, or CTV applications.

        For receiving real-time events in native applications, use the
        **JavaScript Native Bridge Protocol** instead. The SDK automatically sends
        events to standard native interfaces (Android WebView, iOS WKWebView,
        React Native, Flutter, etc.).

        See documentation: https://api.trillboards.com/docs/integrations/partner-native-bridge.md

        ### Events Recorded:
        - `ad_started` - Ad began playing
        - `ad_ended` - Ad finished (complete or skipped)
        - `no_fill` - No programmatic ad available
        - `error` - Ad playback error
      operationId: recordProgrammaticEvent
      parameters:
        - name: deviceId
          in: path
          required: true
          description: Device fingerprint or external device ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - event
              properties:
                event:
                  type: string
                  enum: [ad_started, ad_ended, no_fill, error]
                ad_id:
                  type: string
                request_id:
                  type: string
                reason:
                  type: string
                error_code:
                  type: string
                error_message:
                  type: string
                variant_name:
                  type: string
                screen_orientation:
                  type: string
                  enum: [portrait, landscape]
                slot_width:
                  type: integer
                slot_height:
                  type: integer
      responses:
        '200':
          description: Event recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /analytics:
    get:
      tags:
        - Analytics
      summary: Get partner analytics
      operationId: getAnalytics
      security:
        - BearerAuth: []
      parameters:
        - name: start_date
          in: query
          schema:
            type: string
            format: date
        - name: end_date
          in: query
          schema:
            type: string
            format: date
      responses:
        '200':
          description: Analytics data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      partner:
                        type: object
                        properties:
                          total_devices:
                            type: integer
                          active_devices:
                            type: integer
                          total_impressions:
                            type: integer
                          total_revenue_cents:
                            type: integer
                          pending_payout_cents:
                            type: integer
                      devices_by_status:
                        type: array
                        items:
                          type: object
                      impressions_by_day:
                        type: array
                        items:
                          type: object
                          properties:
                            _id:
                              type: string
                              description: Date (YYYY-MM-DD)
                            impressions:
                              type: integer
                      period:
                        type: object
                        properties:
                          start:
                            type: string
                            format: date-time
                          end:
                            type: string
                            format: date-time

  /screens/resolve:
    get:
      tags:
        - CMS
      summary: Resolve screen ID by device identifier
      description: |
        Resolve a partner screen using a device identifier (fingerprint, device_id, or external_device_id).
        Use this when you only have device IDs but want to call screen-based CMS endpoints.
      operationId: resolvePartnerScreen
      security:
        - BearerAuth: []
      parameters:
        - name: device_id
          in: query
          description: Partner device ID or fingerprint
          schema:
            type: string
        - name: external_device_id
          in: query
          description: Partner external device ID (device_id from registration)
          schema:
            type: string
        - name: fingerprint
          in: query
          description: Device fingerprint (fp)
          schema:
            type: string
      responses:
        '200':
          description: Screen resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      screen_id:
                        type: string
                      device_id:
                        type: string
                      fingerprint:
                        type: string
                      external_device_id:
                        type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /devices/batch/orientation:
    post:
      tags:
        - Devices
      summary: Batch update screen orientation
      description: |
        Update `screen_orientation` for multiple devices/screens in one request.
        Accepts device identifiers (ObjectId, fingerprint, or partner_external_id).
      operationId: batchUpdateDeviceOrientation
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - device_ids
                - screen_orientation
              properties:
                device_ids:
                  type: array
                  minItems: 1
                  items:
                    type: string
                screen_orientation:
                  type: string
                  enum: [portrait, landscape, mixed, unknown]
      responses:
        '200':
          description: Batch update result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  total_devices:
                    type: integer
                  matched_devices:
                    type: integer
                  updated_screens:
                    type: integer
                  missing_device_ids:
                    type: array
                    items:
                      type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/{screenId}/default-streams:
    get:
      tags:
        - CMS
      summary: List default streams for a screen
      operationId: listPartnerDefaultStreams
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Default streams list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      streams:
                        type: array
                        items:
                          $ref: '#/components/schemas/PartnerDefaultStream'
                      current:
                        $ref: '#/components/schemas/PartnerDefaultStream'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags:
        - CMS
      summary: Create a default stream (URL-based)
      operationId: createPartnerDefaultStream
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerDefaultStreamCreate'
      responses:
        '201':
          description: Default stream created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PartnerDefaultStream'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/{screenId}/default-streams/upload:
    post:
      tags:
        - CMS
      summary: Upload a video default stream
      operationId: createPartnerDefaultStreamUpload
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - video
              properties:
                video:
                  type: string
                  format: binary
                name:
                  type: string
                description:
                  type: string
      responses:
        '201':
          description: Default stream created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PartnerDefaultStream'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/{screenId}/default-streams/images:
    post:
      tags:
        - CMS
      summary: Upload image carousel default stream
      operationId: createPartnerDefaultStreamImages
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - images
              properties:
                images:
                  type: array
                  items:
                    type: string
                    format: binary
                name:
                  type: string
                description:
                  type: string
                image_duration:
                  type: integer
                  description: Seconds per image
      responses:
        '201':
          description: Default stream created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PartnerDefaultStream'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/{screenId}/default-streams/{streamId}:
    put:
      tags:
        - CMS
      summary: Update a default stream
      operationId: updatePartnerDefaultStream
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
        - name: streamId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerDefaultStreamUpdate'
      responses:
        '200':
          description: Default stream updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PartnerDefaultStream'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
        - CMS
      summary: Delete a default stream
      operationId: deletePartnerDefaultStream
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
        - name: streamId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Default stream deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/{screenId}/default-streams/order:
    put:
      tags:
        - CMS
      summary: Reorder default streams
      operationId: reorderPartnerDefaultStreams
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - order
              properties:
                order:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Default streams reordered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/{screenId}/l-bar:
    get:
      tags:
        - CMS
      summary: Get L-Bar settings
      operationId: getPartnerLBarSettings
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: L-Bar settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PartnerLBarSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
        - CMS
      summary: Update L-Bar settings
      operationId: updatePartnerLBarSettings
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                display_preferences:
                  $ref: '#/components/schemas/PartnerLBarDisplayPreferences'
                l_bar_content:
                  $ref: '#/components/schemas/PartnerLBarContent'
      responses:
        '200':
          description: L-Bar settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PartnerLBarSettings'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/{screenId}/l-bar/live-updates:
    put:
      tags:
        - CMS
      summary: Update L-Bar live updates
      operationId: updatePartnerLBarLiveUpdates
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                queue_number:
                  type: string
                wait_time_minutes:
                  type: number
                promotion:
                  type: string
                custom_status:
                  type: string
      responses:
        '200':
          description: L-Bar live updates applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /ads/self-promo:
    get:
      tags:
        - CMS
      summary: List self-promo ads
      operationId: listPartnerSelfPromoAds
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Self-promo ads list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PartnerSelfPromoAd'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - CMS
      summary: Create self-promo ad (URL-based)
      operationId: createPartnerSelfPromoAd
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerSelfPromoCreate'
      responses:
        '201':
          description: Self-promo ad created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PartnerSelfPromoAd'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /ads/self-promo/upload:
    post:
      tags:
        - CMS
      summary: Create self-promo ad (upload)
      operationId: createPartnerSelfPromoUpload
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - media
                - name
              properties:
                media:
                  type: string
                  format: binary
                name:
                  type: string
                description:
                  type: string
                campaign_link:
                  type: string
                  format: uri
                screen_ids:
                  type: array
                  items:
                    type: string
      responses:
        '201':
          description: Self-promo ad created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PartnerSelfPromoAd'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /ads/self-promo/schedule:
    post:
      tags:
        - CMS
      summary: Schedule existing self-promo ad to screens
      operationId: schedulePartnerSelfPromo
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - ad_id
                - screen_ids
              properties:
                ad_id:
                  type: string
                screen_ids:
                  type: array
                  items:
                    type: string
                start_date:
                  type: string
                  format: date
                end_date:
                  type: string
                  format: date
      responses:
        '200':
          description: Self-promo scheduled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /ads/self-promo/{adId}/disable:
    post:
      tags:
        - CMS
      summary: Disable self-promo ad allocations
      operationId: disablePartnerSelfPromo
      security:
        - BearerAuth: []
      parameters:
        - name: adId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                screen_ids:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Self-promo disabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /ads/self-promo/placements:
    get:
      tags:
        - CMS
      summary: List self-promo placements by screen
      operationId: listPartnerSelfPromoPlacements
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Self-promo placements
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PartnerSelfPromoPlacement'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /webhooks:
    post:
      tags:
        - Webhooks
      summary: Create webhook subscription
      description: |
        Subscribe to webhook events. Webhooks deliver real-time notifications
        when events occur on your devices.

        **Available Events:**
        - `device.online` - Device comes online after being offline
        - `device.offline` - Device goes offline (no heartbeat for 5+ minutes)
        - `impression.recorded` - Ad impression recorded on a device
        - `campaign.allocated` - New campaign allocated to a device
        - `payout.processed` - Revenue payout processed
        - `programmatic.ad_started` - Programmatic ad started
        - `programmatic.ad_ended` - Programmatic ad ended
        - `programmatic.no_fill` - Programmatic no-fill response
        - `programmatic.error` - Programmatic error

        **Security:**
        - HTTPS required in production
        - HMAC-SHA256 signature verification
        - Secret shown only once in response - store it securely!

        **Signature Verification:**
        ```
        signature = HMAC-SHA256(webhook_secret, timestamp + "." + JSON.stringify(payload))
        ```
        Verify against `X-Trillboards-Signature` header (prefixed with `sha256=`).
      operationId: createWebhook
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
                - events
              properties:
                url:
                  type: string
                  format: uri
                  description: Webhook endpoint URL (HTTPS required in production)
                  example: https://example.com/webhooks/trillboards
                events:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum:
                      - device.online
                      - device.offline
                      - impression.recorded
                      - campaign.allocated
                      - payout.processed
                      - programmatic.ad_started
                      - programmatic.ad_ended
                      - programmatic.no_fill
                      - programmatic.error
                  description: Events to subscribe to
                  example: ["device.online", "device.offline", "impression.recorded"]
      responses:
        '201':
          description: Webhook created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      webhook_id:
                        type: string
                        example: wh_m4k7x9_a1b2c3d4e5f6
                      url:
                        type: string
                      events:
                        type: array
                        items:
                          type: string
                      secret:
                        type: string
                        description: Webhook signing secret (shown only once!)
                        example: whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
                      status:
                        type: string
                        enum: [active, inactive]
              example:
                success: true
                message: "Webhook created. Save the secret - it won't be shown again!"
                data:
                  webhook_id: "wh_m4k7x9_a1b2c3d4e5f6"
                  url: "https://example.com/webhooks/trillboards"
                  events: ["device.online", "device.offline"]
                  secret: "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
                  status: "active"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      tags:
        - Webhooks
      summary: List webhooks
      description: Get all webhook subscriptions for your account.
      operationId: listWebhooks
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Webhooks list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      webhooks:
                        type: array
                        items:
                          $ref: '#/components/schemas/Webhook'
              example:
                success: true
                data:
                  webhooks:
                    - webhook_id: "wh_m4k7x9_a1b2c3d4e5f6"
                      url: "https://example.com/webhooks/trillboards"
                      events: ["device.online", "device.offline"]
                      status: "active"
                      success_count: 42
                      failure_count: 2
                      last_triggered_at: "2026-01-01T02:30:00.000Z"
                      created_at: "2025-12-01T00:00:00.000Z"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /webhooks/{webhookId}:
    patch:
      tags:
        - Webhooks
      summary: Update webhook
      description: Update webhook URL, events, or status.
      operationId: updateWebhook
      security:
        - BearerAuth: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - device.online
                      - device.offline
                      - impression.recorded
                      - campaign.allocated
                      - payout.processed
                      - programmatic.ad_started
                      - programmatic.ad_ended
                      - programmatic.no_fill
                      - programmatic.error
                status:
                  type: string
                  enum: [active, inactive]
      responses:
        '200':
          description: Webhook updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/Webhook'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
        - Webhooks
      summary: Delete webhook
      description: Remove a webhook subscription.
      operationId: deleteWebhook
      security:
        - BearerAuth: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Webhook deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: Webhook deleted
        '404':
          $ref: '#/components/responses/NotFound'

  /webhooks/{webhookId}/test:
    post:
      tags:
        - Webhooks
      summary: Test webhook
      description: |
        Send a test event to verify your webhook endpoint is working.
        The test event uses the `test` event type with a sample payload.
      operationId: testWebhook
      security:
        - BearerAuth: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Test result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      status:
                        type: integer
                      response_time_ms:
                        type: integer
                      error:
                        type: string
              example:
                success: true
                message: "Test webhook sent successfully"
                data:
                  status: 200
                  response_time_ms: 142
                  error: null
        '404':
          $ref: '#/components/responses/NotFound'

  /webhooks/{webhookId}/deliveries:
    get:
      tags:
        - Webhooks
      summary: Get delivery history
      description: Get recent webhook delivery attempts for debugging.
      operationId: getWebhookDeliveries
      security:
        - BearerAuth: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
      responses:
        '200':
          description: Delivery history
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      deliveries:
                        type: array
                        items:
                          $ref: '#/components/schemas/WebhookDelivery'
              example:
                success: true
                data:
                  deliveries:
                    - id: "507f1f77bcf86cd799439011"
                      event_type: "device.online"
                      status: "success"
                      http_status: 200
                      response_time_ms: 142
                      attempted_at: "2026-01-01T02:30:00.000Z"
                      delivered_at: "2026-01-01T02:30:00.142Z"
                    - id: "507f1f77bcf86cd799439012"
                      event_type: "impression.recorded"
                      status: "failed"
                      http_status: 500
                      response_time_ms: 5012
                      error_message: "Connection timeout"
                      attempted_at: "2026-01-01T02:25:00.000Z"
                      retry_count: 2
        '404':
          $ref: '#/components/responses/NotFound'

  # ==========================================
  # CONTENT PREFERENCES
  # ==========================================

  /content-presets:
    get:
      tags:
        - CMS
      summary: List content presets
      description: |
        List all available content preference presets.
        Presets provide pre-configured content filtering rules for common venue types.
      operationId: listContentPresets
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Content presets retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ContentPreset'
                  has_more:
                    type: boolean
                  total_count:
                    type: integer
                  url:
                    type: string
              example:
                object: list
                data:
                  - id: "family_friendly"
                    name: "Family Friendly"
                    description: "Safe content for family environments"
                    blocked_categories: ["alcohol", "gambling", "adult"]
                    blocked_advertisers: []
                    min_ad_rating: "G"
                  - id: "bar_nightclub"
                    name: "Bar & Nightclub"
                    description: "Content suitable for adult venues"
                    blocked_categories: []
                    blocked_advertisers: []
                    min_ad_rating: "PG-13"
                has_more: false
                total_count: 5
                url: "/v1/partner/content-presets"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /content-presets/{presetId}:
    get:
      tags:
        - CMS
      summary: Get content preset
      description: Get details of a specific content preset.
      operationId: getContentPreset
      security:
        - BearerAuth: []
      parameters:
        - name: presetId
          in: path
          required: true
          description: Preset ID (e.g., "family_friendly", "bar_nightclub")
          schema:
            type: string
      responses:
        '200':
          description: Preset retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentPreset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Preset not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /screens/{screenId}/content-preferences:
    get:
      tags:
        - CMS
      summary: Get screen content preferences
      description: |
        Get content filtering preferences for a specific screen.
        Controls what types of ads can be displayed.
      operationId: getContentPreferences
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Content preferences retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentPreferencesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags:
        - CMS
      summary: Update content preferences (full)
      description: |
        Full update of content filtering preferences for a screen.
        Replaces all existing preferences with the provided values.
        Triggers real-time update to the screen via WebSocket.
      operationId: updateContentPreferences
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContentPreferencesInput'
      responses:
        '200':
          description: Preferences updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentPreferencesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags:
        - CMS
      summary: Update content preferences (partial)
      description: |
        Partial update of content preferences. Only updates provided fields.
        Triggers real-time update to the screen via WebSocket.
      operationId: patchContentPreferences
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContentPreferencesInput'
      responses:
        '200':
          description: Preferences updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentPreferencesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/{screenId}/apply-preset:
    post:
      tags:
        - CMS
      summary: Apply content preset to screen
      description: |
        Apply a predefined content preset to a screen.
        This overwrites existing content preferences with the preset values.
      operationId: applyContentPreset
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - preset_id
              properties:
                preset_id:
                  type: string
                  description: ID of the preset to apply
            example:
              preset_id: "family_friendly"
      responses:
        '200':
          description: Preset applied
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ContentPreferencesResponse'
                  - type: object
                    properties:
                      preset_applied:
                        type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/batch/content-preferences:
    post:
      tags:
        - CMS
      summary: Batch update content preferences
      description: |
        Update content preferences for multiple screens at once.
        Can apply custom preferences or a preset to up to 100 screens.
      operationId: batchUpdateContentPreferences
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - screen_ids
              properties:
                screen_ids:
                  type: array
                  items:
                    type: string
                  maxItems: 100
                  description: Array of screen IDs to update
                preferences:
                  $ref: '#/components/schemas/ContentPreferencesInput'
                preset_id:
                  type: string
                  description: Preset ID to apply (alternative to preferences)
            examples:
              with_preferences:
                summary: Custom preferences
                value:
                  screen_ids: ["screen1", "screen2", "screen3"]
                  preferences:
                    blocked_categories: ["alcohol", "gambling"]
              with_preset:
                summary: Apply preset
                value:
                  screen_ids: ["screen1", "screen2", "screen3"]
                  preset_id: "family_friendly"
      responses:
        '200':
          description: Batch update completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ==========================================
  # DISPLAY SETTINGS
  # ==========================================

  /screens/{screenId}/display-settings:
    get:
      tags:
        - CMS
      summary: Get display settings
      description: |
        Get display configuration for a screen including orientation,
        ad surfaces, and playback policy.
      operationId: getDisplaySettings
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Display settings retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisplaySettingsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags:
        - CMS
      summary: Update display settings (full)
      description: |
        Full update of display settings for a screen.
        Triggers real-time update and screen refresh via WebSocket.
      operationId: updateDisplaySettings
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DisplaySettingsInput'
      responses:
        '200':
          description: Settings updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisplaySettingsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags:
        - CMS
      summary: Update display settings (partial)
      description: Partial update of display settings.
      operationId: patchDisplaySettings
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DisplaySettingsInput'
      responses:
        '200':
          description: Settings updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisplaySettingsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/batch/display-settings:
    post:
      tags:
        - CMS
      summary: Batch update display settings
      description: Update display settings for multiple screens (up to 100).
      operationId: batchUpdateDisplaySettings
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - screen_ids
                - settings
              properties:
                screen_ids:
                  type: array
                  items:
                    type: string
                  maxItems: 100
                settings:
                  $ref: '#/components/schemas/DisplaySettingsInput'
      responses:
        '200':
          description: Batch update completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ==========================================
  # PROGRAMMATIC SETTINGS
  # ==========================================

  /screens/{screenId}/programmatic-settings:
    get:
      tags:
        - CMS
      summary: Get programmatic settings
      description: |
        Get programmatic/header bidding settings for a screen.
        Controls Google IMA, Prebid, and other programmatic ad sources.
      operationId: getProgrammaticSettings
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Programmatic settings retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProgrammaticSettingsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags:
        - CMS
      summary: Update programmatic settings (full)
      description: Full update of programmatic settings.
      operationId: updateProgrammaticSettings
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProgrammaticSettingsInput'
      responses:
        '200':
          description: Settings updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProgrammaticSettingsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags:
        - CMS
      summary: Update programmatic settings (partial)
      description: Partial update of programmatic settings.
      operationId: patchProgrammaticSettings
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProgrammaticSettingsInput'
      responses:
        '200':
          description: Settings updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProgrammaticSettingsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /screens/batch/programmatic-settings:
    post:
      tags:
        - CMS
      summary: Batch update programmatic settings
      description: Update programmatic settings for multiple screens (up to 100).
      operationId: batchUpdateProgrammaticSettings
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - screen_ids
                - settings
              properties:
                screen_ids:
                  type: array
                  items:
                    type: string
                  maxItems: 100
                settings:
                  $ref: '#/components/schemas/ProgrammaticSettingsInput'
      responses:
        '200':
          description: Batch update completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ==========================================
  # AUDIENCE DATA EXPORT
  # ==========================================

  /screens/{screenId}/audience-metrics:
    get:
      tags:
        - Analytics
      summary: Get real-time audience metrics
      description: |
        Get real-time audience metrics for a screen including face detection,
        attention tracking, and demographic signals from the Troveworks AI pipeline.
      operationId: getAudienceMetrics
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
        - name: period
          in: query
          description: Time period for metrics
          schema:
            type: string
            enum: [1h, 6h, 24h]
            default: 1h
        - name: include_history
          in: query
          description: Include historical data points
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Audience metrics retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudienceMetricsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /audience-export:
    get:
      tags:
        - Analytics
      summary: Export audience data
      description: |
        Export aggregated audience data for multiple screens.
        Supports JSON and CSV formats.
      operationId: exportAudienceData
      security:
        - BearerAuth: []
      parameters:
        - name: screen_ids
          in: query
          required: true
          description: Comma-separated screen IDs (max 50)
          schema:
            type: string
        - name: start_date
          in: query
          description: Start date (ISO 8601)
          schema:
            type: string
            format: date
        - name: end_date
          in: query
          description: End date (ISO 8601)
          schema:
            type: string
            format: date
        - name: format
          in: query
          description: Export format
          schema:
            type: string
            enum: [json, csv]
            default: json
        - name: aggregation
          in: query
          description: Data aggregation level
          schema:
            type: string
            enum: [hourly, daily, weekly]
            default: daily
      responses:
        '200':
          description: Audience data exported
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudienceExportResponse'
            text/csv:
              schema:
                type: string
                description: CSV formatted data
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /screens/{screenId}/demographics:
    get:
      tags:
        - Analytics
      summary: Get screen demographics
      description: |
        Get demographic summary for a screen including age distribution,
        gender split, income profile, and lifestyle segments.
      operationId: getScreenDemographics
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
        - name: period
          in: query
          description: Time period
          schema:
            type: string
            enum: [24h, 7d, 30d]
            default: 7d
      responses:
        '200':
          description: Demographics retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DemographicsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /audience-analytics:
    get:
      tags:
        - Analytics
      summary: Get partner-wide audience analytics
      description: |
        Get aggregated audience analytics across all partner screens.
        Provides overview of impressions, active screens, and audience metrics.
      operationId: getAudienceAnalytics
      security:
        - BearerAuth: []
      parameters:
        - name: period
          in: query
          description: Time period
          schema:
            type: string
            enum: [24h, 7d, 30d]
            default: 7d
      responses:
        '200':
          description: Analytics retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudienceAnalyticsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ==========================================
  # DATA EXPORT API
  # ==========================================

  /data-export/summary:
    get:
      tags:
        - Analytics
      summary: List available datasets and per-dataset record counts
      description: |
        Return per-dataset record counts and date ranges for all datasets exposed
        by `GET /data-export/{dataset}`. Restricted to screens owned by the
        authenticated partner. K-anonymity ≥5 is enforced on every downstream
        export.
      operationId: getDataExportSummary
      security:
        - BearerAuth: []
      parameters:
        - name: days
          in: query
          description: Lookback in days (max 90)
          schema:
            type: integer
            minimum: 1
            maximum: 90
            default: 7
      responses:
        '200':
          description: Per-dataset summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataExportSummaryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /data-export/{dataset}:
    get:
      tags:
        - Analytics
      summary: Export a partner-scoped dataset
      description: |
        Export a partner-scoped dataset for the authenticated partner's screens.
        K-anonymity ≥5 is enforced at the SQL HAVING level for every dataset; partner
        screen scoping is enforced via `pgRepo.getEarnerScreensByPartnerMongoIds`,
        so a partner can never read another partner's rows.

        Available datasets:
        - `audience_metrics` — hourly audience aggregates (face count, attention, demographics, mood, weather)
        - `attention_ledger` — per-impression Verified Attention Seconds (VAS) measurements
        - `impression_ledger` — settled completed impressions (vast_event / proof_of_play / partner_sdk)
        - `waterfall_events` — VAST waterfall request outcomes per ad opportunity
        - `intent_signals` — purchase / consideration / awareness signals
        - `audience_loyalty_features` — per-device cross-day loyalty features keyed by partner-scoped pseudonymous IDs
          (`tb_<sha256(canonical_device_key || partner_salt).slice(0, 16)>`). Same physical device produces a
          DIFFERENT ID for a different partner — partner-specific salt prevents cross-partner identity
          correlation. Gated by `AUDIENCE_LOYALTY_FEATURES_ENABLED` SSM env. Soak window is 14 days
          post-PR3 cutover so cross-day matching only operates on rows ingested after the pepper-cutover.
      operationId: exportDataset
      x-trillboards-feature-flag: AUDIENCE_LOYALTY_FEATURES_ENABLED
      security:
        - BearerAuth: []
      parameters:
        - name: dataset
          in: path
          required: true
          description: Dataset name
          schema:
            type: string
            enum:
              - audience_metrics
              - attention_ledger
              - impression_ledger
              - waterfall_events
              - intent_signals
              - audience_loyalty_features
        - name: days
          in: query
          description: Lookback in days (max 90)
          schema:
            type: integer
            minimum: 1
            maximum: 90
            default: 7
        - name: limit
          in: query
          description: Max rows to return (hard cap 50000)
          schema:
            type: integer
            minimum: 1
            maximum: 50000
            default: 10000
        - name: offset
          in: query
          description: Pagination offset
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Dataset exported
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      rows:
                        type: array
                        description: |
                          Row shape depends on the requested `dataset`. For
                          `audience_loyalty_features` see `AudienceLoyaltyFeaturesRow`.
                          For other datasets the row shape is documented per-dataset
                          in the Trillboards data dictionary.
                        items:
                          oneOf:
                            - $ref: '#/components/schemas/AudienceLoyaltyFeaturesRow'
                            - type: object
                              additionalProperties: true
                      total:
                        type: integer
                      truncated:
                        type: boolean
                      lookback_days:
                        type: integer
                      k_anonymity_threshold:
                        type: integer
        '400':
          description: Unknown dataset (response includes the list of available datasets in `available`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /data/geohash-audience-panel:
    get:
      tags:
        - Analytics
      summary: Geographic audience-density panel (data-licensing)
      description: |
        Geographic audience-density panel keyed on `(geohash6, hour, venue_category, income_tier)` —
        the licensable buyer-facing data product mirroring what Foursquare / Placer / SafeGraph
        sell to Viant / LiveRamp / T-Vision.

        Source-of-record: the ClickHouse `AggregatingMergeTree` target
        `trillboards.geohash_audience_density_hourly`, populated by two materialized views
        (device-density from `signal_observations` and income-tier breakdown from `audience_metrics`),
        both joined to `screens_live` with a 180-day TTL.

        Privacy enforcement (un-bypassable from the application layer):

        - **K-anonymity ≥ 5** at SQL `HAVING` (or per-partner override via `partners.k_anon_floor`).
          Rows with fewer than the k-anon floor unique devices are suppressed before noise is applied.
        - **Deterministic Laplace differential-privacy noise** is applied per row, per numeric column,
          seeded by `partner_id || date || row_key`. Re-running the same query returns the same noise
          values — preventing the epsilon-laundering attack where a partner averages repeat queries to
          recover the true aggregate.
        - **Daily epsilon budget** per partner via the `partner_dp_budget(partner_id, date, epsilon_spent,
          epsilon_max)` PG table. Once the daily budget is exhausted, the export returns an empty
          `rows: []` with `budget_exhausted: true` — no further rows ship that day for that partner.

        Default date is yesterday (UTC) because the hourly panel is finalized at hour-grain;
        today-so-far is partial.
      operationId: getGeohashAudiencePanel
      security:
        - BearerAuth: []
      parameters:
        - name: date
          in: query
          description: Day to export (UTC). Defaults to yesterday.
          schema:
            type: string
            format: date
            example: "2026-05-11"
        - name: geohash_prefix
          in: query
          description: |
            Optional geohash6 prefix filter (base32 alphabet `[0-9a-hjkmnp-z]`, 1–6 chars).
            Matches rows whose `geohash6` starts with the prefix.
          schema:
            type: string
            pattern: '^[0-9a-hjkmnp-z]{1,6}$'
            example: "9q8yy"
        - name: venue_category
          in: query
          description: |
            Optional venue-category filter sourced from `earner_screens.venue_parent` /
            `earner_screens.venue_category`. Alphanumeric token, max 64 chars.
          schema:
            type: string
            pattern: '^[a-zA-Z][a-zA-Z0-9_-]{0,63}$'
            example: "retail"
        - name: epsilon
          in: query
          description: |
            DP epsilon to spend on this query. Clamped to `[0.01, partner_dp_budget.epsilon_max]`.
            Larger epsilon = less noise per row but faster budget consumption. Defaults to 0.1.
          schema:
            type: number
            format: float
            minimum: 0.01
            maximum: 100
            default: 0.1
      responses:
        '200':
          description: |
            Per-row noised aggregates plus the partner's k-anon threshold, epsilon spent on this
            call, and the daily DP budget snapshot. `rows` is empty + `budget_exhausted: true`
            when the day's epsilon budget cannot accommodate this request.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    required:
                      - rows
                      - date
                      - partner_id
                      - k_anonymity_threshold
                      - epsilon_used
                      - partner_dp_budget
                    properties:
                      rows:
                        type: array
                        description: Noised per-row aggregates (k-anon-filtered + Laplace-perturbed).
                        items:
                          type: object
                          required:
                            - hour
                            - geohash6
                            - venue_category
                            - income_tier
                            - unique_devices
                            - observation_count
                            - avg_dwell_seconds
                            - avg_face_count
                            - avg_attention
                            - screen_count
                          properties:
                            hour:
                              type: string
                              description: Hour bucket as the integer hour (0..23) cast to string.
                              example: "14"
                            geohash6:
                              type: string
                              description: 6-character geohash bucket.
                              example: "9q8yyk"
                            venue_category:
                              type: string
                              description: Venue category from `screens_live.venue_category`.
                              example: "retail"
                            income_tier:
                              type: string
                              description: Income tier bucket (e.g. `low`, `mid`, `high`).
                              example: "mid"
                            unique_devices:
                              type: integer
                              minimum: 0
                              description: Noised unique-device count (Laplace, sensitivity 1).
                              example: 27
                            observation_count:
                              type: integer
                              minimum: 0
                              description: Noised observation count (Laplace, sensitivity 1).
                              example: 412
                            avg_dwell_seconds:
                              type: number
                              format: float
                              minimum: 0
                              description: Noised average dwell seconds (Laplace, sensitivity 5).
                              example: 7.42
                            avg_face_count:
                              type: number
                              format: float
                              minimum: 0
                              description: Noised average face count per observation (Laplace, sensitivity 1).
                              example: 1.18
                            avg_attention:
                              type: number
                              format: float
                              minimum: 0
                              maximum: 1
                              description: Noised average attention score (Laplace, sensitivity 0.1).
                              example: 0.612
                            screen_count:
                              type: integer
                              minimum: 0
                              description: Distinct screen count contributing to this bucket.
                              example: 3
                      date:
                        type: string
                        format: date
                        description: Day exported (UTC).
                        example: "2026-05-11"
                      partner_id:
                        type: string
                        format: uuid
                        description: UUID partner_id (from `partners.partner_id`, migration 030).
                        example: "8e4d2f9a-1c0b-4e6d-9f87-2b1c4a5e7d9f"
                      k_anonymity_threshold:
                        type: integer
                        minimum: 5
                        description: |
                          K-anonymity floor applied at SQL `HAVING`. Default 5; override via
                          `partners.k_anon_floor`.
                        example: 5
                      epsilon_used:
                        type: number
                        format: float
                        minimum: 0
                        description: |
                          Epsilon spent on this call (0 if `budget_exhausted` or CH unavailable).
                          Deducted from `partner_dp_budget.epsilon_spent` on success.
                        example: 0.1
                      partner_dp_budget:
                        type: object
                        required:
                          - epsilon_max
                          - epsilon_spent_before
                        properties:
                          epsilon_max:
                            type: number
                            format: float
                            description: Maximum epsilon allowed per day for this partner.
                            example: 1.0
                          epsilon_spent_before:
                            type: number
                            format: float
                            description: Epsilon already spent today before this call.
                            example: 0.3
                      budget_exhausted:
                        type: boolean
                        description: |
                          True when the daily epsilon budget cannot accommodate this request.
                          `rows` is empty in this case; no epsilon is deducted.
                        example: false
        '400':
          description: |
            Invalid `date`, `geohash_prefix`, or `venue_category` parameter
            (returned by the validation guards in `exportGeohashAudiencePanel`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                success: false
                error: "exportGeohashAudiencePanel: invalid date '2026/05/12'; expected YYYY-MM-DD"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Partner not provisioned for the data-licensing panel (no `partner_id` UUID).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                success: false
                error: Partner not provisioned for data-licensing panel
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ==========================================
  # DASHBOARD ENDPOINTS
  # ==========================================

  /dashboard:
    get:
      tags:
        - Dashboard
      summary: Get dashboard overview
      description: |
        Get comprehensive dashboard overview including partner info, screen stats,
        earnings summary, top performing screens, and recent activity.
      operationId: getDashboard
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Dashboard data retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/DashboardResponse'
              example:
                success: true
                data:
                  partner:
                    name: "Acme Vending Co"
                    slug: "acmevending"
                    status: "active"
                    created_at: "2025-12-01T00:00:00.000Z"
                  stats:
                    total_screens: 3500
                    active_screens: 3420
                    offline_screens: 80
                    pending_screens: 0
                    total_impressions_today: 125000
                    total_impressions_week: 875000
                    total_impressions_month: 3500000
                  earnings:
                    total_earned_cents: 4500000
                    pending_payout_cents: 125000
                    available_payout_cents: 350000
                    next_payout_date: "2026-02-01"
                    revenue_share_percent: 80
                  top_screens:
                    - screen_id: "507f1f77bcf86cd799439011"
                      name: "Mall of America - Floor 2"
                      location: "Bloomington, MN"
                      impressions_today: 1250
                  recent_activity:
                    - type: "impression"
                      screen_id: "507f1f77bcf86cd799439011"
                      ad_id: "ad_123"
                      timestamp: "2026-01-26T12:30:00.000Z"
                  stripe_status:
                    account_status: "active"
                    connect_account_id: "***"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /screens/{screenId}/analytics:
    get:
      tags:
        - Dashboard
      summary: Get per-screen analytics
      description: |
        Get detailed analytics for a specific screen including impression trends,
        daily timeline, and performance metrics over a configurable period.
      operationId: getScreenAnalytics
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          description: Screen ID
          schema:
            type: string
        - name: period
          in: query
          description: Time period for analytics
          schema:
            type: string
            enum: [7d, 30d, 90d]
            default: 7d
      responses:
        '200':
          description: Screen analytics retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/ScreenAnalyticsResponse'
              example:
                success: true
                data:
                  screen:
                    id: "507f1f77bcf86cd799439011"
                    name: "Mall of America - Floor 2"
                    location: "Bloomington, MN"
                    status: "active"
                    online_status: "online"
                  impressions:
                    total: 8750
                    daily_average: 1250
                    trend_percent: 12
                  timeline:
                    - date: "2026-01-20"
                      impressions: 1100
                      avg_duration_ms: 15200
                    - date: "2026-01-21"
                      impressions: 1300
                      avg_duration_ms: 14800
                  period:
                    days: 7
                    start: "2026-01-19T00:00:00.000Z"
                    end: "2026-01-26T23:59:59.999Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Screen does not belong to this partner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'

  # ==========================================
  # EARNINGS ENDPOINTS
  # ==========================================

  /earnings:
    get:
      tags:
        - Earnings
      summary: Get earnings summary
      description: |
        Get comprehensive earnings summary including lifetime earnings, current month,
        available balance, breakdown by source, and payout information.
      operationId: getEarnings
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Earnings summary retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/EarningsResponse'
              example:
                success: true
                data:
                  total_lifetime_cents: 4500000
                  this_month_cents: 450000
                  available_for_payout_cents: 350000
                  pending_confirmation_cents: 125000
                  next_payout_estimate:
                    amount_cents: 350000
                    date: "2026-02-01"
                    stripe_status: "active"
                  breakdown:
                    direct_campaigns: 3000000
                    programmatic_google: 1200000
                    programmatic_floodgates: 300000
                  payout_threshold_cents: 10000
                  revenue_share_percent: 80
        '401':
          $ref: '#/components/responses/Unauthorized'

  /earnings/transactions:
    get:
      tags:
        - Earnings
      summary: Get transaction history
      description: |
        Get paginated list of earnings transactions and payouts.
        Transactions are aggregated by day showing impressions and calculated earnings.
      operationId: getTransactions
      security:
        - BearerAuth: []
      parameters:
        - name: limit
          in: query
          description: Maximum transactions to return
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: offset
          in: query
          description: Offset for pagination
          schema:
            type: integer
            default: 0
        - name: type
          in: query
          description: Filter by transaction type
          schema:
            type: string
            enum: [all, earning, payout]
            default: all
      responses:
        '200':
          description: Transactions retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      transactions:
                        type: array
                        items:
                          $ref: '#/components/schemas/Transaction'
                      pagination:
                        $ref: '#/components/schemas/Pagination'
              example:
                success: true
                data:
                  transactions:
                    - id: "daily_2026-01-25"
                      type: "earning"
                      amount_cents: 15000
                      source: "impressions"
                      impressions: 5000
                      date: "2026-01-25"
                      created_at: "2026-01-25T00:00:00.000Z"
                    - id: "daily_2026-01-24"
                      type: "earning"
                      amount_cents: 14200
                      source: "impressions"
                      impressions: 4733
                      date: "2026-01-24"
                      created_at: "2026-01-24T00:00:00.000Z"
                  pagination:
                    total: 45
                    limit: 50
                    offset: 0
                    has_more: false
        '401':
          $ref: '#/components/responses/Unauthorized'

  /earnings/breakdown:
    get:
      tags:
        - Earnings
      summary: Get detailed earnings breakdown
      description: |
        Get earnings breakdown grouped by day, week, or screen.
        Useful for detailed financial reporting and analysis.
      operationId: getEarningsBreakdown
      security:
        - BearerAuth: []
      parameters:
        - name: start_date
          in: query
          description: Start date (ISO 8601 format)
          schema:
            type: string
            format: date
        - name: end_date
          in: query
          description: End date (ISO 8601 format)
          schema:
            type: string
            format: date
        - name: group_by
          in: query
          description: How to group the breakdown
          schema:
            type: string
            enum: [day, week, screen]
            default: day
      responses:
        '200':
          description: Earnings breakdown retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/EarningsBreakdownResponse'
              example:
                success: true
                data:
                  summary:
                    total_cents: 450000
                    total_impressions: 150000
                    direct_cents: 450000
                    programmatic_cents: 0
                  breakdown:
                    - date: "2026-01-25"
                      impressions: 5000
                      earnings_cents: 15000
                    - date: "2026-01-24"
                      impressions: 4733
                      earnings_cents: 14200
                  pending_programmatic:
                    estimated_cents: 0
                    confirmation_date: null
                  period:
                    start: "2025-12-27T00:00:00.000Z"
                    end: "2026-01-26T23:59:59.999Z"
                    group_by: "day"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /earnings/payout:
    post:
      tags:
        - Earnings
      summary: Request a payout
      description: |
        Request a payout of available earnings to your connected Stripe account.
        Requires Stripe Connect onboarding to be complete.

        **Requirements:**
        - Stripe account must be connected and active
        - Available balance must meet minimum threshold ($100 default)
        - If `amount_cents` not specified, full available balance is requested
      operationId: requestPayout
      security:
        - BearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_cents:
                  type: integer
                  description: Amount to payout in cents (optional, defaults to available balance)
                  minimum: 0
            example:
              amount_cents: 350000
      responses:
        '200':
          description: Payout request submitted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/PayoutResponse'
              example:
                success: true
                message: "Payout request submitted"
                data:
                  payout_id: "po_1234567890_abc123"
                  amount_cents: 350000
                  status: "processing"
                  estimated_arrival: "2026-01-31"
                  stripe_transfer_id: null
        '400':
          description: Payout requirements not met
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: "Minimum payout is $100.00"
                  data:
                    type: object
                    properties:
                      available_cents:
                        type: integer
                      threshold_cents:
                        type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ==========================================
  # TEAM MANAGEMENT ENDPOINTS
  # ==========================================

  /team/invite:
    post:
      tags:
        - Team
      summary: Invite a team member
      description: |
        Invite a new team member to join your partner organization.
        The invitee will receive an email with instructions to join.

        **Authorization:** Requires both partner API key AND user JWT with owner role.
        Include user JWT in `X-User-Token` header.

        **Roles:**
        - `admin` - Can manage screens, view analytics, configure webhooks
        - `viewer` - Can only view screen list and status
      operationId: inviteTeamMember
      security:
        - BearerAuth: []
      parameters:
        - name: X-User-Token
          in: header
          required: true
          description: User JWT token for authorization
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Email address of the invitee
                role:
                  type: string
                  enum: [admin, viewer]
                  default: viewer
                  description: Role to assign to the team member
                name:
                  type: string
                  description: Optional name of the invitee
            example:
              email: "teammate@example.com"
              role: "viewer"
              name: "John Smith"
      responses:
        '201':
          description: Invitation sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/TeamInviteResponse'
              example:
                success: true
                message: "Invitation sent successfully"
                data:
                  membership_id: "507f1f77bcf86cd799439011"
                  email: "teammate@example.com"
                  role: "viewer"
                  status: "pending"
                  expires_at: "2026-02-02T00:00:00.000Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Only owners can invite team members
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: User already has membership or pending invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /team:
    get:
      tags:
        - Team
      summary: List team members
      description: |
        List all team members for the partner organization.
        Returns member info, roles, and permissions.
      operationId: listTeamMembers
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          description: Filter by membership status
          schema:
            type: string
            enum: [active, pending, all]
            default: active
      responses:
        '200':
          description: Team members retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      members:
                        type: array
                        items:
                          $ref: '#/components/schemas/TeamMember'
                      total:
                        type: integer
                      partner:
                        type: object
                        properties:
                          name:
                            type: string
                          slug:
                            type: string
              example:
                success: true
                data:
                  members:
                    - membership_id: "507f1f77bcf86cd799439011"
                      user_id: "507f1f77bcf86cd799439012"
                      email: "owner@example.com"
                      name: "Jane Owner"
                      photo: null
                      role: "owner"
                      status: "active"
                      invited_at: "2025-12-01T00:00:00.000Z"
                      accepted_at: "2025-12-01T00:00:00.000Z"
                      permissions:
                        view_screens: true
                        view_screen_status: true
                        view_analytics: true
                        view_earnings: true
                        manage_team: true
                    - membership_id: "507f1f77bcf86cd799439013"
                      user_id: "507f1f77bcf86cd799439014"
                      email: "viewer@example.com"
                      name: "John Viewer"
                      photo: null
                      role: "viewer"
                      status: "active"
                      invited_at: "2026-01-15T00:00:00.000Z"
                      accepted_at: "2026-01-15T12:00:00.000Z"
                      permissions:
                        view_screens: true
                        view_screen_status: true
                        view_analytics: false
                        view_earnings: false
                        manage_team: false
                  total: 2
                  partner:
                    name: "Acme Vending Co"
                    slug: "acmevending"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /team/{userId}:
    delete:
      tags:
        - Team
      summary: Remove a team member
      description: |
        Remove a team member from the partner organization.
        The owner cannot be removed.

        **Authorization:** Requires both partner API key AND user JWT with owner role.
      operationId: removeTeamMember
      security:
        - BearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          description: User ID, membership ID, or email of the member to remove
          schema:
            type: string
        - name: X-User-Token
          in: header
          required: true
          description: User JWT token for authorization
          schema:
            type: string
      responses:
        '200':
          description: Team member removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: "Team member removed successfully"
        '400':
          description: Cannot remove owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Only owners can remove team members
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'

  /team/{userId}/role:
    patch:
      tags:
        - Team
      summary: Change team member role
      description: |
        Change the role of a team member. Cannot change owner's role.

        **Authorization:** Requires both partner API key AND user JWT with owner role.
      operationId: changeTeamMemberRole
      security:
        - BearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          description: User ID of the member
          schema:
            type: string
        - name: X-User-Token
          in: header
          required: true
          description: User JWT token for authorization
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
              properties:
                role:
                  type: string
                  enum: [admin, viewer]
            example:
              role: "admin"
      responses:
        '200':
          description: Role changed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      user_id:
                        type: string
                      old_role:
                        type: string
                      new_role:
                        type: string
                      permissions:
                        $ref: '#/components/schemas/RolePermissions'
              example:
                success: true
                message: "Role changed from viewer to admin"
                data:
                  user_id: "507f1f77bcf86cd799439014"
                  old_role: "viewer"
                  new_role: "admin"
                  permissions:
                    view_screens: true
                    view_screen_status: true
                    view_analytics: true
                    view_impressions: true
                    view_earnings: false
                    edit_screens: true
                    manage_team: false
        '400':
          description: Cannot change owner's role or invalid role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Only owners can change roles
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'

  /team/{membershipId}/resend:
    post:
      tags:
        - Team
      summary: Resend invitation
      description: |
        Resend the invitation email to a pending team member.
        Generates a new invitation token with extended expiry.

        **Authorization:** Requires both partner API key AND user JWT with owner role.
      operationId: resendInvitation
      security:
        - BearerAuth: []
      parameters:
        - name: membershipId
          in: path
          required: true
          description: Membership ID of the pending invitation
          schema:
            type: string
        - name: X-User-Token
          in: header
          required: true
          description: User JWT token for authorization
          schema:
            type: string
      responses:
        '200':
          description: Invitation resent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      email:
                        type: string
                      expires_at:
                        type: string
                        format: date-time
              example:
                success: true
                message: "Invitation resent successfully"
                data:
                  email: "teammate@example.com"
                  expires_at: "2026-02-02T00:00:00.000Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Only owners can resend invitations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Pending invitation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /team/invite/{token}:
    get:
      tags:
        - Team
      summary: Get invitation details
      description: |
        Get details about an invitation by token.
        This is a public endpoint used during the signup flow.
      operationId: getInvitationByToken
      parameters:
        - name: token
          in: path
          required: true
          description: Invitation token from email link
          schema:
            type: string
      responses:
        '200':
          description: Invitation details retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      partner:
                        type: object
                        properties:
                          name:
                            type: string
                          slug:
                            type: string
                      email:
                        type: string
                      role:
                        type: string
                      invited_at:
                        type: string
                        format: date-time
                      expires_at:
                        type: string
                        format: date-time
              example:
                success: true
                data:
                  partner:
                    name: "Acme Vending Co"
                    slug: "acmevending"
                  email: "teammate@example.com"
                  role: "viewer"
                  invited_at: "2026-01-26T00:00:00.000Z"
                  expires_at: "2026-02-02T00:00:00.000Z"
        '404':
          description: Invalid or expired invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /team/accept:
    post:
      tags:
        - Team
      summary: Accept team invitation
      description: |
        Accept a team invitation using the token from the invitation email.
        Can be called with or without authentication.

        **Without auth:** User with the invited email must exist
        **With auth:** Links invitation to the authenticated user's account
      operationId: acceptInvitation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
              properties:
                token:
                  type: string
                  description: Invitation token from email link
            example:
              token: "inv_abc123def456"
      responses:
        '200':
          description: Invitation accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      partner:
                        type: object
                        properties:
                          id:
                            type: string
                          name:
                            type: string
                          slug:
                            type: string
                      role:
                        type: string
                      permissions:
                        $ref: '#/components/schemas/RolePermissions'
                      portal_url:
                        type: string
              example:
                success: true
                message: "Successfully joined Acme Vending Co"
                data:
                  partner:
                    id: "507f1f77bcf86cd799439011"
                    name: "Acme Vending Co"
                    slug: "acmevending"
                  role: "viewer"
                  permissions:
                    view_screens: true
                    view_screen_status: true
                    view_analytics: false
                  portal_url: "https://trillboards.com/earner"
        '400':
          description: Token missing or user account required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Invalid or expired invitation token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Already a member of this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================
  # EARNER UPGRADE ENDPOINT (Alternative Registration)
  # Note: This endpoint is at /v2/earner, not /v1/partner
  # ============================================

  /upgrade-to-partner:
    post:
      tags:
        - Partners
      summary: Self-service upgrade from Earner to Partner
      description: |
        **Base URL:** `https://api.trillboards.com/v2/earner` (not /v1/partner)

        Allows existing earners to upgrade to Partner status and receive an API key
        for programmatic screen management. This is an alternative to `/register` for
        users who already have a Trillboards earner account.

        **Authentication:** Requires user JWT token (not partner API key)

        **Use case:** An earner with existing screens wants to:
        - Register additional screens via API
        - Build a custom integration
        - Manage screens programmatically

        After upgrade:
        - User receives Partner API key (one-time display)
        - User can see both their existing (userId-based) screens AND new partner screens
        - Existing screens remain unchanged
        - New screens registered via API will have partner_id set

        **Rate limit:** 1 partner per user (prevents abuse)
      operationId: upgradeToPartner
      security:
        - UserJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - partner_name
                - api_agreement_accepted
              properties:
                partner_name:
                  type: string
                  minLength: 2
                  maxLength: 100
                  example: "Take 10 Media"
                  description: Name of the partner organization
                partner_slug:
                  type: string
                  pattern: '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$'
                  maxLength: 50
                  example: "take10media"
                  description: |
                    URL-safe slug for the partner.
                    Auto-generated from partner_name if not provided.
                    Must be lowercase alphanumeric with hyphens.
                partner_type:
                  type: string
                  enum: [vending_machine, kiosk, digital_signage, retail_display, other]
                  default: digital_signage
                  description: Type of integration
                api_agreement_accepted:
                  type: boolean
                  description: Must be true to accept API terms of service
      responses:
        '201':
          description: Successfully upgraded to partner status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Successfully upgraded to partner status"
                  api_key:
                    type: string
                    example: "trb_partner_xxxxxxxxxxxxxxxxxxxx"
                    description: |
                      **IMPORTANT:** This is shown only once! Save it securely.
                      Cannot be retrieved after this response.
                  partner:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      slug:
                        type: string
                  portal_url:
                    type: string
                    example: "https://trillboards.com/earner/auth"
                  docs_url:
                    type: string
                    example: "https://api.trillboards.com/docs/partner"
                  warning:
                    type: string
                    example: "IMPORTANT: Save your API key now - it cannot be retrieved later."
        '400':
          description: Invalid request (missing partner_name or api_agreement_accepted)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized - missing or invalid user JWT
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden - user is not an earner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            Conflict - either:
            - User already owns a partner organization
            - Partner slug already taken
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                  partner_slug:
                    type: string
                    description: If already an owner, shows existing partner slug
                  suggested_slug:
                    type: string
                    description: If slug taken, suggests alternative

  # ============================================
  # VAST INTEGRATION ENDPOINTS
  # For enterprise partners with existing video players
  # ============================================

  /vast/config:
    get:
      tags:
        - VAST Integration
      summary: Get VAST configuration for direct Google integration
      description: |
        Returns a VAST tag URL template that partners use to fetch ads directly from Google Ad Manager.

        **This is NOT a VAST proxy.** Trillboards provides the configuration; partners fetch VAST directly from Google.

        **Architecture:**
        1. Partner calls this endpoint (cached for 1 hour)
        2. Partner builds VAST URL using the template
        3. Partner fetches VAST directly from Google (`pubads.g.doubleclick.net`)
        4. Partner plays video in their player
        5. Partner reports impressions via `/tracking/batch`

        **Scale:** At 35,000 screens with 1-hour cache, this is ~10 requests/second (trivial load).

        **Security:** Partners only allowlist 2 domains: `api.trillboards.com` and `pubads.g.doubleclick.net`.
      operationId: getVastConfig
      security:
        - BearerAuth: []
      parameters:
        - name: device_id
          in: query
          description: Device identifier for targeting optimization
          schema:
            type: string
            example: "PARTNER-DEVICE-001"
        - name: screen_id
          in: query
          description: Screen ID for targeting (alternative to device_id)
          schema:
            type: string
            example: "507f1f77bcf86cd799439011"
      responses:
        '200':
          description: VAST configuration returned successfully
          headers:
            Cache-Control:
              description: Caching directive (public, max-age=3600)
              schema:
                type: string
                example: "public, max-age=3600"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      vast_tag_template:
                        type: string
                        description: |
                          VAST URL template with placeholders for {width} and {height}.
                          Partner replaces placeholders and appends correlator.
                        example: "https://pubads.g.doubleclick.net/gampad/ads?iu=/105549217/Trillboards_Retail_Video&sz={width}x{height}&venuetype=2&..."
                      parameters:
                        type: object
                        properties:
                          width:
                            type: integer
                            example: 640
                          height:
                            type: integer
                            example: 480
                          venuetype:
                            type: integer
                            description: Venue type code for targeting
                            example: 2
                          min_duration:
                            type: integer
                            description: Minimum ad duration in seconds
                            example: 6
                          max_duration:
                            type: integer
                            description: Maximum ad duration in seconds
                            example: 300
                      ad_unit:
                        type: string
                        description: Google Ad Manager ad unit path
                        example: "/105549217/Trillboards_Retail_Video"
                      variant:
                        type: string
                        description: Configuration variant name for debugging
                        example: "default_retail"
                      floor_price_cpm:
                        type: integer
                        description: Floor price in cents (CPM)
                        example: 400
                      blocked_categories:
                        type: array
                        items:
                          type: string
                        description: IAB content categories to block
                        example: ["IAB25-3", "IAB26"]
                      timeout_ms:
                        type: integer
                        description: Recommended VAST fetch timeout
                        example: 12000
                      cache_ttl_seconds:
                        type: integer
                        description: How long to cache this config
                        example: 3600
                      tracking:
                        type: object
                        properties:
                          batch_endpoint:
                            type: string
                            example: "https://api.trillboards.com/v1/partner/tracking/batch"
                          single_endpoint:
                            type: string
                            example: "https://api.trillboards.com/v1/partner/impression"
                          max_batch_size:
                            type: integer
                            example: 100
                      integration_guide:
                        type: string
                        example: "https://api.trillboards.com/docs/partner/vast-integration"
              example:
                success: true
                data:
                  vast_tag_template: "https://pubads.g.doubleclick.net/gampad/ads?iu=/105549217/Trillboards_Retail_Video&sz={width}x{height}&venuetype=2&npa=0&..."
                  parameters:
                    width: 640
                    height: 480
                    venuetype: 2
                    min_duration: 6
                    max_duration: 300
                  ad_unit: "/105549217/Trillboards_Retail_Video"
                  variant: "default_retail"
                  floor_price_cpm: 400
                  blocked_categories: ["IAB25-3", "IAB26"]
                  timeout_ms: 12000
                  cache_ttl_seconds: 3600
                  tracking:
                    batch_endpoint: "https://api.trillboards.com/v1/partner/tracking/batch"
                    single_endpoint: "https://api.trillboards.com/v1/partner/impression"
                    max_batch_size: 100
                  integration_guide: "https://api.trillboards.com/docs/partner/vast-integration"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ============================================
  # BATCH TRACKING ENDPOINTS
  # High-volume impression reporting with Ed25519 proofs
  # ============================================

  /tracking/batch:
    post:
      tags:
        - Batch Tracking
      summary: Record batch impressions with proof generation
      description: |
        High-volume endpoint for reporting multiple impressions in a single request.

        **Features:**
        - Process up to 100 impressions per batch
        - Ed25519 cryptographic proof for each impression
        - Duplicate detection (impressions with same impression_id + ad_id are ignored)
        - Optional audience metrics (face count, attention)

        **Batching Strategy:**
        - Buffer impressions client-side
        - Flush every 1-5 minutes or when buffer reaches 100
        - Handle failures with local retry queue

        **Proof Verification:**
        Each proof can be independently verified using the public key from `/tracking/.well-known/public-key`.
      operationId: recordTrackingBatch
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - impressions
              properties:
                impressions:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    required:
                      - device_id
                      - ad_id
                    properties:
                      device_id:
                        type: string
                        description: Partner's device identifier
                        example: "PARTNER-DEVICE-001"
                      ad_id:
                        type: string
                        description: Ad identifier from VAST response
                        example: "ima_1706745600_abc123"
                      event:
                        type: string
                        enum: [start, complete, error, firstQuartile, midpoint, thirdQuartile]
                        default: complete
                        description: VAST event type
                      timestamp:
                        type: string
                        format: date-time
                        description: When the event occurred (defaults to now)
                        example: "2026-01-31T10:00:00Z"
                      duration_ms:
                        type: integer
                        description: Video duration played in milliseconds
                        example: 15000
                      impression_id:
                        type: string
                        description: Optional client-generated ID (auto-generated if not provided)
                        example: "imp_client_abc123"
                      audience:
                        type: object
                        description: Optional audience metrics
                        properties:
                          face_count:
                            type: integer
                            description: Number of viewers detected
                            example: 3
                          attention:
                            type: number
                            format: float
                            minimum: 0
                            maximum: 1
                            description: Attention score (0-1)
                            example: 0.87
                          dwell_time_ms:
                            type: integer
                            description: How long viewers were present
                            example: 45000
            example:
              impressions:
                - device_id: "PARTNER-DEVICE-001"
                  ad_id: "ima_1706745600_abc123"
                  event: "complete"
                  timestamp: "2026-01-31T10:00:00Z"
                  duration_ms: 15000
                  audience:
                    face_count: 3
                    attention: 0.87
                - device_id: "PARTNER-DEVICE-002"
                  ad_id: "ima_1706745600_def456"
                  event: "complete"
                  timestamp: "2026-01-31T10:00:05Z"
                  duration_ms: 30000
      responses:
        '200':
          description: Batch processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      processed:
                        type: integer
                        description: Number of impressions successfully recorded
                        example: 100
                      failed:
                        type: integer
                        description: Number of impressions that failed validation
                        example: 0
                      duplicates:
                        type: integer
                        description: Number of duplicate impressions skipped
                        example: 0
                      proofs:
                        type: array
                        description: Cryptographic proofs for each processed impression
                        items:
                          type: object
                          properties:
                            impression_id:
                              type: string
                              example: "imp_1706745600_a1b2c3"
                            signature:
                              type: string
                              description: Ed25519 signature (or SHA256 fallback)
                              example: "ed25519=7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d..."
                            public_key_id:
                              type: string
                              example: "pk_v2_001"
                      errors:
                        type: array
                        description: Details of failed impressions (max 10 shown)
                        items:
                          type: object
                          properties:
                            impression_id:
                              type: string
                            error:
                              type: string
                      processing_time_ms:
                        type: integer
                        description: Time to process the batch
                        example: 45
              example:
                success: true
                data:
                  processed: 2
                  failed: 0
                  duplicates: 0
                  proofs:
                    - impression_id: "imp_1706745600_a1b2c3"
                      signature: "ed25519=7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
                      public_key_id: "pk_v2_001"
                    - impression_id: "imp_1706745605_d4e5f6"
                      signature: "ed25519=8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c"
                      public_key_id: "pk_v2_001"
                  errors: []
                  processing_time_ms: 45
        '400':
          description: Invalid request (empty array, too many impressions, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                empty_array:
                  value:
                    success: false
                    message: "impressions array cannot be empty"
                too_many:
                  value:
                    success: false
                    message: "Maximum 100 impressions per batch"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /tracking/.well-known/public-key:
    get:
      tags:
        - Batch Tracking
      summary: Get public key for proof verification
      description: |
        Returns the Ed25519 public key used to sign impression proofs.

        **No authentication required.** This is a public endpoint to enable third-party verification.

        **Verification Process:**
        1. Fetch public key from this endpoint
        2. Reconstruct canonical payload: `v2.{timestamp}.{adId}.{impressionId}.{screenId}.{deviceId}`
        3. Verify Ed25519 signature using the public key

        See `/tracking/batch` response for signature format.
      operationId: getTrackingPublicKey
      responses:
        '200':
          description: Public key information
          headers:
            Cache-Control:
              description: Caching directive (24 hours)
              schema:
                type: string
                example: "public, max-age=86400"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      publicKey:
                        type: string
                        nullable: true
                        description: Hex-encoded Ed25519 public key (null if using HMAC fallback)
                        example: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
                      algorithm:
                        type: string
                        enum: [ed25519, sha256]
                        description: Signature algorithm (ed25519 for public key, sha256 for HMAC fallback)
                        example: "ed25519"
                      version:
                        type: string
                        example: "v2"
                      keyId:
                        type: string
                        description: Key identifier for rotation support
                        example: "pk_v2_001"
                      verificationUrl:
                        type: string
                        example: "https://api.trillboards.com/v1/partner/tracking/verify"
                      documentation:
                        type: string
                        example: "https://api.trillboards.com/docs/proof-of-play"
              example:
                success: true
                data:
                  publicKey: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
                  algorithm: "ed25519"
                  version: "v2"
                  keyId: "pk_v2_001"
                  verificationUrl: "https://api.trillboards.com/v1/partner/tracking/verify"
                  documentation: "https://api.trillboards.com/docs/proof-of-play"

  # ────────────────────────────────────────────
  # Stripe Connect
  # ────────────────────────────────────────────

  /stripe/connect:
    post:
      tags:
        - Stripe Connect
      summary: Create Stripe Connect account
      description: |
        Creates a Stripe Connect Express account for the partner. If the partner already
        has a Stripe account, returns the existing account details with HTTP 200.
      operationId: createStripeConnectAccount
      security:
        - BearerAuth: []
      responses:
        '201':
          description: Stripe Connect account created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      account_id:
                        type: string
                        example: acct_1234567890
                      charges_enabled:
                        type: boolean
                      payouts_enabled:
                        type: boolean
                      details_submitted:
                        type: boolean
                      country:
                        type: string
                        example: US
                      default_currency:
                        type: string
                        example: usd
                      next_step:
                        type: string
                        nullable: true
        '200':
          description: Stripe account already exists
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: Stripe API error

  /stripe/onboarding-link:
    get:
      tags:
        - Stripe Connect
      summary: Get Stripe onboarding link
      description: |
        Returns a temporary Stripe-hosted onboarding URL where the partner can
        complete identity verification and bank account setup.
      operationId: getStripeOnboardingLink
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Onboarding link generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      onboarding_url:
                        type: string
                        format: uri
                      expires_at:
                        type: string
                        format: date-time
                      account_id:
                        type: string
                      instructions:
                        type: string
        '400':
          description: Partner has no Stripe account — call POST /stripe/connect first
        '401':
          $ref: '#/components/responses/Unauthorized'

  /stripe/login-link:
    get:
      tags:
        - Stripe Connect
      summary: Get Stripe dashboard login link
      description: Returns a temporary link to the partner's Stripe Express dashboard.
      operationId: getStripeLoginLink
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Login link generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      login_url:
                        type: string
                        format: uri
                      account_id:
                        type: string
                      instructions:
                        type: string
        '400':
          description: Partner has no Stripe account
        '401':
          $ref: '#/components/responses/Unauthorized'

  /stripe/status:
    get:
      tags:
        - Stripe Connect
      summary: Get Stripe account status
      description: |
        Returns the current status of the partner's Stripe Connect account including
        onboarding requirements and payout readiness.
      operationId: getStripeAccountStatus
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Account status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      has_account:
                        type: boolean
                      status:
                        type: string
                        enum: [not_connected, pending, active, restricted]
                      account_id:
                        type: string
                        nullable: true
                      charges_enabled:
                        type: boolean
                      payouts_enabled:
                        type: boolean
                      details_submitted:
                        type: boolean
                      country:
                        type: string
                      default_currency:
                        type: string
                      requirements:
                        type: object
                        properties:
                          currently_due:
                            type: array
                            items:
                              type: string
                          eventually_due:
                            type: array
                            items:
                              type: string
                          past_due:
                            type: array
                            items:
                              type: string
                          disabled_reason:
                            type: string
                            nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'

  /stripe/payout:
    post:
      tags:
        - Stripe Connect
      summary: Request a payout
      description: |
        Initiates a payout transfer to the partner's Stripe Connect account.
        Minimum payout is $5.00 (500 cents). The partner must have an active
        Stripe account and sufficient available balance.
      operationId: requestStripePayout
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount_cents
              properties:
                amount_cents:
                  type: integer
                  minimum: 500
                  description: Payout amount in cents (minimum $5.00)
                  example: 10000
                description:
                  type: string
                  description: Optional description for the payout
                idempotency_key:
                  type: string
                  description: Deduplication key to prevent duplicate payouts
      responses:
        '200':
          description: Payout initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Payout of $100.00 initiated
                  data:
                    type: object
                    properties:
                      transfer_id:
                        type: string
                        example: tr_1234567890
                      amount_cents:
                        type: integer
                      currency:
                        type: string
                      destination:
                        type: string
                      status:
                        type: string
                        enum: [pending, reversed]
                      created:
                        type: string
                        format: date-time
                      description:
                        type: string
                      estimated_arrival:
                        type: string
                        format: date-time
                        description: Estimated arrival (2-3 business days)
        '400':
          description: Invalid amount, insufficient balance, or account not active
        '401':
          $ref: '#/components/responses/Unauthorized'

  /stripe/payouts:
    get:
      tags:
        - Stripe Connect
      summary: Get payout history
      description: Returns a paginated list of past Stripe payouts for the partner.
      operationId: getStripePayoutHistory
      security:
        - BearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 25
            maximum: 100
          description: Maximum number of payouts to return
      responses:
        '200':
          description: Payout history
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      transfers:
                        type: array
                        items:
                          type: object
                          properties:
                            transfer_id:
                              type: string
                            amount_cents:
                              type: integer
                            currency:
                              type: string
                            status:
                              type: string
                              enum: [completed, reversed]
                            created:
                              type: string
                              format: date-time
                            description:
                              type: string
                      has_more:
                        type: boolean
                      total:
                        type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /stripe/webhook:
    post:
      tags:
        - Stripe Connect
      summary: Stripe webhook receiver
      description: |
        Receives webhook events from Stripe for Connect account updates and transfer status changes.
        This endpoint is called by Stripe, not by the partner. It uses Stripe signature verification.

        **Handled events:** `account.updated`, `transfer.created`, `transfer.paid`, `transfer.reversed`
      operationId: handleStripeWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Raw Stripe webhook event payload
      responses:
        '200':
          description: Webhook acknowledged
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                    example: true
                  event_type:
                    type: string

  # ────────────────────────────────────────────
  # Connect Dashboard
  # ────────────────────────────────────────────

  /connect/dashboard:
    get:
      tags:
        - Connect Dashboard
      summary: Get partner dashboard overview
      description: |
        Returns a comprehensive dashboard with screen counts, impression stats,
        earnings data, daily charts, and top-performing screens.
        Optimized for embedding in partner SDK dashboards.
      operationId: getConnectDashboard
      security:
        - BearerAuth: []
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum: ['7d', '30d', '90d']
            default: '30d'
          description: Time period for analytics data
      responses:
        '200':
          description: Dashboard data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      partner:
                        type: object
                        properties:
                          name:
                            type: string
                          slug:
                            type: string
                          status:
                            type: string
                          stripe_status:
                            type: string
                            enum: [pending, active, restricted, not_connected]
                          revenue_share_percent:
                            type: number
                          cpm_cents:
                            type: number
                      screens:
                        type: object
                        properties:
                          total:
                            type: integer
                          online:
                            type: integer
                          offline:
                            type: integer
                          pending:
                            type: integer
                      impressions:
                        type: object
                        properties:
                          today:
                            type: integer
                          period:
                            type: integer
                          all_time:
                            type: integer
                          daily_average:
                            type: number
                          trend_percent:
                            type: number
                      earnings:
                        type: object
                        properties:
                          today_cents:
                            type: integer
                          period_cents:
                            type: integer
                          lifetime_cents:
                            type: integer
                          pending_payout_cents:
                            type: integer
                          available_cents:
                            type: integer
                          daily_average_cents:
                            type: integer
                      charts:
                        type: object
                        properties:
                          daily_impressions:
                            type: array
                            items:
                              type: object
                              properties:
                                date:
                                  type: string
                                  format: date
                                impressions:
                                  type: integer
                                earnings_cents:
                                  type: integer
                          hourly_today:
                            type: array
                            items:
                              type: object
                              properties:
                                hour:
                                  type: integer
                                  minimum: 0
                                  maximum: 23
                                impressions:
                                  type: integer
                      top_screens:
                        type: array
                        items:
                          type: object
                          properties:
                            screen_id:
                              type: string
                            name:
                              type: string
                            location:
                              type: string
                              nullable: true
                            status:
                              type: string
                              enum: [online, offline, unknown]
                            impressions:
                              type: integer
                            earnings_cents:
                              type: integer
                      period:
                        type: object
                        properties:
                          days:
                            type: integer
                          start:
                            type: string
                            format: date-time
                          end:
                            type: string
                            format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'

  /connect/earnings:
    get:
      tags:
        - Connect Dashboard
      summary: Get earnings chart data
      description: Returns daily or weekly earnings breakdown for charting.
      operationId: getConnectEarningsChart
      security:
        - BearerAuth: []
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum: ['7d', '30d', '90d']
            default: '30d'
        - name: group_by
          in: query
          schema:
            type: string
            enum: [day, week]
            default: day
      responses:
        '200':
          description: Earnings chart data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      summary:
                        type: object
                        properties:
                          total_impressions:
                            type: integer
                          total_earnings_cents:
                            type: integer
                          data_points:
                            type: integer
                          group_by:
                            type: string
                      chart:
                        type: array
                        items:
                          type: object
                          properties:
                            label:
                              type: string
                            date:
                              type: string
                            impressions:
                              type: integer
                            earnings_cents:
                              type: integer
                            unique_screens:
                              type: integer
                            avg_view_duration_ms:
                              type: number
                      period:
                        type: object
                        properties:
                          days:
                            type: integer
                          start:
                            type: string
                            format: date-time
                          end:
                            type: string
                            format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'

  /connect/health:
    get:
      tags:
        - Connect Dashboard
      summary: Get screen health status
      description: |
        Returns the online/offline status of all partner devices with heartbeat
        timestamps, uptime percentages, and location data.
      operationId: getConnectScreenHealth
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [online, offline, all]
            default: all
          description: Filter by screen status
      responses:
        '200':
          description: Screen health data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      summary:
                        type: object
                        properties:
                          total:
                            type: integer
                          online:
                            type: integer
                          offline:
                            type: integer
                          never_connected:
                            type: integer
                      screens:
                        type: array
                        items:
                          type: object
                          properties:
                            device_id:
                              type: string
                            external_device_id:
                              type: string
                            fingerprint:
                              type: string
                            screen_id:
                              type: string
                              nullable: true
                            name:
                              type: string
                            status:
                              type: string
                              enum: [online, offline, never_connected, unknown]
                            screen_status:
                              type: string
                              enum: [pending, active, inactive]
                            last_heartbeat:
                              type: string
                              format: date-time
                              nullable: true
                            last_seen:
                              type: string
                              format: date-time
                              nullable: true
                            time_since_last_seen_seconds:
                              type: number
                              nullable: true
                            uptime_24h_percent:
                              type: number
                              nullable: true
                            location:
                              type: object
                              properties:
                                city:
                                  type: string
                                  nullable: true
                                state:
                                  type: string
                                  nullable: true
                                lat:
                                  type: number
                                  nullable: true
                                lng:
                                  type: number
                                  nullable: true
                            venue_type:
                              type: string
                              nullable: true
                            registered_at:
                              type: string
                              format: date-time
                      filter:
                        type: string
                      checked_at:
                        type: string
                        format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ────────────────────────────────────────────
  # Fleet Management
  # ────────────────────────────────────────────

  /fleet/overview:
    get:
      tags:
        - Fleet Management
      summary: Get fleet overview
      description: |
        Returns a summary of all devices in the partner's fleet including
        online/offline counts, venue type distribution, and country breakdown.
      operationId: getFleetOverview
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Fleet overview
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      total_devices:
                        type: integer
                      online:
                        type: integer
                      offline:
                        type: integer
                      programmatic_enabled:
                        type: integer
                      venue_types:
                        type: object
                        additionalProperties:
                          type: integer
                        example:
                          retail: 45
                          transit: 12
                          office: 8
                      countries:
                        type: object
                        additionalProperties:
                          type: integer
                        example:
                          US: 50
                          CA: 10
        '401':
          $ref: '#/components/responses/Unauthorized'

  /fleet/analytics:
    get:
      tags:
        - Fleet Management
      summary: Get fleet analytics
      description: |
        Returns fleet-wide performance metrics for the last 24 hours including
        total impressions, fill rate, average CPM, and attention score.
      operationId: getFleetAnalytics
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Fleet analytics
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      screen_count:
                        type: integer
                      impressions_24h:
                        type: integer
                      fill_rate_pct:
                        type: number
                        description: Fill rate percentage (0-100)
                      avg_cpm:
                        type: number
                        description: Average CPM in dollars
                      avg_attention:
                        type: number
                        description: Average attention score (0-1)
        '401':
          $ref: '#/components/responses/Unauthorized'

  /fleet/command:
    post:
      tags:
        - Fleet Management
      summary: Push command to fleet
      description: |
        Sends a command to all devices in the fleet (or a filtered subset).
        Commands expire after 10 minutes. Maximum 500 devices per batch.
      operationId: pushFleetCommand
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - command
              properties:
                command:
                  type: object
                  required:
                    - type
                  properties:
                    type:
                      type: string
                      enum: [reload, update_settings, restart, screenshot, clear_cache, update_content, refresh_ads, update_config, get_state]
                    payload:
                      type: object
                      description: Command-specific data
                filter:
                  type: object
                  description: Optional filter to target specific devices
                  properties:
                    screenIds:
                      type: array
                      items:
                        type: string
                    venueType:
                      type: string
                    country:
                      type: string
      responses:
        '200':
          description: Command queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      targeted:
                        type: integer
                        description: Total devices matching filter
                      queued:
                        type: integer
                        description: Devices successfully queued
                      errors:
                        type: array
                        items:
                          type: object
                          properties:
                            screenId:
                              type: string
                            error:
                              type: string
        '400':
          description: Missing command.type
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /fleet/settings:
    post:
      tags:
        - Fleet Management
      summary: Update fleet-wide settings
      description: |
        Updates settings across all devices in the fleet (or a filtered subset).
        Settings are merged with existing values, not replaced.

        **Allowed settings:** `header_bidding_settings`, `content_preferences`, `display_settings`
      operationId: updateFleetSettings
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - settings
              properties:
                settings:
                  type: object
                  description: Settings to update (merged with existing)
                  properties:
                    header_bidding_settings:
                      type: object
                      properties:
                        enabled:
                          type: boolean
                        allocation:
                          type: number
                        min_interval_seconds:
                          type: integer
                        force_request:
                          type: boolean
                    content_preferences:
                      type: object
                    display_settings:
                      type: object
                filter:
                  type: object
                  properties:
                    screenIds:
                      type: array
                      items:
                        type: string
                    venueType:
                      type: string
                    country:
                      type: string
      responses:
        '200':
          description: Settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      targeted:
                        type: integer
                      updated:
                        type: integer
                      errors:
                        type: array
                        items:
                          type: object
        '400':
          description: Empty settings or invalid keys
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ────────────────────────────────────────────
  # Venue Intelligence
  # ────────────────────────────────────────────

  /venues/{venueId}/intelligence:
    get:
      tags:
        - Venue Intelligence
      summary: Get venue intelligence report
      description: |
        Returns aggregated venue intelligence including multi-screen deduplication,
        timeseries data with demographics, weather context, and audience metrics.
        Maximum date range is 30 days.
      operationId: getVenueIntelligence
      security:
        - BearerAuth: []
      parameters:
        - name: venueId
          in: path
          required: true
          schema:
            type: string
          description: Venue ID (MongoDB ObjectId)
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Start date (ISO 8601)
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: End date (ISO 8601)
        - name: interval
          in: query
          schema:
            type: string
            enum: ['5min', '15min', '1h', '1d']
            default: '1h'
          description: Time bucket interval
      responses:
        '200':
          description: Venue intelligence data
          content:
            application/json:
              schema:
                type: object
                properties:
                  venue_id:
                    type: string
                  from:
                    type: string
                    format: date-time
                  to:
                    type: string
                    format: date-time
                  interval:
                    type: string
                  dedup:
                    type: object
                    properties:
                      method:
                        type: string
                        enum: [lincoln_petersen, single_camera]
                      unique_total:
                        type: integer
                      raw_total:
                        type: integer
                      camera_count:
                        type: integer
                  summary:
                    type: object
                    properties:
                      total_unique_viewers:
                        type: integer
                      avg_attention:
                        type: number
                      buckets:
                        type: integer
                  timeseries:
                    type: array
                    items:
                      type: object
                      properties:
                        bucket:
                          type: string
                          format: date-time
                        viewers:
                          type: integer
                          description: Deduplicated viewer count
                        raw_viewers:
                          type: integer
                        peak_viewers:
                          type: integer
                        avg_attention:
                          type: number
                        avg_engagement:
                          type: number
                        mood:
                          type: string
                          nullable: true
                        income_level:
                          type: string
                          nullable: true
                        age_distribution:
                          type: object
                          additionalProperties:
                            type: number
                        gender_distribution:
                          type: object
                          properties:
                            male:
                              type: number
                            female:
                              type: number
                        weather:
                          type: object
                          properties:
                            condition:
                              type: string
                              nullable: true
                            temperature_f:
                              type: number
                              nullable: true
                            is_raining:
                              type: boolean
                        brand_mention_windows:
                          type: integer
                        avg_noise_level:
                          type: number
                          nullable: true
                        measurements:
                          type: integer
                        screen_count:
                          type: integer
        '400':
          description: Missing or invalid parameters (from/to required, max 30 days)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Venue not found or not authorized

  # ────────────────────────────────────────────
  # Usage
  # ────────────────────────────────────────────

  /usage:
    get:
      tags:
        - Usage
      summary: Get API usage summary
      description: |
        Returns API usage statistics including total calls, error rates,
        top endpoints, and daily breakdown. Default range is 30 days, maximum 90 days.
      operationId: getUsageSummary
      security:
        - BearerAuth: []
      parameters:
        - name: start_date
          in: query
          schema:
            type: string
            format: date-time
          description: Start date (ISO 8601). Defaults to 30 days ago.
        - name: end_date
          in: query
          schema:
            type: string
            format: date-time
          description: End date (ISO 8601). Defaults to now.
      responses:
        '200':
          description: Usage summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: usage_summary
                  period:
                    type: object
                    properties:
                      start_date:
                        type: string
                        format: date-time
                      end_date:
                        type: string
                        format: date-time
                  totalCalls:
                    type: integer
                  totalErrors:
                    type: integer
                  errorRate:
                    type: number
                    description: Error rate percentage (0-100)
                  avgResponseTime:
                    type: number
                    description: Average response time in milliseconds
                  topEndpoints:
                    type: array
                    items:
                      type: object
                      properties:
                        endpoint:
                          type: string
                        method:
                          type: string
                        total_calls:
                          type: integer
                        total_errors:
                          type: integer
                  dailyBreakdown:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        total_calls:
                          type: integer
                        total_errors:
                          type: integer
                        avg_response_ms:
                          type: number
        '401':
          $ref: '#/components/responses/Unauthorized'

  /usage/dashboard:
    get:
      tags:
        - Usage
      summary: Get real-time usage dashboard
      description: |
        Returns a comprehensive usage dashboard with today's stats, rate limit status,
        and last 7 days breakdown. Combines real-time Redis data with historical MongoDB data.
      operationId: getUsageDashboard
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Usage dashboard
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: usage_dashboard
                  today:
                    type: object
                    properties:
                      total_calls:
                        type: integer
                      total_errors:
                        type: integer
                      error_rate:
                        type: number
                      avg_response_ms:
                        type: number
                      top_endpoints:
                        type: array
                        items:
                          type: object
                          properties:
                            endpoint:
                              type: string
                            method:
                              type: string
                            total_calls:
                              type: integer
                            total_errors:
                              type: integer
                            avg_response_ms:
                              type: number
                  rate_limit:
                    type: object
                    properties:
                      current_rate:
                        type: integer
                      max_rate:
                        type: integer
                      window_seconds:
                        type: integer
                        example: 60
                      window_reset:
                        type: string
                        format: date-time
                      percent_used:
                        type: number
                  last_7_days:
                    type: object
                    properties:
                      total_calls:
                        type: integer
                      total_errors:
                        type: integer
                      error_rate:
                        type: number
                      avg_response_ms:
                        type: number
                      daily_breakdown:
                        type: array
                        items:
                          type: object
                          properties:
                            date:
                              type: string
                              format: date
                            total_calls:
                              type: integer
                            total_errors:
                              type: integer
                            avg_response_ms:
                              type: number
        '401':
          $ref: '#/components/responses/Unauthorized'

  /usage/rate-limit:
    get:
      tags:
        - Usage
      summary: Get rate limit status
      description: |
        Returns the current rate limit status for the partner based on their data access tier.

        | Tier | Rate Limit |
        |------|-----------|
        | Basic | 200/min |
        | Developer | 1,000/min |
        | Enterprise | 5,000/min |
      operationId: getRateLimitStatus
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Rate limit status
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: rate_limit_status
                  tier:
                    type: string
                    enum: [basic, developer, enterprise]
                  current_rate:
                    type: integer
                  max_rate:
                    type: integer
                  window_seconds:
                    type: integer
                    example: 60
                  window_reset:
                    type: string
                    format: date-time
                  percent_used:
                    type: number
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ────────────────────────────────────────────
  # SSE Events
  # ────────────────────────────────────────────

  /events/stream:
    get:
      tags:
        - Events
      summary: Subscribe to real-time events (SSE)
      description: |
        Opens a Server-Sent Events (SSE) connection for real-time event streaming.
        Maximum 5 concurrent connections per partner.

        Supports `Last-Event-ID` header for reconnection and missed event replay.
        The server sends heartbeat comments (`:`) every 30 seconds to keep the connection alive.
      operationId: subscribeToEvents
      security:
        - BearerAuth: []
      parameters:
        - name: events
          in: query
          schema:
            type: string
          description: Comma-separated list of event types to subscribe to. Empty subscribes to all events.
          example: device.online,device.offline,impression.recorded
      responses:
        '200':
          description: SSE event stream
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  SSE-formatted messages:
                  ```
                  event: device.online
                  id: evt_1234567890_abc123
                  data: {"device_id":"...","timestamp":"..."}
                  ```
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          description: Maximum SSE connections reached (5 per partner)

  /events/connections:
    get:
      tags:
        - Events
      summary: List active SSE connections
      description: Returns the number of active SSE connections and their details.
      operationId: getActiveSSEConnections
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Active connections
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: sse_connections
                  count:
                    type: integer
                  max_connections:
                    type: integer
                    example: 5
                  connections:
                    type: array
                    items:
                      type: object
                      properties:
                        connection_id:
                          type: string
                        subscribed_events:
                          type: array
                          items:
                            type: string
                        connected_at:
                          type: string
                          format: date-time
                        ip:
                          type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ────────────────────────────────────────────
  # Network Config
  # ────────────────────────────────────────────

  /network-config:
    get:
      tags:
        - Network Config
      summary: Get network configuration
      description: Returns the partner's white-label network configuration and parent partner ID.
      operationId: getNetworkConfig
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Network configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: network_config
                  network_config:
                    type: object
                    properties:
                      default_content_preferences:
                        type: object
                      default_waterfall:
                        type: object
                      branding:
                        type: object
                  parent_partner_id:
                    type: string
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
    put:
      tags:
        - Network Config
      summary: Update network configuration
      description: |
        Full update of network configuration. Each provided section is merged
        with the existing configuration (not replaced entirely).
      operationId: updateNetworkConfig
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                default_content_preferences:
                  type: object
                  description: Default content preferences for new screens
                default_waterfall:
                  type: object
                  description: Default waterfall/ad serving configuration
                branding:
                  type: object
                  description: White-label branding settings
      responses:
        '200':
          description: Configuration updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: network_config
                  network_config:
                    type: object
                  parent_partner_id:
                    type: string
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Network Config
      summary: Partial update network configuration
      description: |
        Partial update of specific fields within the network configuration.
        Only the provided nested keys are updated; unmentioned keys are preserved.

        **Accepted top-level keys:** `default_content_preferences`, `default_waterfall`, `branding`
      operationId: patchNetworkConfig
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                default_content_preferences:
                  type: object
                default_waterfall:
                  type: object
                branding:
                  type: object
      responses:
        '200':
          description: Configuration updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: network_config
                  network_config:
                    type: object
                  parent_partner_id:
                    type: string
                    nullable: true
        '400':
          description: No valid fields to update
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ────────────────────────────────────────────
  # Quick Start
  # ────────────────────────────────────────────

  /quick-start:
    post:
      tags:
        - Partners
      summary: Quick start — register partner and first device in one call
      description: |
        Registers a new partner and creates their first device in a single API call.
        Returns API key, device credentials, SDK snippets, and configuration.

        **Rate limited:** 5 requests per hour per IP address.

        **Important:** The API key is only shown once in the response. Store it securely!
      operationId: quickStart
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - company_name
                - email
              properties:
                company_name:
                  type: string
                  description: Company or partner name
                  example: Acme Screens Inc
                email:
                  type: string
                  format: email
                  example: developer@acme.com
                device_name:
                  type: string
                  description: Name for the first device
                partner_type:
                  type: string
                  default: digital_signage
                device_location:
                  type: object
                  description: Location for the first device
                device_display:
                  type: object
                  description: Display configuration for the first device
      responses:
        '201':
          description: Partner registered and first device created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      partner:
                        type: object
                        properties:
                          partner_id:
                            type: string
                          name:
                            type: string
                          slug:
                            type: string
                          status:
                            type: string
                            example: active
                          revenue_share_percent:
                            type: number
                      credentials:
                        type: object
                        properties:
                          api_key:
                            type: string
                            description: Store securely — shown only once!
                            example: trb_partner_a1b2c3d4e5f6g7h8
                          api_key_prefix:
                            type: string
                          warning:
                            type: string
                      device:
                        type: object
                        properties:
                          device_id:
                            type: string
                          external_device_id:
                            type: string
                          fingerprint:
                            type: string
                          screen_id:
                            type: string
                          name:
                            type: string
                          status:
                            type: string
                          embed_url:
                            type: string
                            format: uri
                      integration:
                        type: object
                        properties:
                          sdk_snippet:
                            type: string
                          iframe_snippet:
                            type: string
                          embed_url:
                            type: string
                            format: uri
                          api_base:
                            type: string
                          sdk_url:
                            type: string
                          docs_url:
                            type: string
                      sdk_config:
                        type: object
                        properties:
                          ad_interval:
                            type: integer
                          max_ads_per_hour:
                            type: integer
                          image_duration:
                            type: integer
                          allowed_ad_types:
                            type: array
                            items:
                              type: string
                          sound_enabled:
                            type: boolean
                          cache_size:
                            type: integer
                      next_steps:
                        type: array
                        items:
                          type: string
        '400':
          description: Missing company_name or email
        '409':
          description: Partner with this email already exists
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ────────────────────────────────────────────
  # API Key Rotation
  # ────────────────────────────────────────────

  /api-key/rotate:
    post:
      tags:
        - Partners
      summary: Rotate API key
      description: |
        Generates a new API key with a 24-hour grace period for the old key.
        Both keys work during the grace period, after which the old key is invalidated.

        **Important:** The new API key is only shown once. Store it securely!
      operationId: rotateApiKey
      security:
        - BearerAuth: []
      responses:
        '200':
          description: New API key generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      api_key:
                        type: string
                        description: New API key — shown only once!
                        example: trb_partner_new_key_abc123
                      previous_key_expires_at:
                        type: string
                        format: date-time
                        description: When the old key stops working
                      grace_period_hours:
                        type: integer
                        example: 24
                      message:
                        type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ────────────────────────────────────────────
  # Device Command Acknowledgment
  # ────────────────────────────────────────────

  /device/{deviceId}/commands/{commandId}/ack:
    post:
      tags:
        - Devices
      summary: Acknowledge a device command
      description: |
        Acknowledges receipt and execution of a command sent to a device.
        Typically called by the SDK/device after processing a fleet command.
      operationId: ackDeviceCommand
      security: []
      parameters:
        - name: deviceId
          in: path
          required: true
          schema:
            type: string
          description: Device identifier (fingerprint or external ID)
        - name: commandId
          in: path
          required: true
          schema:
            type: string
          description: Command ID to acknowledge
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  default: acked
                  description: Acknowledgment status
                result:
                  type: object
                  description: Command execution result
      responses:
        '200':
          description: Command acknowledged
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Command acknowledged
        '403':
          description: Command does not belong to this device
        '404':
          description: Device or command not found

  # ────────────────────────────────────────────
  # Sandbox (Beta)
  # ────────────────────────────────────────────

  /sandbox/screen:
    post:
      tags:
        - Sandbox
      summary: Create a sandbox screen
      description: |
        Creates a synthetic test screen with realistic data for integration testing.
        Maximum 10 sandbox screens per partner. Sandbox data expires after 24 hours.
      operationId: createSandboxScreen
      x-beta: true
      security:
        - BearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Custom screen name
                venue_type:
                  type: string
                  description: Override venue type
      responses:
        '201':
          description: Sandbox screen created
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: sandbox_screen
                  id:
                    type: string
                    example: sandbox_screen_abc123
                  screen_id:
                    type: string
                  partner_id:
                    type: string
                  is_sandbox:
                    type: boolean
                    example: true
                  name:
                    type: string
                  fingerprint:
                    type: string
                  device_id:
                    type: string
                  status:
                    type: string
                    example: online
                  location:
                    type: object
                    properties:
                      name:
                        type: string
                      latitude:
                        type: number
                      longitude:
                        type: number
                      city:
                        type: string
                      state:
                        type: string
                      venue_type:
                        type: string
                  display_settings:
                    type: object
                  audience:
                    type: object
                    properties:
                      current_viewers:
                        type: integer
                      avg_dwell_time_sec:
                        type: number
                      demographics:
                        type: object
                  created_at:
                    type: string
                    format: date-time
                  last_heartbeat:
                    type: string
                    format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          description: Maximum 10 sandbox screens per partner

  /sandbox/event:
    post:
      tags:
        - Sandbox
      summary: Generate a test event
      description: |
        Generates a realistic test event that can be used to validate webhook integrations.
        If an SSE connection is active, the event is also delivered via SSE.

        **Supported event types:** `device.online`, `device.offline`, `impression.recorded`,
        `impression.verified`, `campaign.allocated`, `payout.processed`, `audience.spike`,
        `audience.demographics_update`, `audience.purchase_intent`, `audience.venue_busy`,
        `audience.context_update`, `venue.traffic_summary`, `auction.completed`, `bid.won`,
        `bid.lost`, `creative.performance`, `screen.anomaly_detected`
      operationId: generateSandboxEvent
      x-beta: true
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - event_type
              properties:
                event_type:
                  type: string
                  description: Event type to generate
                  example: impression.recorded
                screen_id:
                  type: string
                  description: Target sandbox screen (auto-selects first if omitted)
      responses:
        '201':
          description: Test event generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: sandbox_event
                  event_id:
                    type: string
                  event:
                    type: string
                  timestamp:
                    type: string
                    format: date-time
                  is_sandbox:
                    type: boolean
                    example: true
                  data:
                    type: object
                    description: Event payload (varies by event type)
                  screen_id:
                    type: string
                    nullable: true
                  sse_delivered:
                    type: integer
                    description: Number of SSE connections that received this event
        '400':
          description: Missing or invalid event_type
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sandbox/reset:
    post:
      tags:
        - Sandbox
      summary: Reset sandbox data
      description: Clears all sandbox screens and events for the partner.
      operationId: resetSandbox
      x-beta: true
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Sandbox reset
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: sandbox_reset
                  success:
                    type: boolean
                    example: true
                  screens_cleared:
                    type: integer
                  events_cleared:
                    type: integer
                  reset_at:
                    type: string
                    format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sandbox/status:
    get:
      tags:
        - Sandbox
      summary: Get sandbox status
      description: Returns current sandbox screens, event counts, and recent activity.
      operationId: getSandboxStatus
      x-beta: true
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Sandbox status
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: sandbox_status
                  sandbox_screens:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        status:
                          type: string
                        location:
                          type: object
                        last_heartbeat:
                          type: string
                          format: date-time
                        created_at:
                          type: string
                          format: date-time
                  total_screens:
                    type: integer
                  total_test_events:
                    type: integer
                  last_activity:
                    type: string
                    format: date-time
                    nullable: true
                  recent_events:
                    type: array
                    items:
                      type: object
                      properties:
                        event_id:
                          type: string
                        event:
                          type: string
                        timestamp:
                          type: string
                          format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ────────────────────────────────────────────
  # Audience Intelligence (Beta)
  # ────────────────────────────────────────────

  /audience/live/{screenId}:
    get:
      tags:
        - Audience Intelligence
      summary: Get live audience data for a screen
      description: |
        Returns real-time audience data including face count, attention score, mood,
        demographics, purchase intent, and crowd density. Available to all tiers.
      operationId: getLiveAudience
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Live audience data
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: audience_live
                  screen_id:
                    type: string
                  status:
                    type: string
                    enum: [live, offline]
                  audience:
                    type: object
                    nullable: true
                    description: Null when screen is offline
                    properties:
                      face_count:
                        type: integer
                      attention_score:
                        type: number
                      income_level:
                        type: string
                        nullable: true
                      mood:
                        type: string
                        nullable: true
                      lifestyle_segment:
                        type: string
                        nullable: true
                      emotional_engagement:
                        type: number
                        nullable: true
                      ad_receptivity:
                        type: number
                        nullable: true
                      dwell_time_ms:
                        type: number
                        nullable: true
                      occupancy:
                        type: string
                        nullable: true
                      noise_level:
                        type: number
                        nullable: true
                      purchase_intent:
                        type: string
                        nullable: true
                        enum: [NONE, BROWSING, CONSIDERING, COMPARING, READY_TO_BUY]
                      crowd_density:
                        type: number
                      dominant_ambience:
                        type: string
                        nullable: true
                  last_updated:
                    type: string
                    format: date-time
                    nullable: true
                  message:
                    type: string
                    description: Present when screen is offline
                  request_id:
                    type: string
        '404':
          description: Screen not found
        '401':
          $ref: '#/components/responses/Unauthorized'

  /audience/heatmap:
    get:
      tags:
        - Audience Intelligence
      summary: Get audience heatmap data
      description: |
        Returns geospatial audience heatmap data across partner screens.
        Available to all tiers.
      operationId: getAudienceHeatmap
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: screen_ids
          in: query
          schema:
            type: string
          description: Comma-separated screen IDs to include (omit for all screens)
        - name: days
          in: query
          schema:
            type: integer
            default: 14
          description: Lookback period in days
      responses:
        '200':
          description: Heatmap data
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: audience_heatmap
                  screens_included:
                    type: integer
                  lookback_days:
                    type: integer
                  cells:
                    type: integer
                  heatmap:
                    type: array
                    items:
                      type: object
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  /audience/stats:
    get:
      tags:
        - Audience Intelligence
      summary: Get audience data statistics
      description: |
        Returns high-level statistics about audience data availability for the partner's screens.
        Available to all tiers.
      operationId: getAudienceStats
      x-beta: true
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Audience stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: audience_stats
                  total_screens:
                    type: integer
                  screens_with_data:
                    type: integer
                  total_vectors:
                    type: integer
                  oldest_data:
                    type: string
                    format: date-time
                    nullable: true
                  newest_data:
                    type: string
                    format: date-time
                    nullable: true
                  data_access_tier:
                    type: string
                    enum: [basic, developer, enterprise]
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  /audience/lookalikes/{screenId}:
    get:
      tags:
        - Audience Intelligence
      summary: Find lookalike screens
      description: |
        Finds screens with similar audience profiles using vector similarity search.
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: getLookalikeScreens
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
          description: Source screen to find lookalikes for
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
          description: Maximum number of matches
        - name: country
          in: query
          schema:
            type: string
          description: Filter by country
        - name: venue_type
          in: query
          schema:
            type: string
          description: Filter by venue type
        - name: min_similarity
          in: query
          schema:
            type: number
            default: 0.7
          description: Minimum similarity score (0-1)
      responses:
        '200':
          description: Lookalike matches
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: audience_lookalike
                  source_screen_id:
                    type: string
                  matches_count:
                    type: integer
                  filters:
                    type: object
                    properties:
                      country:
                        type: string
                        nullable: true
                      venue_type:
                        type: string
                        nullable: true
                      min_similarity:
                        type: number
                  matches:
                    type: array
                    items:
                      type: object
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Screen not found

  /audience/predict/{screenId}:
    get:
      tags:
        - Audience Intelligence
      summary: Predict screen audience
      description: |
        Predicts audience composition for a specific hour and day of week
        using historical data. Requires usage-based billing (`requireBilling('data_api')`).
      operationId: predictScreenAudience
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
        - name: hour
          in: query
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 23
          description: Hour of day (UTC)
        - name: day
          in: query
          required: true
          schema:
            type: integer
            minimum: 0
            maximum: 6
          description: Day of week (0=Sunday, 6=Saturday)
        - name: lookback_days
          in: query
          schema:
            type: integer
            default: 30
          description: Historical lookback period in days
      responses:
        '200':
          description: Audience prediction
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: audience_prediction
                  screen_id:
                    type: string
                  hour_utc:
                    type: integer
                  day_of_week:
                    type: integer
                  lookback_days:
                    type: integer
                  prediction:
                    type: object
                    nullable: true
                    description: Null if insufficient historical data
                  message:
                    type: string
                    description: Present when prediction is null
                  request_id:
                    type: string
        '400':
          description: Invalid hour or day values
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Screen not found

  # ────────────────────────────────────────────
  # Audience Segments (Beta — usage-based billing)
  # ────────────────────────────────────────────

  /audience/segments:
    post:
      tags:
        - Audience Segments
      summary: Create audience segment
      description: |
        Creates a custom audience segment based on rules (demographics, venue type, geography, etc.).
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: createAudienceSegment
      x-beta: true
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - rules
              properties:
                name:
                  type: string
                  description: Segment name
                  example: High-Income Urban Millennials
                description:
                  type: string
                rules:
                  type: array
                  items:
                    type: object
                    required:
                      - field
                      - operator
                      - value
                    properties:
                      field:
                        type: string
                        description: Rule field (e.g., venue_type, country, city, state)
                      operator:
                        type: string
                        description: Comparison operator (e.g., eq, neq, in, gt, lt)
                      value:
                        description: Rule value
      responses:
        '201':
          description: Segment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudienceSegment'
        '400':
          description: Validation error (missing name, invalid rules)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '409':
          description: Duplicate segment name
        '429':
          description: Segment limit exceeded
    get:
      tags:
        - Audience Segments
      summary: List audience segments
      description: |
        Returns all audience segments for the partner.
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: listAudienceSegments
      x-beta: true
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Segment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AudienceSegment'
                  total_count:
                    type: integer
                  has_more:
                    type: boolean
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'

  /audience/segments/{segmentId}:
    get:
      tags:
        - Audience Segments
      summary: Get audience segment
      description: |
        Returns a single audience segment by ID.
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: getAudienceSegment
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Segment details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudienceSegment'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Segment not found
    patch:
      tags:
        - Audience Segments
      summary: Update audience segment
      description: |
        Partially updates an audience segment. All fields are optional.
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: updateAudienceSegment
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                rules:
                  type: array
                  items:
                    type: object
                    properties:
                      field:
                        type: string
                      operator:
                        type: string
                      value: {}
                is_active:
                  type: boolean
      responses:
        '200':
          description: Segment updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudienceSegment'
        '400':
          description: Validation error
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Segment not found
        '409':
          description: Duplicate segment name
    delete:
      tags:
        - Audience Segments
      summary: Delete audience segment
      description: |
        Permanently deletes an audience segment.
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: deleteAudienceSegment
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Segment deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: audience_segment
                  id:
                    type: string
                  deleted:
                    type: boolean
                    example: true
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Segment not found

  /audience/segments/{segmentId}/match:
    post:
      tags:
        - Audience Segments
      summary: Match screens to segment
      description: |
        Finds screens whose audience matches the segment rules.
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: matchAudienceSegment
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: own_screens_only
          in: query
          schema:
            type: string
            enum: ['true', 'false']
            default: 'false'
          description: Only match screens owned by this partner
      responses:
        '200':
          description: Matching screens
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: audience_segment
                  segment_id:
                    type: string
                  segment_name:
                    type: string
                  matched_count:
                    type: integer
                  own_screens_only:
                    type: boolean
                  matches:
                    type: array
                    items:
                      type: object
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Segment not found

  /audience/segments/{segmentId}/reach:
    post:
      tags:
        - Audience Segments
      summary: Estimate segment reach
      description: |
        Estimates the potential reach of a segment including matched screens,
        projected daily impressions, and estimated revenue.
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: estimateSegmentReach
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
        - name: own_screens_only
          in: query
          schema:
            type: string
            enum: ['true', 'false']
            default: 'false'
      responses:
        '200':
          description: Reach estimation
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: segment_reach
                  segment_id:
                    type: string
                  segment_name:
                    type: string
                  matched_screens:
                    type: integer
                  online_screens:
                    type: integer
                  estimated_daily_impressions:
                    type: integer
                  estimated_daily_cpm:
                    type: number
                  estimated_daily_revenue:
                    type: number
                  venue_breakdown:
                    type: object
                    additionalProperties:
                      type: integer
                  country_breakdown:
                    type: object
                    additionalProperties:
                      type: integer
                  has_historical_data:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Segment not found

  /audience/segments/{segmentId}/vast-tag:
    post:
      tags:
        - Audience Segments
      summary: Generate VAST tag for segment
      description: |
        Generates a VAST tag URL targeting screens that match the segment rules.
        Requires usage-based billing (`requireBilling('data_api')`).
      operationId: generateSegmentVastTag
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - creative_url
              properties:
                creative_url:
                  type: string
                  format: uri
                  description: Video creative URL
                duration:
                  type: integer
                  minimum: 5
                  maximum: 120
                  default: 15
                  description: Video duration in seconds
                click_through:
                  type: string
                  format: uri
                  description: Click-through landing page URL
                own_screens_only:
                  type: boolean
                  default: false
      responses:
        '200':
          description: VAST tag generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: segment_vast_tag
                  segment_id:
                    type: string
                  segment_name:
                    type: string
                  vast_tag_url:
                    type: string
                    format: uri
                  targeted_screens:
                    type: integer
                  screen_ids:
                    type: array
                    items:
                      type: string
                  cust_params:
                    type: array
                    items:
                      type: string
                  creative:
                    type: object
                    properties:
                      url:
                        type: string
                      duration:
                        type: integer
                      click_through:
                        type: string
                        nullable: true
        '400':
          description: Missing creative_url
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Segment not found

  # ────────────────────────────────────────────
  # Intent Catalog (Beta)
  # ────────────────────────────────────────────

  /intent/categories:
    get:
      tags:
        - Intent Catalog
      summary: List intent categories
      description: |
        Returns the catalog of purchase intent categories with CPM multipliers
        and matching thresholds.
      operationId: listIntentCategories
      x-beta: true
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Intent categories
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          example: active_purchase_intent
                        name:
                          type: string
                          example: Active Purchase Intent
                        description:
                          type: string
                        premium_multiplier:
                          type: number
                          description: CPM multiplier applied when this intent is detected
                          example: 5.0
                        min_intent_score:
                          type: number
                          description: Minimum intent score to match this category
                          example: 0.8
                        purchase_stages:
                          type: array
                          items:
                            type: string
                            enum: [READY_TO_BUY, COMPARING, CONSIDERING, BROWSING]
                  count:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /intent/categories/{categoryId}:
    get:
      tags:
        - Intent Catalog
      summary: Get intent category details
      description: Returns detailed information about a specific intent category including triggers.
      operationId: getIntentCategory
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: categoryId
          in: path
          required: true
          schema:
            type: string
          example: active_purchase_intent
      responses:
        '200':
          description: Intent category details
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: intent_category
                  id:
                    type: string
                  name:
                    type: string
                  description:
                    type: string
                  triggers:
                    type: array
                    items:
                      type: string
                  premium_multiplier:
                    type: number
                  min_intent_score:
                    type: number
                  purchase_stages:
                    type: array
                    items:
                      type: string
                  iab_segments:
                    type: array
                    items:
                      type: object
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Intent category not found

  /intent/pricing/{screenId}:
    get:
      tags:
        - Intent Catalog
      summary: Get intent-based pricing for a screen
      description: |
        Returns real-time intent-based pricing for a specific screen based on
        current audience signals (speech, emotion, behavior). Includes the detected
        intent category, purchase stage, and premium CPM.
      operationId: getIntentPricing
      x-beta: true
      security:
        - BearerAuth: []
      parameters:
        - name: screenId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Intent pricing data
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: intent_pricing
                  screen_id:
                    type: string
                  current_intent:
                    type: object
                    properties:
                      score:
                        type: number
                        description: Intent confidence score (0-1)
                      purchase_stage:
                        type: string
                        enum: [NONE, BROWSING, CONSIDERING, COMPARING, READY_TO_BUY]
                      categories:
                        type: array
                        items:
                          type: string
                      should_trigger:
                        type: boolean
                        description: Whether an ad swap should be triggered
                  category_match:
                    type: object
                    nullable: true
                    properties:
                      category_id:
                        type: string
                      category_name:
                        type: string
                      premium_multiplier:
                        type: number
                      intent_score:
                        type: number
                      purchase_stage:
                        type: string
                  pricing:
                    type: object
                    properties:
                      base_cpm:
                        type: number
                      premium_multiplier:
                        type: number
                      intent_cpm:
                        type: number
                        description: Effective CPM after intent premium
                      intent_premium:
                        type: number
                        description: Additional CPM from intent detection
                      category_id:
                        type: string
                      category_name:
                        type: string
        '400':
          description: Invalid screen ID
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Screen not found

  # ────────────────────────────────────────────
  # Billing & Pricing
  # ────────────────────────────────────────────

  /pricing:
    get:
      tags:
        - Billing
      summary: Get machine-readable pricing
      description: |
        Returns full pricing information for all Trillboards products including
        graduated usage tiers, free tier thresholds, committed-use discounts,
        and billing setup URLs. No authentication required — designed for
        programmatic price discovery by AI agents and DSPs.

        Response is cached for 1 hour (Cache-Control: public, max-age=3600).
      operationId: getPricing
      security: []
      responses:
        '200':
          description: Pricing information
          headers:
            Cache-Control:
              schema:
                type: string
                example: "public, max-age=3600"
            Link:
              schema:
                type: string
                example: '</.well-known/adagents.json>; rel="adcp-discovery"'
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: pricing
                  model:
                    type: string
                    example: usage_based
                  currency:
                    type: string
                    example: usd
                  description:
                    type: string
                    example: "Pay-per-use with generous free tiers. No subscriptions, no commitments."
                  products:
                    type: object
                    description: Pricing details per product
                    properties:
                      partner_platform:
                        type: object
                        properties:
                          name:
                            type: string
                          description:
                            type: string
                          pricing:
                            type: string
                          revenue_share:
                            type: object
                            properties:
                              earner:
                                type: number
                              platform:
                                type: number
                              distributor:
                                type: number
                      data_api:
                        type: object
                        properties:
                          name:
                            type: string
                          description:
                            type: string
                          unit:
                            type: string
                          tiers:
                            type: array
                            items:
                              type: object
                              properties:
                                up_to:
                                  type: integer
                                  nullable: true
                                  description: "null means unlimited (final tier)"
                                unit_price_usd:
                                  type: number
                          notes:
                            type: string
                      proof_of_play:
                        type: object
                        properties:
                          name:
                            type: string
                          description:
                            type: string
                          unit:
                            type: string
                          tiers:
                            type: array
                            items:
                              type: object
                              properties:
                                up_to:
                                  type: integer
                                  nullable: true
                                unit_price_usd:
                                  type: number
                          notes:
                            type: string
                      attribution:
                        type: object
                        properties:
                          name:
                            type: string
                          description:
                            type: string
                          meters:
                            type: object
                            additionalProperties:
                              type: object
                              properties:
                                unit_price_usd:
                                  type: number
                                per_unit:
                                  type: string
                                first_campaign_free:
                                  type: boolean
                      data_marketplace:
                        type: object
                        properties:
                          name:
                            type: string
                          description:
                            type: string
                          meters:
                            type: object
                            additionalProperties:
                              type: object
                              properties:
                                unit_price_usd:
                                  type: number
                                per_unit:
                                  type: string
                                free_quota_per_month:
                                  type: integer
                          revenue_share:
                            type: object
                          notes:
                            type: string
                      programmatic:
                        type: object
                        properties:
                          name:
                            type: string
                          description:
                            type: string
                          pricing:
                            type: string
                      fein_edge_ai:
                        type: object
                        properties:
                          name:
                            type: string
                          description:
                            type: string
                          pricing:
                            type: string
                  committed_use:
                    type: object
                    description: Volume discount tiers for prepaid credits
                    additionalProperties:
                      type: object
                      properties:
                        price_usd:
                          type: number
                        credits_usd:
                          type: number
                        discount_pct:
                          type: number
                  free_tiers:
                    type: object
                    description: Free tier limits per product and meter
                  billing_setup_url:
                    type: string
                    example: /v1/partner/billing/setup
                  agent_register_url:
                    type: string
                    example: /v1/partner/agent/register
                  request_id:
                    type: string
              example:
                object: pricing
                model: usage_based
                currency: usd
                description: "Pay-per-use with generous free tiers. No subscriptions, no commitments."
                request_id: req_abc123

  /billing/setup:
    post:
      tags:
        - Billing
      summary: Set up usage-based billing
      description: |
        Attaches a Stripe payment method to your partner account and activates
        pay-per-use billing. Required after exceeding free tier limits.

        After setup, usage beyond free tiers is metered and invoiced monthly.
        You can also purchase committed-use credits for volume discounts.
      operationId: setupBilling
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - payment_method_id
              properties:
                payment_method_id:
                  type: string
                  description: Stripe payment method token obtained from Stripe.js or Stripe Elements
                  example: pm_1OxABC123def456
            example:
              payment_method_id: pm_1OxABC123def456
      responses:
        '201':
          description: Billing activated
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: billing_setup
                  stripe_customer_id:
                    type: string
                    example: cus_ABC123
                  billing_status:
                    type: string
                    example: active
                  message:
                    type: string
                    example: "Billing is now active. Usage beyond free tiers will be invoiced monthly."
                  request_id:
                    type: string
        '400':
          description: Invalid or missing payment method
        '401':
          $ref: '#/components/responses/Unauthorized'

  /billing/usage-summary:
    get:
      tags:
        - Billing
      summary: Get current billing period usage
      description: |
        Returns usage summary for the current billing period including per-product
        breakdown, free tier consumption, paid usage, credit balance, and total cost.
      operationId: getUsageSummary
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Usage summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: usage_summary
                  billing_status:
                    type: string
                    enum: [none, active, suspended]
                    description: Current billing status
                  credit_balance_cents:
                    type: integer
                    description: Remaining prepaid credit balance in cents
                    example: 62500
                  period_start:
                    type: string
                    format: date-time
                    description: Start of current billing period
                  period_end:
                    type: string
                    format: date-time
                    description: End of current billing period
                  products:
                    type: object
                    description: Per-product usage breakdown
                    additionalProperties:
                      type: object
                      properties:
                        meters:
                          type: object
                          additionalProperties:
                            type: object
                            properties:
                              usage:
                                type: integer
                              free_quota:
                                type: integer
                              billable:
                                type: integer
                              cost_cents:
                                type: integer
                  total_cost_cents:
                    type: integer
                    description: Total cost in cents for current period
                    example: 4500
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  /billing/credits:
    post:
      tags:
        - Billing
      summary: Purchase committed-use credits
      description: |
        Immediately charges the partner's payment method and adds credits at a discount.

        Available tiers:
        | Tier | Price | Credits | Discount |
        |------|-------|---------|----------|
        | tier_500 | $500 | $625 | 25% |
        | tier_2000 | $2,000 | $3,100 | 55% |
        | tier_5000 | $5,000 | $10,000 | 100% |

        Requires an active billing setup (payment method attached).
      operationId: purchaseCredits
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tier
              properties:
                tier:
                  type: string
                  enum: [tier_500, tier_2000, tier_5000]
                  description: Credit purchase tier
            example:
              tier: tier_2000
      responses:
        '201':
          description: Credits purchased
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: credit_purchase
                  payment_intent_id:
                    type: string
                    description: Stripe PaymentIntent ID
                    example: pi_ABC123
                  charged_cents:
                    type: integer
                    description: Amount charged in cents
                    example: 200000
                  credits_added_cents:
                    type: integer
                    description: Credits added to balance in cents
                    example: 310000
                  discount_pct:
                    type: number
                    description: Discount percentage applied
                    example: 55
                  request_id:
                    type: string
        '400':
          description: Invalid tier or billing not set up
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'

  /billing/credits/checkout:
    post:
      tags:
        - Billing
      summary: Create Stripe Checkout session for credits
      description: |
        Creates a Stripe Checkout session for purchasing committed-use credits.
        Redirect the user to the returned checkout_url to complete payment.
        Credits are automatically applied after successful payment via webhook.
      operationId: createCreditCheckout
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tier
              properties:
                tier:
                  type: string
                  enum: [tier_500, tier_2000, tier_5000]
                success_url:
                  type: string
                  format: uri
                  description: URL to redirect after successful payment
                cancel_url:
                  type: string
                  format: uri
                  description: URL to redirect if payment is cancelled
            example:
              tier: tier_500
              success_url: "https://example.com/billing/success"
              cancel_url: "https://example.com/billing/cancel"
      responses:
        '201':
          description: Checkout session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: checkout_session
                  checkout_url:
                    type: string
                    format: uri
                    description: Stripe Checkout URL — redirect user here
                    example: "https://checkout.stripe.com/c/pay/cs_test_abc123"
                  session_id:
                    type: string
                    description: Stripe Checkout session ID
                    example: cs_test_abc123
                  message:
                    type: string
                    example: "Redirect the user to checkout_url to complete the purchase."
                  request_id:
                    type: string
        '400':
          description: Invalid tier
        '401':
          $ref: '#/components/responses/Unauthorized'

  /agent/register:
    post:
      tags:
        - Agent Registration
      summary: Register an AI agent
      description: |
        Self-service registration for AI agents and automated systems. Returns an API key,
        MCP endpoint, sandbox screens for testing, and free tier information.

        No authentication required — designed for fully autonomous agent onboarding.
        Rate limited to 5 registrations per hour per IP.
      operationId: registerAgent
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - agent_type
                - name
                - email
              properties:
                agent_type:
                  type: string
                  enum: [dsp_agent, ssp_agent, trading_desk, analytics_platform, developer]
                  description: Type of agent being registered
                name:
                  type: string
                  description: Agent or company name
                  example: "My DSP Agent"
                email:
                  type: string
                  format: email
                  description: Contact email
                  example: agent@example.com
                company:
                  type: string
                  description: Company name (optional)
                description:
                  type: string
                  description: Brief description of the agent's purpose
            example:
              agent_type: dsp_agent
              name: "My DSP Agent"
              email: agent@example.com
              company: "Acme DSP"
              description: "Automated DOOH media buying agent"
      responses:
        '201':
          description: Agent registered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: agent_registration
                  partner_id:
                    type: string
                    description: Unique partner ID
                  api_key:
                    type: string
                    description: API key for authentication (display once — save it!)
                    example: trb_partner_xxxxx
                  mcp_endpoint:
                    type: string
                    example: "https://api.trillboards.com/mcp/"
                  mcp_transport:
                    type: string
                    example: streamable-http
                  tools_url:
                    type: string
                    example: "https://api.trillboards.com/mcp/"
                  pricing_url:
                    type: string
                    example: "https://api.trillboards.com/v1/partner/pricing"
                  docs_url:
                    type: string
                    example: "https://api.trillboards.com/developer"
                  sandbox:
                    type: object
                    properties:
                      screens:
                        type: array
                        items:
                          type: object
                          properties:
                            device_id:
                              type: string
                            venue_type:
                              type: string
                            name:
                              type: string
                      note:
                        type: string
                  free_tiers:
                    type: object
                    description: Free tier limits per product
                  available_tools:
                    type: array
                    items:
                      type: string
                    description: MCP tools available to this agent
                  agent_type:
                    type: string
                  next_steps:
                    type: array
                    items:
                      type: string
                    description: Guidance for getting started
                  request_id:
                    type: string
        '400':
          description: Missing required fields or invalid agent_type
        '429':
          $ref: '#/components/responses/TooManyRequests'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        API key authentication. Include your API key as a Bearer token:
        ```
        Authorization: Bearer trb_partner_xxxxx
        ```

    UserJWT:
      type: http
      scheme: bearer
      description: |
        User JWT authentication for earner-to-partner upgrade.
        Used only for the `/v2/earner/upgrade-to-partner` endpoint.
        This is the same JWT token used for the Earner Portal.

  schemas:
    Partner:
      type: object
      properties:
        partner_id:
          type: string
        name:
          type: string
        slug:
          type: string
        status:
          type: string
          enum: [pending, active, suspended, terminated]
        partner_type:
          type: string
        integration_tier:
          type: string
        sdk_config:
          $ref: '#/components/schemas/SDKConfig'
        revenue_share_percent:
          type: number
        stats:
          $ref: '#/components/schemas/PartnerStats'
        stripe_account_status:
          type: string

    SDKConfig:
      type: object
      properties:
        ad_interval:
          type: integer
          description: Seconds between ads
        max_ads_per_hour:
          type: integer
        image_duration:
          type: integer
          description: Seconds to display image ads
        allowed_ad_types:
          type: array
          items:
            type: string
        sound_enabled:
          type: boolean
        cache_size:
          type: integer
        overlay_button_text:
          type: string

    PartnerStats:
      type: object
      properties:
        total_devices:
          type: integer
        active_devices:
          type: integer
        total_impressions:
          type: integer
        total_revenue_cents:
          type: integer
        pending_payout_cents:
          type: integer
        last_impression_at:
          type: string
          format: date-time

    Device:
      type: object
      properties:
        device_id:
          type: string
        external_device_id:
          type: string
        fingerprint:
          type: string
        name:
          type: string
        device_type:
          type: string
        status:
          type: string
          enum: [pending, active, offline, suspended]
        display:
          type: object
          description: |
            Screen display specification. `width`/`height` are pixel resolution;
            `width_in`/`height_in` are physical inches. Pixel-only inputs are
            NOT recorded as physical size — pass `width_in`/`height_in` (most
            accurate) or `ppi` so the server can derive inches from pixels.
          properties:
            width:
              type: integer
              description: Display resolution width in pixels
            height:
              type: integer
              description: Display resolution height in pixels
            orientation:
              type: string
            ppi:
              type: number
              description: Pixels per inch (required to derive physical size from width/height)
            width_in:
              type: number
              description: Physical screen width in inches
            height_in:
              type: number
              description: Physical screen height in inches
        location:
          type: object
        venue:
          type: object
        enrichment:
          type: object
        screen_id:
          type: string
        embed_url:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DeviceSummary:
      type: object
      properties:
        device_id:
          type: string
        external_device_id:
          type: string
        fingerprint:
          type: string
        name:
          type: string
        status:
          type: string
        device_type:
          type: string
        display:
          type: object
        location:
          type: object
        venue:
          type: object
        enrichment:
          type: object
        screen_id:
          type: string
        embed_url:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    PartnerHeartbeatRequest:
      type: object
      description: |
        Optional heartbeat payload. Nested telemetry objects are preferred; legacy flat keys
        are still accepted for backward compatibility.
      properties:
        telemetry:
          $ref: '#/components/schemas/PartnerHeartbeatTelemetry'
        metadata:
          type: object
          additionalProperties: true
          description: Alternate namespace for telemetry keys (legacy support).
        sdk:
          type: object
          properties:
            version:
              type: string
            ima_supported:
              type: boolean
            ima_integration:
              type: string
              enum: [android_native, html5_webview, disabled, unknown]
            ima_sdk_version:
              type: string
            android_api_level:
              type: integer
            webview_chromium_major:
              type: integer
            desugaring_enabled:
              type: boolean
        device:
          type: object
          properties:
            os:
              type: string
            osv:
              type: string
            make:
              type: string
            model:
              type: string
            android_api_level:
              type: integer
            webview_chromium_major:
              type: integer
        network:
          type: object
          properties:
            connectionType:
              type: string
            effectiveType:
              type: string
            bandwidthMbps:
              type: number
            downlinkMbps:
              type: number
            latencyRtt:
              type: number
            rtt:
              type: number
            saveData:
              type: boolean
        ad:
          type: object
          properties:
            last_vast_request_at:
              type: string
              format: date-time
            last_vast_response_at:
              type: string
              format: date-time
            proof_of_play_enabled:
              type: boolean
        power_state:
          type: string
          enum: [on, standby, off, unknown]
        input_source:
          type: string
        brightness:
          type: number
        volume:
          type: number
        muted:
          type: boolean
        network_strength_dbm:
          type: number
        uptime_seconds:
          type: number
        temperature_c:
          type: number
        agent_status:
          type: string
        agent_version:
          type: string
        sdk_version:
          type: string
        ima_supported:
          type: boolean
        ima_integration:
          type: string
          enum: [android_native, html5_webview, disabled, unknown]
        ima_sdk_version:
          type: string
        ima_support_reason:
          type: string
        desugaring_enabled:
          type: boolean
        android_api_level:
          type: integer
        webview_chromium_major:
          type: integer
        last_ima_error_code:
          type: integer
        last_vast_error_code:
          type: integer
        last_vast_request_at:
          type: string
          format: date-time
        last_vast_response_at:
          type: string
          format: date-time
        proof_of_play_enabled:
          type: boolean

    PartnerHeartbeatTelemetry:
      type: object
      properties:
        powerState:
          type: string
          enum: [on, standby, off, unknown]
        inputSource:
          type: string
        brightness:
          type: number
        volume:
          type: number
        muted:
          type: boolean
        networkStrengthDbm:
          type: number
        uptimeSeconds:
          type: number
        temperatureC:
          type: number
        agentStatus:
          type: string
        os:
          type: string
        osv:
          type: string
        osVersion:
          type: string
        make:
          type: string
        model:
          type: string
        agentVersion:
          type: string
        connectionType:
          type: string
        ima_integration:
          type: string
          enum: [android_native, html5_webview, disabled, unknown]
        ima_sdk_version:
          type: string
        ima_supported:
          type: boolean
        ima_support_reason:
          type: string
        desugaring_enabled:
          type: boolean
        android_api_level:
          type: integer
        webview_chromium_major:
          type: integer
        last_ima_error_code:
          type: integer
        last_vast_error_code:
          type: integer
        last_vast_request_at:
          type: string
          format: date-time
        last_vast_response_at:
          type: string
          format: date-time
        proof_of_play_enabled:
          type: boolean
        networkConnectionType:
          type: string
        networkEffectiveType:
          type: string
        bandwidthMbps:
          type: number
        downlinkMbps:
          type: number
        latencyRtt:
          type: number
        rtt:
          type: number
        saveData:
          type: boolean
      additionalProperties: true

    PartnerAdDeliveryProfile:
      type: object
      properties:
        mode:
          type: string
          enum: [ima_sdk, vast_fallback]
        reason:
          type: string
        auto_recovery_enabled:
          type: boolean
        ima_integration:
          type: string
          enum: [android_native, html5_webview, disabled, unknown]
        android_api_level:
          type: integer
          nullable: true
        webview_chromium_major:
          type: integer
          nullable: true
        notes:
          type: array
          items:
            type: string

    PartnerHeartbeatCommand:
      type: object
      properties:
        id:
          type: string
        command:
          type: string
          description: Command normalized for partner SDK compatibility.
        payload:
          type: object
          additionalProperties: true
        queued_at:
          type: string
          format: date-time

    PartnerHeartbeatData:
      type: object
      properties:
        status:
          type: string
          enum: [online, offline]
        last_seen_at:
          type: string
          format: date-time
        ad_delivery_profile:
          $ref: '#/components/schemas/PartnerAdDeliveryProfile'
        commands:
          type: array
          items:
            $ref: '#/components/schemas/PartnerHeartbeatCommand'

    PartnerHeartbeatResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/PartnerHeartbeatData'

    # ---------------------------------------------------------------------------
    # Device heartbeat — agent-core sensing payload (functional fields only).
    #
    # The shape mirrors `validation/deviceHeartbeatSchemas.js` Zod schema and
    # Kotlin `HeartbeatPayload.kt` for ONLY the fields verified populated in
    # 30-day production telemetry. Stub fields (Phase 6 UWB / Auracast /
    # Channel Sounding, MDM enrollment, native sensors, CSI) are excluded —
    # see the audit comment at the top of `deviceHeartbeatSchemas.js` for the
    # full exclusion list and rationale.
    # ---------------------------------------------------------------------------

    PartnerDeviceHeartbeatBody:
      type: object
      additionalProperties: true
      description: |
        Heartbeat payload from agent-core / agent-core-lite Android partner
        SDKs. Only includes fields verified populated in production
        `signal_observations` (last 30 days). Stub fields wired in Kotlin but
        never observed in CH are intentionally excluded; re-add only after
        telemetry confirms >1% population over 7 days.
      properties:
        schemaVersion:
          type: integer
          minimum: 1
          maximum: 255
          description: Heartbeat schema version (1=baseline, 2=nativeSensorSnapshot)
        fingerprint:
          type: string
        screenId:
          type: string
          nullable: true
        status:
          type: string
        metadata:
          type: object
          additionalProperties: true
        capabilities:
          $ref: '#/components/schemas/DeviceCapabilityPayload'
        advertisingId:
          type: string
          nullable: true
        advertisingIdType:
          type: string
          nullable: true
          enum: [gaid, idfa, null]
        limitAdTracking:
          type: boolean
          nullable: true
        nearbyBleDevices:
          type: array
          maxItems: 50
          nullable: true
          items:
            $ref: '#/components/schemas/BleScanResultData'
          description: Capped at 50 devices (RSSI-sorted, strongest first).
        bleDeviceCount:
          type: integer
          minimum: 0
        wifiBssidHash:
          type: string
          nullable: true
        wifiSsidHash:
          type: string
          nullable: true
        gatewayIpHash:
          type: string
          nullable: true
        nearbyWifiNetworks:
          type: array
          maxItems: 50
          nullable: true
          items:
            $ref: '#/components/schemas/WifiScanResult'
        wifiNetworkCount:
          type: integer
          minimum: 0
        wifiEnvironment:
          $ref: '#/components/schemas/WifiEnvironmentSnapshot'
        discoveredNetworkDevices:
          type: array
          maxItems: 50
          nullable: true
          items:
            $ref: '#/components/schemas/MdnsNetworkDevice'
        networkDeviceCount:
          type: integer
          minimum: 0
        ssdpDevices:
          type: array
          maxItems: 50
          nullable: true
          items:
            $ref: '#/components/schemas/SsdpDeviceInfo'
        httpProbes:
          type: array
          maxItems: 50
          nullable: true
          items:
            $ref: '#/components/schemas/HttpProbeResult'
        skipReasonCounts:
          type: object
          description: Per-source skip-reason counter map. Shape `{source: {reason: count}}`.
          additionalProperties:
            type: object
            additionalProperties:
              type: integer
              minimum: 0

    BleScanResultData:
      type: object
      additionalProperties: true
      required: [rawAddress, rssi]
      properties:
        rawAddress:
          type: string
        rssi:
          type: integer
          minimum: -120
          maximum: 0
        deviceType:
          type: integer
          nullable: true
        manufacturerCompanyId:
          type: integer
          nullable: true
        appleContinuitySubtype:
          type: integer
          nullable: true
        stableManufacturerPayloadHex:
          type: string
          nullable: true
        serviceUuids:
          type: array
          nullable: true
          items:
            type: string
        txPowerDbm:
          type: integer
          nullable: true
        ibeaconUuid:
          type: string
          nullable: true
        ibeaconMajor:
          type: integer
          nullable: true
        ibeaconMinor:
          type: integer
          nullable: true

    WifiScanResult:
      type: object
      additionalProperties: true
      required: [rawBssid, signalStrengthDbm, frequencyMhz]
      properties:
        rawBssid:
          type: string
        signalStrengthDbm:
          type: integer
          minimum: -120
          maximum: 0
        frequencyMhz:
          type: integer
          minimum: 2400
          maximum: 7200
        channelWidthMhz:
          type: integer
          nullable: true

    WifiEnvironmentSnapshot:
      type: object
      additionalProperties: true
      nullable: true
      properties:
        networkCount:
          type: integer
          nullable: true
        connectedSignalDbm:
          type: integer
          nullable: true
        connectedFrequencyMhz:
          type: integer
          nullable: true
        connectedChannelWidthMhz:
          type: integer
          nullable: true
        connectedLinkSpeedMbps:
          type: integer
          nullable: true
        frequencyBand:
          type: string
          nullable: true
        channelCongestionRatio:
          type: number
          nullable: true
        rssiVariance:
          type: number
          nullable: true
        uniqueBssidCount:
          type: integer
          nullable: true
        medianSignalDbm:
          type: integer
          nullable: true
        signalSpreadDbm:
          type: integer
          nullable: true
        scanTimestampMs:
          type: integer
          nullable: true

    MdnsNetworkDevice:
      type: object
      additionalProperties: true
      required: [serviceType, instanceName]
      properties:
        serviceType:
          type: string
        instanceName:
          type: string
        host:
          type: string
          nullable: true
        port:
          type: integer
          nullable: true
        mdnsModel:
          type: string
          nullable: true
        mdnsVendor:
          type: string
          nullable: true
        mdnsSoftwareVersion:
          type: string
          nullable: true

    SsdpDeviceInfo:
      type: object
      additionalProperties: true
      required: [location, st]
      properties:
        location:
          type: string
        server:
          type: string
          nullable: true
        friendlyName:
          type: string
          nullable: true
        manufacturer:
          type: string
          nullable: true
        modelName:
          type: string
          nullable: true
        udn:
          type: string
          nullable: true
        st:
          type: string

    HttpProbeResult:
      type: object
      additionalProperties: true
      required: [host]
      properties:
        host:
          type: string
        port:
          type: integer
          nullable: true
        server:
          type: string
          nullable: true

    DeviceCapabilityPayload:
      type: object
      additionalProperties: true
      properties:
        powerControl:
          type: object
          additionalProperties: true
          nullable: true
          properties:
            cec: {type: boolean, nullable: true}
            wol: {type: boolean, nullable: true}
            softBlackout: {type: boolean, nullable: true}
        inputControl:
          type: object
          additionalProperties: true
          nullable: true
          properties:
            hdmi: {type: boolean, nullable: true}
            appSwitch: {type: boolean, nullable: true}
        audioControl:
          type: object
          additionalProperties: true
          nullable: true
          properties:
            volume: {type: boolean, nullable: true}
            mute: {type: boolean, nullable: true}
        environmentSensors:
          type: object
          additionalProperties: true
          nullable: true
          properties:
            ambientLight: {type: boolean, nullable: true}
            temperature: {type: boolean, nullable: true}
        audienceSensing:
          type: object
          additionalProperties: true
          nullable: true
          properties:
            cameraAvailable: {type: boolean, nullable: true}
            cameraType: {type: string, nullable: true}
            cameraCount: {type: integer, nullable: true}
            microphoneAvailable: {type: boolean, nullable: true}
            sensingMode: {type: string, nullable: true}
            cameraHealth:
              type: object
              additionalProperties: true
              nullable: true
              properties:
                state: {type: string, nullable: true}
                lastFrameAtMs: {type: integer, nullable: true}
                retryCount: {type: integer, nullable: true}
                lastFailureClass: {type: string, nullable: true}
                lastFailureMessage: {type: string, nullable: true}
        managementCapabilities:
          type: object
          additionalProperties: true
          nullable: true
          properties:
            restart: {type: boolean, nullable: true}
            reboot: {type: boolean, nullable: true}
            screenshot: {type: boolean, nullable: true}
            clearCache: {type: boolean, nullable: true}
            kioskMode: {type: boolean, nullable: true}
            installApk: {type: boolean, nullable: true}
            wipe: {type: boolean, nullable: true}
        mlCapabilities:
          type: object
          additionalProperties: true
          nullable: true
          properties:
            chipsetVendor: {type: string, nullable: true}
            chipsetName: {type: string, nullable: true}
            totalRamMb: {type: integer, nullable: true}
            availableRamMb: {type: integer, nullable: true}
            gpuName: {type: string, nullable: true}
            hasNpu: {type: boolean, nullable: true}
            npuName: {type: string, nullable: true}
            gpuDelegateSupported: {type: boolean, nullable: true}
            nnapiSupported: {type: boolean, nullable: true}
            recommendedModelTier: {type: string, nullable: true}
            maxVlmSizeMb: {type: integer, nullable: true}
            cpuAbi: {type: string, nullable: true}
            osApiLevel: {type: integer, nullable: true}
            screenWidthPx: {type: integer, nullable: true}
            screenHeightPx: {type: integer, nullable: true}
            densityDpi: {type: integer, nullable: true}
        wifiCsiSensing:
          type: object
          additionalProperties: true
          nullable: true
          properties:
            csiAvailable: {type: boolean, nullable: true}
            csiNodeCount: {type: integer, nullable: true}
            csiHardwareType: {type: string, nullable: true}
        isDeviceOwner:
          type: boolean
          nullable: true
        agentType:
          type: string
          nullable: true
        agentVersion:
          type: string
          nullable: true
        capabilitiesVersion:
          type: integer
          nullable: true

    PartnerSafeHeartbeatTelemetry:
      type: object
      additionalProperties: true
      description: |
        Partner-safe subset of device telemetry. Strips MDM compliance,
        Vertex embeddings, identity-correction internals, partner identifiers,
        and Mongo `_id` so partners only see what they need to decide
        whether a device is healthy enough to receive ads.
      properties:
        powerState: {type: string, nullable: true}
        onlineStatus:
          type: string
          enum: [online, offline, standby, unknown]
          nullable: true
        agentVersion: {type: string, nullable: true}
        agentStatus: {type: string, nullable: true}
        networkStrengthDbm:
          type: integer
          minimum: -120
          maximum: 0
          nullable: true
        bandwidthMbps: {type: number, nullable: true}
        latencyRtt: {type: integer, nullable: true}
        connectionType: {type: string, nullable: true}
        effectiveType: {type: string, nullable: true}
        brightness: {type: number, nullable: true}
        volume: {type: number, nullable: true}
        muted: {type: boolean, nullable: true}
        temperatureC: {type: number, nullable: true}
        uptimeSeconds: {type: integer, nullable: true}
        visibilityState: {type: string, nullable: true}
        consecutiveAdFailures: {type: integer, nullable: true}
        make: {type: string, nullable: true}
        model: {type: string, nullable: true}
        os: {type: string, nullable: true}
        osv: {type: string, nullable: true}
        ua: {type: string, nullable: true}

    PartnerHeartbeatMeasurementMetadata:
      type: object
      additionalProperties: true
      properties:
        receivedAt:
          type: string
          format: date-time
          nullable: true
        cachedAt:
          type: string
          format: date-time
          nullable: true
        ageSeconds:
          type: number
          nullable: true
        source:
          type: string
          enum: [redis_twin, pg_fallback, unknown]

    PartnerHeartbeatQueryData:
      type: object
      additionalProperties: true
      required: [deviceId]
      properties:
        deviceId:
          type: string
        lastHeartbeatAt:
          type: string
          format: date-time
          nullable: true
        stalenessSeconds:
          type: number
          nullable: true
        telemetry:
          $ref: '#/components/schemas/PartnerSafeHeartbeatTelemetry'
        adDeliveryProfile:
          $ref: '#/components/schemas/PartnerAdDeliveryProfile'
        measurementMetadata:
          $ref: '#/components/schemas/PartnerHeartbeatMeasurementMetadata'

    PartnerHeartbeatQueryResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/PartnerHeartbeatQueryData'

    Ad:
      type: object
      properties:
        id:
          type: string
          description: Advertisement ID
        allocation_id:
          type: string
        type:
          type: string
          enum: [video, image]
        url:
          type: string
          format: uri
          description: Media URL (CDN)
        duration:
          type: integer
          description: Duration in seconds
        title:
          type: string
        impression_url:
          type: string
          format: uri
          description: Call this to record impression
        pixel_url:
          type: string
          format: uri
          description: 1x1 tracking pixel URL

    AdSettings:
      type: object
      properties:
        ad_interval:
          type: integer
        max_ads_per_hour:
          type: integer
        image_duration:
          type: integer
        sound_enabled:
          type: boolean
        overlay_button_text:
          type: string

    HeaderBiddingSettings:
      type: object
      nullable: true
      properties:
        enabled:
          type: boolean
        vast_tag_url:
          type: string
          format: uri
        variant_name:
          type: string
        allocation:
          type: number
        provider:
          type: string
        request_mode:
          type: string
        force_request:
          type: boolean
        min_interval_seconds:
          type: integer
        prebid:
          type: object
          nullable: true

    PartnerDefaultStreamCreate:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
        stream_type:
          type: string
          enum: [local, youtube, uploaded, images, twitch, google_slides, canva, social_wall, generic_iframe]
        youtube_url:
          type: string
          format: uri
        video_url:
          type: string
          format: uri
        images:
          type: array
          items:
            type: string
            format: uri
        image_duration:
          type: integer
          description: Seconds per image
        twitch_url:
          type: string
          format: uri
        google_slides_url:
          type: string
          format: uri
        canva_url:
          type: string
          format: uri
        social_wall_url:
          type: string
          format: uri
        generic_iframe_url:
          type: string
          format: uri
        embed_url:
          type: string
          format: uri
        iframe_config:
          type: object
          properties:
            auto_advance:
              type: boolean
            slide_delay_ms:
              type: integer
            refresh_interval_seconds:
              type: integer
            sandbox_flags:
              type: string
            allow_flags:
              type: string
        duration_seconds:
          type: number
          description: Optional explicit duration for iframe-based streams

    PartnerDefaultStreamUpdate:
      allOf:
        - $ref: '#/components/schemas/PartnerDefaultStreamCreate'

    PartnerDefaultStream:
      type: object
      properties:
        _id:
          type: string
        screenId:
          type: string
        partner_id:
          type: string
        name:
          type: string
        description:
          type: string
        stream_type:
          type: string
        youtube_url:
          type: string
        video_url:
          type: string
        images:
          type: array
          items:
            type: string
        image_duration:
          type: integer
        twitch_url:
          type: string
        google_slides_url:
          type: string
        canva_url:
          type: string
        social_wall_url:
          type: string
        generic_iframe_url:
          type: string
        embed_url:
          type: string
        iframe_config:
          type: object
        duration_seconds:
          type: number
        playback_order:
          type: integer
        is_currently_playing:
          type: boolean
        is_active:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    PartnerLBarDisplayPreferences:
      type: object
      properties:
        display_mode:
          type: string
          enum: [fullscreen, l-bar]
        l_bar_position:
          type: string
          enum: [bottom, right]
        l_bar_size:
          type: integer
        l_bar_ads_enabled:
          type: boolean
        l_bar_ad_interval:
          type: integer
        l_bar_ad_duration:
          type: integer
        ad_surface_mode:
          type: string
          enum: [fullscreen, lbar, pip, all]
        pip_settings:
          type: object
          properties:
            enabled:
              type: boolean
            position:
              type: string
              enum: [top-left, top-right, bottom-left, bottom-right]
            size_ratio:
              type: number
            aspect_ratio:
              type: number
            margin:
              type: integer

    PartnerLBarContent:
      type: object
      properties:
        enabled:
          type: boolean
        business_name:
          type: string
        store_hours:
          type: object
        contact_info:
          type: object
        social_media:
          type: object
        current_promotion:
          type: string
        live_updates:
          type: object
        display_settings:
          type: object
        custom_messages:
          type: array
          items:
            type: string

    PartnerLBarSettings:
      type: object
      properties:
        display_preferences:
          $ref: '#/components/schemas/PartnerLBarDisplayPreferences'
        l_bar_content:
          $ref: '#/components/schemas/PartnerLBarContent'
        ad_surfaces:
          type: object
        playback_policy:
          type: object

    PartnerSelfPromoCreate:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        description:
          type: string
        image_url:
          type: string
          format: uri
        video_url:
          type: string
          format: uri
        campaign_link:
          type: string
          format: uri
        screen_ids:
          type: array
          items:
            type: string
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date

    PartnerSelfPromoAd:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string
        description:
          type: string
        campaign_link:
          type: string
          format: uri
        photos:
          type: array
          items:
            type: string
        video:
          type: string
        status:
          type: string
        createdAt:
          type: string
          format: date-time

    PartnerSelfPromoPlacement:
      type: object
      properties:
        screen:
          type: object
          properties:
            _id:
              type: string
            name:
              type: string
        ads:
          type: array
          items:
            type: object
            properties:
              allocation_id:
                type: string
              status:
                type: string
              start_date:
                type: string
                format: date
              end_date:
                type: string
                format: date
              advertisement:
                $ref: '#/components/schemas/PartnerSelfPromoAd'

    Webhook:
      type: object
      properties:
        webhook_id:
          type: string
          description: Unique webhook identifier
          example: wh_m4k7x9_a1b2c3d4e5f6
        url:
          type: string
          format: uri
          description: Webhook endpoint URL
          example: https://example.com/webhooks/trillboards
        events:
          type: array
          items:
            type: string
            enum:
              - device.online
              - device.offline
              - impression.recorded
              - campaign.allocated
              - payout.processed
          description: Subscribed event types
        status:
          type: string
          enum: [active, inactive]
          description: Webhook status
        success_count:
          type: integer
          description: Total successful deliveries
        failure_count:
          type: integer
          description: Total failed deliveries
        last_triggered_at:
          type: string
          format: date-time
          description: Last time webhook was triggered
        created_at:
          type: string
          format: date-time
          description: When webhook was created

    WebhookDelivery:
      type: object
      properties:
        id:
          type: string
          description: Delivery record ID
        event_type:
          type: string
          description: Event that triggered this delivery
        status:
          type: string
          enum: [pending, success, failed, retrying]
          description: Delivery status
        http_status:
          type: integer
          description: HTTP status code from endpoint
        response_time_ms:
          type: integer
          description: Response time in milliseconds
        error_message:
          type: string
          description: Error message if failed
        retry_count:
          type: integer
          description: Number of retry attempts
        attempted_at:
          type: string
          format: date-time
          description: When delivery was first attempted
        delivered_at:
          type: string
          format: date-time
          description: When delivery succeeded

    WebhookEvent:
      type: object
      description: |
        Standard webhook event payload sent to your endpoint.
        All events follow this structure.
      properties:
        event:
          type: string
          description: Event type
          example: device.online
        timestamp:
          type: string
          format: date-time
          description: When event occurred
        data:
          type: object
          description: Event-specific data
      example:
        event: "device.online"
        timestamp: "2026-01-01T02:30:00.000Z"
        data:
          device_id: "507f1f77bcf86cd799439011"
          fingerprint: "P_a1b2c3d4e5f6"
          external_device_id: "machine-001"
          name: "Mall Kiosk #42"

    # ==========================================
    # CONTENT PREFERENCES SCHEMAS
    # ==========================================

    ContentPreset:
      type: object
      properties:
        id:
          type: string
          description: Unique preset identifier
        name:
          type: string
          description: Human-readable preset name
        description:
          type: string
          description: Preset description
        blocked_categories:
          type: array
          items:
            type: string
          description: Categories blocked by this preset
        blocked_advertisers:
          type: array
          items:
            type: string
          description: Advertiser IDs blocked
        min_ad_rating:
          type: string
          enum: [G, PG, PG-13, R]
          description: Minimum ad rating allowed
        preferences:
          type: object
          description: Full preference object applied by this preset

    ContentPreferencesInput:
      type: object
      properties:
        blocked_categories:
          type: array
          items:
            type: string
          description: Ad categories to block (e.g., "alcohol", "gambling", "adult")
        blocked_advertisers:
          type: array
          items:
            type: string
          description: Specific advertiser IDs to block
        min_ad_rating:
          type: string
          enum: [G, PG, PG-13, R]
          description: Minimum ad content rating
        preferred_categories:
          type: array
          items:
            type: string
          description: Preferred ad categories (prioritized in auctions)
        max_ad_frequency:
          type: integer
          description: Max times same ad can show per hour
        language_preference:
          type: string
          description: Preferred ad language (ISO 639-1 code)

    ContentPreferencesResponse:
      type: object
      properties:
        object:
          type: string
          example: content_preferences
        screen_id:
          type: string
        blocked_categories:
          type: array
          items:
            type: string
        blocked_advertisers:
          type: array
          items:
            type: string
        min_ad_rating:
          type: string
        preferred_categories:
          type: array
          items:
            type: string
        max_ad_frequency:
          type: integer
        language_preference:
          type: string
        updated_at:
          type: string
          format: date-time
        request_id:
          type: string

    # ==========================================
    # DISPLAY SETTINGS SCHEMAS
    # ==========================================

    DisplaySettingsInput:
      type: object
      properties:
        orientation:
          type: string
          enum: [landscape, portrait, auto]
          description: Screen orientation
        brightness:
          type: integer
          minimum: 0
          maximum: 100
          description: Screen brightness percentage
        volume:
          type: integer
          minimum: 0
          maximum: 100
          description: Audio volume percentage
        mute:
          type: boolean
          description: Mute audio
        display_mode:
          type: string
          enum: [fullscreen, l-bar, split]
          description: Ad display mode
        transition_effect:
          type: string
          enum: [fade, slide, none]
          description: Transition effect between ads
        ad_duration_default:
          type: integer
          description: Default ad duration in seconds
        idle_content:
          type: string
          enum: [default_stream, black, logo]
          description: What to show when no ads
        qr_code_position:
          type: string
          enum: [bottom-left, bottom-right, top-left, top-right, none]
          description: QR code position on screen

    DisplaySettingsResponse:
      type: object
      properties:
        object:
          type: string
          example: display_settings
        screen_id:
          type: string
        orientation:
          type: string
        brightness:
          type: integer
        volume:
          type: integer
        mute:
          type: boolean
        display_mode:
          type: string
        transition_effect:
          type: string
        ad_duration_default:
          type: integer
        idle_content:
          type: string
        qr_code_position:
          type: string
        ad_surfaces:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              position:
                type: string
              width_percent:
                type: number
              height_percent:
                type: number
        playback_policy:
          type: object
          properties:
            max_duration_seconds:
              type: integer
            min_break_between_ads:
              type: integer
            allow_skip:
              type: boolean
        updated_at:
          type: string
          format: date-time
        request_id:
          type: string

    # ==========================================
    # PROGRAMMATIC SETTINGS SCHEMAS
    # ==========================================

    ProgrammaticSettingsInput:
      type: object
      properties:
        enabled:
          type: boolean
          description: Enable programmatic ads
        google_ima:
          type: object
          properties:
            enabled:
              type: boolean
            ad_tag_url:
              type: string
            max_duration:
              type: integer
        prebid:
          type: object
          properties:
            enabled:
              type: boolean
            bidders:
              type: array
              items:
                type: object
                properties:
                  name:
                    type: string
                  params:
                    type: object
        floor_price_cpm:
          type: number
          description: Minimum CPM in dollars
        priority:
          type: string
          enum: [direct_first, programmatic_first, highest_bid]
          description: Ad source priority

    ProgrammaticSettingsResponse:
      type: object
      properties:
        object:
          type: string
          example: programmatic_settings
        screen_id:
          type: string
        enabled:
          type: boolean
        google_ima:
          type: object
          properties:
            enabled:
              type: boolean
            ad_tag_url:
              type: string
            max_duration:
              type: integer
        prebid:
          type: object
          properties:
            enabled:
              type: boolean
            bidders:
              type: array
              items:
                type: object
        floor_price_cpm:
          type: number
        priority:
          type: string
        stats:
          type: object
          properties:
            impressions_today:
              type: integer
            fill_rate_percent:
              type: number
            avg_cpm:
              type: number
        updated_at:
          type: string
          format: date-time
        request_id:
          type: string

    # ==========================================
    # BATCH RESULT SCHEMA
    # ==========================================

    BatchResult:
      type: object
      properties:
        object:
          type: string
          example: batch_result
        total_requested:
          type: integer
          description: Number of screens requested to update
        updated_count:
          type: integer
          description: Number successfully updated
        failed_count:
          type: integer
          description: Number that failed
        updated_screens:
          type: array
          items:
            type: string
          description: IDs of successfully updated screens
        failed_screens:
          type: array
          items:
            type: object
            properties:
              screen_id:
                type: string
              error:
                type: string
        request_id:
          type: string

    # ==========================================
    # AUDIENCE DATA SCHEMAS
    # ==========================================

    AudienceMetricsResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
        data:
          type: object
          properties:
            screen_id:
              type: string
            screen_name:
              type: string
            live:
              type: object
              properties:
                avgFaceCount:
                  type: number
                  description: Average faces detected
                maxFaces:
                  type: integer
                  description: Maximum faces detected in period
                avgAttention:
                  type: number
                  description: Average attention score (0-1)
                incomeLevel:
                  type: string
                  enum: [unknown, low, medium, high, premium]
                mood:
                  type: string
                screenEngagement:
                  type: string
            aggregated:
              type: object
              properties:
                incomeProfile:
                  type: object
                  properties:
                    low:
                      type: number
                    medium:
                      type: number
                    high:
                      type: number
                    premium:
                      type: number
                groupComposition:
                  type: object
                avgFaceCount:
                  type: number
                avgAttention:
                  type: number
                avgDwellMs:
                  type: number
            history:
              type: array
              items:
                type: object
            period:
              type: string
            timestamp:
              type: string
              format: date-time

    AudienceExportResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
        data:
          type: array
          items:
            type: object
            properties:
              screen_id:
                type: string
              screen_name:
                type: string
              location:
                type: string
              country:
                type: string
              period:
                type: object
                properties:
                  start:
                    type: string
                    format: date-time
                  end:
                    type: string
                    format: date-time
              impressions:
                type: integer
              audience:
                type: object
                properties:
                  avg_face_count:
                    type: number
                  avg_attention:
                    type: number
                  demographics:
                    type: object
                  income_profile:
                    type: object
                  lifestyle_segments:
                    type: array
                    items:
                      type: string
        summary:
          type: object
          properties:
            total_screens:
              type: integer
            total_impressions:
              type: integer
            period:
              type: object
            aggregation:
              type: string

    AudienceLoyaltyFeaturesRow:
      type: object
      description: |
        Per-device cross-day loyalty feature row emitted by the
        `audience_loyalty_features` dataset. Keyed by a partner-scoped
        pseudonymous ID — the same physical device yields a different ID
        for each partner because the HMAC salt is partner-specific
        (`partner_salt` on `platform_api_keys`).
      required:
        - device_id_pseudonymous
        - manufacturer_class
        - first_seen_relative_days
        - last_seen_relative_days
        - visit_count_in_window
        - distinct_days_in_window
      properties:
        device_id_pseudonymous:
          type: string
          description: 'Partner-scoped HMAC of cross-day device key (16 hex chars + tb_ prefix). Different partners get different IDs for the same physical device — preventing collusion.'
          example: 'tb_8a2f7c1e4d5b6a92'
        manufacturer_class:
          type: string
          enum: ['premium', 'mainstream', 'audio_iot', 'other']
        manufacturer_label:
          type: string
          example: 'Apple'
        first_seen_relative_days:
          type: integer
          description: 'Days since this device was first observed at any of the partner-scoped screens.'
        last_seen_relative_days:
          type: integer
        visit_count_in_window:
          type: integer
        distinct_days_in_window:
          type: integer
        distinct_venues_in_window:
          type: integer
        venue_archetypes:
          type: array
          items:
            type: string
          description: 'List of venue_category labels (Bar, Restaurant, etc.) the device was observed at.'
        mean_dwell_seconds:
          type: number
        deep_dwell_observations:
          type: integer
        top_hour_of_day:
          type: integer
          minimum: 0
          maximum: 23
        top_day_of_week:
          type: integer
          minimum: 0
          maximum: 6
        demographic_inference:
          type: object
          properties:
            audience_group_composition:
              type: string
            audience_intent_stage:
              type: string
            audience_attire_archetype:
              type: string
            age_bracket:
              type: string
            gender_distribution:
              type: object
            attention_score_avg:
              type: number
        raw_identifiers:
          type: object
          description: 'Raw cross-day-stable identifiers when available (NULL for randomized phone MACs without Continuity).'
          properties:
            ble_mac:
              type: string
              nullable: true
            ble_mac_oui:
              type: string
              nullable: true
              description: 'First 3 bytes of BLE MAC (IEEE OUI vendor lookup).'
            ble_mac_oui_vendor:
              type: string
              nullable: true
            mdns_hostname:
              type: string
              nullable: true
            wifi_bssid:
              type: string
              nullable: true
            manufacturer_company_id:
              type: integer
              nullable: true
        _match_disclosure:
          type: object
          properties:
            primary_signal_source:
              type: string
              enum: ['ble_continuity', 'ble_mac_stable', 'mdns_vendor_model', 'fixed_mac']
            match_confidence:
              type: string
              enum: ['high', 'medium', 'low']

    DataExportSummaryResponse:
      type: object
      description: |
        Per-dataset record counts + date ranges for the partner's screens.
        Returned by `GET /data-export/summary`.
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            datasets:
              type: object
              description: Map of dataset name to record summary
              additionalProperties:
                type: object
                properties:
                  total_records:
                    type: integer
                  earliest:
                    type: string
                    format: date-time
                    nullable: true
                  latest:
                    type: string
                    format: date-time
                    nullable: true
            screen_count:
              type: integer
            lookback_days:
              type: integer
            k_anonymity_threshold:
              type: integer

    DemographicsResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
        data:
          type: object
          properties:
            screen_id:
              type: string
            screen_name:
              type: string
            period:
              type: string
            demographics:
              type: object
              properties:
                age_distribution:
                  type: object
                  properties:
                    18-25:
                      type: number
                    26-35:
                      type: number
                    36-50:
                      type: number
                    50+:
                      type: number
                gender_split:
                  type: object
                  properties:
                    male:
                      type: number
                    female:
                      type: number
                income_profile:
                  type: object
                  properties:
                    low:
                      type: number
                    medium:
                      type: number
                    high:
                      type: number
                    premium:
                      type: number
                lifestyle_segments:
                  type: array
                  items:
                    type: string
                group_composition:
                  type: object
                  properties:
                    solo:
                      type: number
                    couples:
                      type: number
                    families:
                      type: number
                    friends:
                      type: number
                    work_groups:
                      type: number
            data_quality:
              type: object
              properties:
                sample_size:
                  type: integer
                confidence:
                  type: number
            timestamp:
              type: string
              format: date-time

    AudienceAnalyticsResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
        data:
          type: object
          properties:
            period:
              type: string
            overview:
              type: object
              properties:
                total_screens:
                  type: integer
                active_screens:
                  type: integer
                total_impressions:
                  type: integer
                avg_impressions_per_screen:
                  type: integer
            audience:
              type: object
              properties:
                avg_face_count:
                  type: number
                avg_attention:
                  type: number
                screens_with_data:
                  type: integer
            trends:
              type: object
              properties:
                impressions_trend:
                  type: string
                  enum: [increasing, stable, decreasing]
                audience_trend:
                  type: string
                  enum: [increasing, stable, decreasing]
            timestamp:
              type: string
              format: date-time

    # ==========================================
    # DASHBOARD SCHEMAS
    # ==========================================

    DashboardResponse:
      type: object
      properties:
        partner:
          type: object
          properties:
            name:
              type: string
            slug:
              type: string
            status:
              type: string
              enum: [pending, active, suspended, terminated]
            created_at:
              type: string
              format: date-time
        stats:
          type: object
          properties:
            total_screens:
              type: integer
            active_screens:
              type: integer
            offline_screens:
              type: integer
            pending_screens:
              type: integer
            total_impressions_today:
              type: integer
            total_impressions_week:
              type: integer
            total_impressions_month:
              type: integer
        earnings:
          type: object
          properties:
            total_earned_cents:
              type: integer
            pending_payout_cents:
              type: integer
            available_payout_cents:
              type: integer
            next_payout_date:
              type: string
              format: date
            revenue_share_percent:
              type: number
        top_screens:
          type: array
          items:
            type: object
            properties:
              screen_id:
                type: string
              name:
                type: string
              location:
                type: string
              impressions_today:
                type: integer
        recent_activity:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
              screen_id:
                type: string
              ad_id:
                type: string
              timestamp:
                type: string
                format: date-time
        stripe_status:
          type: object
          properties:
            account_status:
              type: string
            connect_account_id:
              type: string

    ScreenAnalyticsResponse:
      type: object
      properties:
        screen:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            location:
              type: string
            status:
              type: string
            online_status:
              type: string
        impressions:
          type: object
          properties:
            total:
              type: integer
            daily_average:
              type: integer
            trend_percent:
              type: integer
              description: Percentage change compared to previous period
        timeline:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              impressions:
                type: integer
              avg_duration_ms:
                type: integer
        period:
          type: object
          properties:
            days:
              type: integer
            start:
              type: string
              format: date-time
            end:
              type: string
              format: date-time

    # ==========================================
    # EARNINGS SCHEMAS
    # ==========================================

    EarningsResponse:
      type: object
      properties:
        total_lifetime_cents:
          type: integer
          description: Total earnings since partner started
        this_month_cents:
          type: integer
          description: Estimated earnings for current month
        available_for_payout_cents:
          type: integer
          description: Amount available for immediate payout
        pending_confirmation_cents:
          type: integer
          description: Programmatic earnings pending Net-60 confirmation
        next_payout_estimate:
          type: object
          properties:
            amount_cents:
              type: integer
            date:
              type: string
              format: date
            stripe_status:
              type: string
        breakdown:
          type: object
          properties:
            direct_campaigns:
              type: integer
            programmatic_google:
              type: integer
            programmatic_floodgates:
              type: integer
        payout_threshold_cents:
          type: integer
          description: Minimum amount required for payout
        revenue_share_percent:
          type: number
          description: Partner's revenue share percentage

    Transaction:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [earning, payout]
        amount_cents:
          type: integer
        source:
          type: string
          description: Source of the transaction (e.g., impressions, payout)
        impressions:
          type: integer
          description: Number of impressions (for earning transactions)
        date:
          type: string
          format: date
        created_at:
          type: string
          format: date-time
        stripe_transfer_id:
          type: string
          description: Stripe transfer ID (for payout transactions)

    Pagination:
      type: object
      properties:
        total:
          type: integer
          description: Total number of items
        limit:
          type: integer
          description: Items per page
        offset:
          type: integer
          description: Current offset
        has_more:
          type: boolean
          description: Whether more items exist

    EarningsBreakdownResponse:
      type: object
      properties:
        summary:
          type: object
          properties:
            total_cents:
              type: integer
            total_impressions:
              type: integer
            direct_cents:
              type: integer
            programmatic_cents:
              type: integer
        breakdown:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
                description: Date or screen_id depending on group_by
              screen_id:
                type: string
                description: Screen ID (when grouped by screen)
              impressions:
                type: integer
              earnings_cents:
                type: integer
        pending_programmatic:
          type: object
          properties:
            estimated_cents:
              type: integer
            confirmation_date:
              type: string
              format: date
        period:
          type: object
          properties:
            start:
              type: string
              format: date-time
            end:
              type: string
              format: date-time
            group_by:
              type: string
              enum: [day, week, screen]

    PayoutResponse:
      type: object
      properties:
        payout_id:
          type: string
        amount_cents:
          type: integer
        status:
          type: string
          enum: [pending, processing, completed, failed]
        estimated_arrival:
          type: string
          format: date
        stripe_transfer_id:
          type: string
          nullable: true

    # ==========================================
    # TEAM MANAGEMENT SCHEMAS
    # ==========================================

    TeamMember:
      type: object
      properties:
        membership_id:
          type: string
          description: Unique membership record ID
        user_id:
          type: string
          description: User ID (null if pending invitation)
        email:
          type: string
          format: email
        name:
          type: string
        photo:
          type: string
          nullable: true
        role:
          type: string
          enum: [owner, admin, viewer]
        status:
          type: string
          enum: [active, pending, revoked]
        invited_at:
          type: string
          format: date-time
        accepted_at:
          type: string
          format: date-time
          nullable: true
        invited_by:
          type: object
          nullable: true
          properties:
            name:
              type: string
            email:
              type: string
        last_portal_access:
          type: string
          format: date-time
          nullable: true
        permissions:
          $ref: '#/components/schemas/RolePermissions'

    RolePermissions:
      type: object
      description: |
        Permission flags for each role. Owners have all permissions,
        admins have operational permissions, viewers have read-only access.
      properties:
        view_screens:
          type: boolean
          description: Can view screen list
        view_screen_status:
          type: boolean
          description: Can view online/offline status
        view_analytics:
          type: boolean
          description: Can view impression analytics
        view_impressions:
          type: boolean
          description: Can view impression data
        view_earnings:
          type: boolean
          description: Can view earnings/revenue
        view_payouts:
          type: boolean
          description: Can view payout history
        request_payouts:
          type: boolean
          description: Can request payouts
        edit_screens:
          type: boolean
          description: Can edit screen settings
        manage_team:
          type: boolean
          description: Can invite/remove team members
        rotate_api_keys:
          type: boolean
          description: Can rotate API keys
        configure_webhooks:
          type: boolean
          description: Can configure webhooks

    TeamInviteResponse:
      type: object
      properties:
        membership_id:
          type: string
        email:
          type: string
          format: email
        role:
          type: string
          enum: [admin, viewer]
        status:
          type: string
          enum: [pending, active]
        expires_at:
          type: string
          format: date-time
          description: When the invitation expires (7 days from creation)

    AudienceSegment:
      type: object
      properties:
        object:
          type: string
          example: audience_segment
        id:
          type: string
        name:
          type: string
        description:
          type: string
        rules:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              operator:
                type: string
              value: {}
        is_active:
          type: boolean
        last_matched_count:
          type: integer
        last_matched_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        request_id:
          type: string

    Error:
      type: object
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
        error:
          type: string

    # BEGIN-GENERATED audience-fein
    # AUTO-GENERATED FROM sidecars — DO NOT EDIT BY HAND. Run: node scripts/openapi/generate-from-sidecars.js
    AudienceAttentionPrediction:
      type: object
      description: Backend-derived predictive attention signal + buyer-grade attentive-seconds. 1:1 with audience_metrics. Never null by contract; placeholder on row 1.
      properties:
        predicted_attention_next_30s:
          type: string
          enum:
            - high
            - medium
            - low
            - none
          description: predicted_attention_next_30s enum {high,medium,low,none}. 'none' is below 'low'; fires when rolling history converges to zero or ≥N ignored windows.
          x-units: enum
          x-since: 2026-05-11
          example: high
          x-sor-ref: docs/schemas/audience-fein/audience_attention_prediction.yaml
        predicted_gaze_continuation_probability:
          type: number
          minimum: 0
          maximum: 1
          description: NUMERIC(4,3). Deprecated 2026-05-12. Replaced by `attentive_seconds_observed`.
          x-units: ratio
          x-since: 2026-05-11
          example: 0.5
          x-sor-ref: docs/schemas/audience-fein/audience_attention_prediction.yaml
          deprecated: true
          x-deprecated-at: 2026-05-12
          x-replacement: attentive_seconds_observed
        predicted_disengagement_imminent:
          type: boolean
          description: predicted_disengagement_imminent drives 'rotate creative early' decisions. Partial index on this column keeps imminent-event queries fast.
          x-units: boolean
          x-since: 2026-05-11
          example: false
          x-sor-ref: docs/schemas/audience-fein/audience_attention_prediction.yaml
        prediction_evidence:
          type: string
          minLength: 1
          maxLength: 280
          x-units: text
          x-since: 2026-05-11
          example: sample
          x-sor-ref: docs/schemas/audience-fein/audience_attention_prediction.yaml
        attentive_seconds_observed:
          type: number
          minimum: 0
          maximum: 999.999
          description: "attentive_seconds_observed NUMERIC(6,3): SUM(per_face[].gaze_seconds WHERE gaze_seconds >= 1.0) — OpenOOH 2024 §4.2 attentive-impression threshold. NEVER NULL; 0.000 = no viewer met threshold. NUMERIC(6,3)."
          x-since: 2026-05-12
          example: 500
          x-sor-ref: docs/schemas/audience-fein/audience_attention_prediction.yaml
        attentive_face_count:
          type: integer
          format: int32
          minimum: 0
          maximum: 1000
          description: "attentive_face_count SMALLINT: distinct viewers meeting OpenOOH ≥1s threshold. Buyer-side denominator for CPMA. NEVER NULL."
          x-units: count
          x-since: 2026-05-12
          example: 0
          x-sor-ref: docs/schemas/audience-fein/audience_attention_prediction.yaml
        attentive_seconds_predicted_15s:
          type: number
          minimum: 0
          maximum: 999.999
          description: "attentive_seconds_predicted_15s NUMERIC(6,3): expected next-15s attentive seconds. Per-screen survival history × attention-weight; bootstrap = linear extrapolation. No global τ. NUMERIC(6,3)."
          x-since: 2026-05-12
          example: 500
          x-sor-ref: docs/schemas/audience-fein/audience_attention_prediction.yaml
      required:
        - predicted_attention_next_30s
        - predicted_disengagement_imminent
        - prediction_evidence
        - attentive_seconds_observed
        - attentive_face_count
        - attentive_seconds_predicted_15s
      x-sor-ref: docs/schemas/audience-fein/audience_attention_prediction.yaml
      x-since: 2026-05-11
    AudienceSocialAttention:
      type: object
      description: Buyer-grade social amplification signal (5 typed columns). Cloud Vertex Gemini producer. 1 row per audience observation when cascade is observable.
      properties:
        social_amplification_factor:
          type: number
          minimum: 0
          maximum: 1
          description: NUMERIC(4,3).
          x-units: ratio
          x-since: 2026-05-11
          example: 0.5
          x-sor-ref: docs/schemas/audience-fein/audience_social_attention.yaml
        cascade_depth:
          type: integer
          format: int32
          minimum: 0
          maximum: 10
          x-units: count
          x-since: 2026-05-11
          example: 0
          x-sor-ref: docs/schemas/audience-fein/audience_social_attention.yaml
        viral_attention_score:
          type: number
          minimum: 0
          maximum: 1
          description: viral_attention_score is the headline number Viant/T-Vision license. Composite of social_amplification_factor × cascade_depth × emotional engagement. NUMERIC(4,3).
          x-units: ratio
          x-since: 2026-05-11
          example: 0.5
          x-sor-ref: docs/schemas/audience-fein/audience_social_attention.yaml
        primary_attention_anchor_face_index:
          type: integer
          format: int32
          minimum: 0
          nullable: true
          description: "All 5 columns NOT NULL except primary_attention_anchor_face_index (only legit NULL: no anchor or solo scene)."
          x-units: count
          x-since: 2026-05-11
          example: 0
          x-sor-ref: docs/schemas/audience-fein/audience_social_attention.yaml
        amplification_evidence:
          type: string
          minLength: 1
          maxLength: 280
          x-units: text
          x-since: 2026-05-11
          example: sample
          x-sor-ref: docs/schemas/audience-fein/audience_social_attention.yaml
      required:
        - social_amplification_factor
        - cascade_depth
        - viral_attention_score
        - amplification_evidence
      x-sor-ref: docs/schemas/audience-fein/audience_social_attention.yaml
      x-since: 2026-05-11
    # END-GENERATED audience-fein
  headers:
    X-RateLimit-Limit:
      description: The maximum number of requests allowed in the current time window
      schema:
        type: integer
        example: 200
    X-RateLimit-Remaining:
      description: The number of requests remaining in the current time window
      schema:
        type: integer
        example: 199
    X-RateLimit-Reset:
      description: Unix timestamp when the rate limit window resets
      schema:
        type: integer
        example: 1704067260
    RateLimit-Policy:
      description: IETF draft-7 rate limit policy (limit, remaining, reset combined)
      schema:
        type: string
        example: "200;w=60"

  responses:
    InternalError:
      description: |
        Canonical Trillboards error envelope for unexpected server errors.
        Surfaces the request_id so the caller can quote it to support for
        log correlation.
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error:
                type: object
                required: [type, code, message, request_id]
                properties:
                  type: { type: string, enum: [api_error] }
                  code: { type: string, example: internal_error }
                  message:
                    type: string
                    example: "An internal error occurred. Please contact support with request_id req_..."
                  request_id:
                    type: string
                    pattern: '^req_[a-f0-9]{32}$'
                  doc_url:
                    type: string
                    format: uri
                    example: https://api.trillboards.com/docs/errors#internal_error
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            message: Missing required fields
    Unauthorized:
      description: Unauthorized - Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            message: API key required
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            message: Device not found
    PaymentRequired:
      description: Free tier exhausted — add a payment method to continue
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  message:
                    type: string
                    example: "Free tier exhausted for data_api. Add a payment method to continue."
                  code:
                    type: string
                    enum: [payment_required]
                  type:
                    type: string
                    enum: [billing_error]
                  product:
                    type: string
                    description: Product that exhausted its free tier
                    enum: [data_api, proof_of_play, attribution, data_marketplace]
                  usage:
                    type: object
                    properties:
                      current:
                        type: integer
                        description: Current usage count this billing period
                        example: 12000
                      free_limit:
                        type: integer
                        description: Free tier limit per month
                        example: 10000
                      exceeded_by:
                        type: integer
                        description: How many units over the free limit
                        example: 2000
                  pricing:
                    type: object
                    description: Graduated pricing tiers for this product
                    nullable: true
                  billing_setup_url:
                    type: string
                    example: /v1/partner/billing/setup
                  pricing_url:
                    type: string
                    example: /v1/partner/pricing
                  help:
                    type: string
                    example: "POST /v1/partner/billing/setup with a Stripe payment method token to enable pay-per-use billing."
              request_id:
                type: string
                example: req_abc123
    TooManyRequests:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
        Retry-After:
          description: Seconds to wait before retrying
          schema:
            type: integer
            example: 60
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                example: false
              message:
                type: string
                example: API rate limit exceeded. Maximum 200 requests per minute.
              retryAfter:
                type: integer
                example: 60
