openapi: 3.1.0
info:
  title: BaPlay API
  version: 1.0.0
  description: Resolve, persist, stream, embed, analyze, and manage video through BaPlay.
  license:
    name: MIT
    identifier: MIT
servers:
  - url: /api/v1
tags:
  - name: System
  - name: Authentication
  - name: Videos
  - name: Subtitles
  - name: Analytics
  - name: Advertising
  - name: API Keys
  - name: Webhooks
paths:
  /health:
    get:
      tags: [System]
      operationId: health
      responses:
        '200':
          description: Service health
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SuccessEnvelope'}
  /auth/register:
    post:
      tags: [Authentication]
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email, password, password_confirmation]
              properties:
                name: {type: string, maxLength: 255}
                email: {type: string, format: email}
                password: {type: string, minLength: 8, maxLength: 72}
                password_confirmation: {type: string}
                device_name: {type: string, maxLength: 100}
      responses:
        '201': {$ref: '#/components/responses/AuthSuccess'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /auth/login:
    post:
      tags: [Authentication]
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: {type: string, format: email}
                password: {type: string}
                device_name: {type: string}
      responses:
        '200': {$ref: '#/components/responses/AuthSuccess'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /auth/logout:
    post:
      tags: [Authentication]
      operationId: logout
      security: [{sanctum: []}]
      responses:
        '200':
          description: Current token revoked
        '401': {$ref: '#/components/responses/Unauthorized'}
  /auth/me:
    get:
      tags: [Authentication]
      operationId: currentUser
      security: [{sanctum: []}, {apiKey: []}]
      responses:
        '200':
          description: Authenticated user
          content:
            application/json:
              schema:
                allOf:
                  - {$ref: '#/components/schemas/SuccessEnvelope'}
                  - type: object
                    properties:
                      data: {$ref: '#/components/schemas/User'}
        '401': {$ref: '#/components/responses/Unauthorized'}
  /embed:
    post:
      tags: [Videos]
      operationId: resolveEmbed
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ResolveVideoRequest'}
      responses:
        '200': {$ref: '#/components/responses/VideoSuccess'}
        '400': {$ref: '#/components/responses/Error'}
        '403': {$ref: '#/components/responses/Error'}
        '404': {$ref: '#/components/responses/Error'}
        '422': {$ref: '#/components/responses/Error'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '502': {$ref: '#/components/responses/Error'}
  /videos:
    get:
      tags: [Videos]
      operationId: listVideos
      security: [{sanctum: []}, {apiKey: []}]
      parameters:
        - in: query
          name: page
          schema: {type: integer, minimum: 1}
        - in: query
          name: per_page
          schema: {type: integer, minimum: 1, maximum: 100}
      responses:
        '200':
          description: Owned videos
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
    post:
      tags: [Videos]
      operationId: createVideo
      security: [{}, {sanctum: []}, {apiKey: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ResolveVideoRequest'}
      responses:
        '200': {$ref: '#/components/responses/VideoSuccess'}
        '201': {$ref: '#/components/responses/VideoSuccess'}
        '400': {$ref: '#/components/responses/Error'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /videos/{id}:
    parameters:
      - {$ref: '#/components/parameters/VideoId'}
    get:
      tags: [Videos]
      operationId: getVideo
      responses:
        '200': {$ref: '#/components/responses/VideoSuccess'}
        '404': {$ref: '#/components/responses/NotFound'}
    delete:
      tags: [Videos]
      operationId: deleteVideo
      security: [{sanctum: []}, {apiKey: []}]
      responses:
        '204': {description: Video deleted}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
  /videos/{id}/status:
    get:
      tags: [Videos]
      operationId: videoStatus
      parameters: [{$ref: '#/components/parameters/VideoId'}]
      responses:
        '200': {description: Video status}
        '404': {$ref: '#/components/responses/NotFound'}
  /videos/{id}/sources:
    get:
      tags: [Videos]
      operationId: videoSources
      parameters: [{$ref: '#/components/parameters/VideoId'}]
      responses:
        '200': {description: Playable sources}
        '404': {$ref: '#/components/responses/NotFound'}
  /videos/{id}/subtitles:
    parameters: [{$ref: '#/components/parameters/VideoId'}]
    get:
      tags: [Subtitles]
      operationId: listSubtitles
      responses:
        '200': {description: Public subtitle tracks}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      tags: [Subtitles]
      operationId: uploadSubtitle
      security: [{sanctum: []}, {apiKey: []}]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [subtitle, language]
              properties:
                subtitle: {type: string, format: binary}
                language: {type: string, maxLength: 10}
                label: {type: string, maxLength: 100}
                is_default: {type: boolean}
      responses:
        '201': {description: Subtitle created}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '422': {$ref: '#/components/responses/ValidationError'}
  /videos/{id}/subtitles/{subtitleId}:
    delete:
      tags: [Subtitles]
      operationId: deleteSubtitle
      security: [{sanctum: []}, {apiKey: []}]
      parameters:
        - {$ref: '#/components/parameters/VideoId'}
        - in: path
          name: subtitleId
          required: true
          schema: {type: integer}
      responses:
        '204': {description: Subtitle deleted}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
  /analytics/event:
    post:
      tags: [Analytics]
      operationId: recordAnalyticsEvent
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AnalyticsEventInput'}
      responses:
        '202':
          description: Event accepted or intentionally suppressed by privacy settings
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SuccessEnvelope'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /analytics/overview:
    get:
      tags: [Analytics]
      operationId: analyticsOverview
      security: [{sanctum: []}, {apiKey: []}]
      parameters:
        - {$ref: '#/components/parameters/AnalyticsFrom'}
        - {$ref: '#/components/parameters/AnalyticsTo'}
      responses:
        '200':
          description: Analytics totals, daily series, and top owned videos
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '422': {$ref: '#/components/responses/ValidationError'}
  /analytics/videos/{id}:
    get:
      tags: [Analytics]
      operationId: videoAnalytics
      security: [{sanctum: []}, {apiKey: []}]
      parameters:
        - {$ref: '#/components/parameters/VideoId'}
        - {$ref: '#/components/parameters/AnalyticsFrom'}
        - {$ref: '#/components/parameters/AnalyticsTo'}
      responses:
        '200':
          description: Owned video totals, daily series, geography, devices, and current viewers
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
  /ads/vast/{videoId}:
    get:
      tags: [Advertising]
      operationId: getVideoAds
      parameters:
        - in: path
          name: videoId
          required: true
          schema: {type: string, minLength: 12, maxLength: 12}
        - in: query
          name: session_id
          schema: {type: string, minLength: 8, maxLength: 64}
        - in: query
          name: referrer_domain
          schema: {type: string, maxLength: 255}
      responses:
        '200':
          description: Targeted, resolved ad placements without third-party tracking URLs
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /ads/impression:
    post:
      tags: [Advertising]
      operationId: reportAdImpression
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [delivery_id, event_type]
              properties:
                delivery_id: {type: string, format: uuid}
                event_type: {type: string, enum: [impression, start, progress_25, progress_50, progress_75, complete, skip, error, click]}
      responses:
        '202': {description: Delivery event accepted or deduplicated}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /ads/tags:
    get:
      tags: [Advertising]
      operationId: listAdTags
      security: [{sanctum: []}]
      responses:
        '200': {description: Owned ad tags}
        '401': {$ref: '#/components/responses/Unauthorized'}
    post:
      tags: [Advertising]
      operationId: createAdTag
      security: [{sanctum: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AdTagInput'}
      responses:
        '201': {description: Ad tag created}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '422': {$ref: '#/components/responses/ValidationError'}
  /ads/tags/{tag}:
    parameters:
      - in: path
        name: tag
        required: true
        schema: {type: integer}
    get:
      tags: [Advertising]
      operationId: getAdTag
      security: [{sanctum: []}]
      responses:
        '200': {description: Owned ad tag}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      tags: [Advertising]
      operationId: updateAdTag
      security: [{sanctum: []}]
      requestBody:
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AdTagInput'}
      responses:
        '200': {description: Updated ad tag}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
    delete:
      tags: [Advertising]
      operationId: deleteAdTag
      security: [{sanctum: []}]
      responses:
        '204': {description: Ad tag deleted}
        '404': {$ref: '#/components/responses/NotFound'}
  /api-keys:
    get:
      tags: [API Keys]
      operationId: listApiKeys
      security: [{sanctum: []}]
      responses:
        '200': {description: API keys without plaintext secrets}
        '401': {$ref: '#/components/responses/Unauthorized'}
    post:
      tags: [API Keys]
      operationId: createApiKey
      security: [{sanctum: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ApiKeyInput'}
      responses:
        '201': {description: API key with one-time plaintext key}
        '422': {$ref: '#/components/responses/ValidationError'}
  /api-keys/{apiKey}:
    parameters:
      - in: path
        name: apiKey
        required: true
        schema: {type: integer}
    get:
      tags: [API Keys]
      operationId: getApiKey
      security: [{sanctum: []}]
      responses:
        '200': {description: API key metadata}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      tags: [API Keys]
      operationId: updateApiKey
      security: [{sanctum: []}]
      requestBody:
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ApiKeyInput'}
      responses:
        '200': {description: Updated API key}
        '404': {$ref: '#/components/responses/NotFound'}
    delete:
      tags: [API Keys]
      operationId: deleteApiKey
      security: [{sanctum: []}]
      responses:
        '204': {description: API key revoked}
  /webhooks:
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      security: [{sanctum: []}, {apiKey: []}]
      responses:
        '200': {description: Owned webhooks}
    post:
      tags: [Webhooks]
      operationId: createWebhook
      security: [{sanctum: []}, {apiKey: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/WebhookInput'}
      responses:
        '201': {description: Webhook with one-time HMAC secret}
        '422': {$ref: '#/components/responses/ValidationError'}
  /webhooks/{webhook}:
    parameters:
      - in: path
        name: webhook
        required: true
        schema: {type: integer}
    get:
      tags: [Webhooks]
      operationId: getWebhook
      security: [{sanctum: []}, {apiKey: []}]
      responses:
        '200': {description: Webhook metadata}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      tags: [Webhooks]
      operationId: updateWebhook
      security: [{sanctum: []}, {apiKey: []}]
      requestBody:
        content:
          application/json:
            schema: {$ref: '#/components/schemas/WebhookInput'}
      responses:
        '200': {description: Updated webhook}
        '422': {$ref: '#/components/responses/ValidationError'}
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      security: [{sanctum: []}, {apiKey: []}]
      responses:
        '204': {description: Webhook deleted}
components:
  securitySchemes:
    sanctum:
      type: http
      scheme: bearer
      bearerFormat: Sanctum
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
  parameters:
    VideoId:
      in: path
      name: id
      required: true
      schema: {type: string, minLength: 12, maxLength: 12}
    AnalyticsFrom:
      in: query
      name: from
      description: Inclusive UTC start date; defaults to 29 days ago.
      schema: {type: string, format: date}
    AnalyticsTo:
      in: query
      name: to
      description: Inclusive UTC end date; defaults to today.
      schema: {type: string, format: date}
  responses:
    AuthSuccess:
      description: User and bearer token
      content:
        application/json:
          schema:
            allOf:
              - {$ref: '#/components/schemas/SuccessEnvelope'}
              - type: object
                properties:
                  data:
                    type: object
                    properties:
                      user: {$ref: '#/components/schemas/User'}
                      token: {type: string}
                      token_type: {type: string, const: Bearer}
    VideoSuccess:
      description: VideoObject response
      content:
        application/json:
          schema:
            allOf:
              - {$ref: '#/components/schemas/SuccessEnvelope'}
              - type: object
                properties:
                  data: {$ref: '#/components/schemas/Video'}
    Error:
      description: API error
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorEnvelope'}
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorEnvelope'}
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorEnvelope'}
    Forbidden:
      description: Permission denied
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorEnvelope'}
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorEnvelope'}
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorEnvelope'}
  schemas:
    SuccessEnvelope:
      type: object
      required: [success, data, meta]
      properties:
        success: {type: boolean, const: true}
        data: {}
        meta: {$ref: '#/components/schemas/Meta'}
    ErrorEnvelope:
      type: object
      required: [success, data, error, meta]
      properties:
        success: {type: boolean, const: false}
        data: {type: 'null'}
        error:
          type: object
          required: [code, message, details]
          properties:
            code: {type: string}
            message: {type: string}
            details: {type: object}
        meta: {$ref: '#/components/schemas/Meta'}
    Meta:
      type: object
      required: [version, timestamp, request_id]
      properties:
        version: {type: string}
        timestamp: {type: string, format: date-time}
        request_id: {type: string, format: uuid}
    ResolveVideoRequest:
      type: object
      required: [url]
      properties:
        url: {type: string, format: uri, maxLength: 2048}
    User:
      type: object
      required: [id, name, email, plan]
      properties:
        id: {type: integer}
        name: {type: string}
        email: {type: string, format: email}
        is_verified: {type: boolean}
        plan: {type: string, enum: [free, creator, pro, business, enterprise]}
        rate_limit: {type: integer}
        play_limit: {type: integer}
        monthly_plays: {type: integer}
    Video:
      type: object
      required: [id, type, provider, status, sources, subtitles, embed]
      properties:
        id: {type: string}
        title: {type: [string, 'null']}
        description: {type: [string, 'null']}
        duration: {type: [integer, 'null']}
        type: {type: string, enum: [mp4, webm, hls, dash, live]}
        provider: {type: string}
        status: {type: string, enum: [pending, resolving, ready, failed, expired]}
        is_live: {type: boolean}
        is_public: {type: boolean}
        poster: {type: [string, 'null'], format: uri}
        sources:
          type: array
          items: {$ref: '#/components/schemas/VideoSource'}
        subtitles:
          type: array
          items: {$ref: '#/components/schemas/Subtitle'}
        embed: {type: object}
        ads:
          type: object
          required: [endpoint]
          properties:
            endpoint: {type: string, format: uri}
    VideoSource:
      type: object
      required: [quality, src, type, is_primary]
      properties:
        quality: {type: string}
        src: {type: string, format: uri}
        type: {type: string}
        is_primary: {type: boolean}
    Subtitle:
      type: object
      required: [id, language, label, format, url, is_default]
      properties:
        id: {type: integer}
        language: {type: string}
        label: {type: string}
        format: {type: string, enum: [srt, vtt]}
        url: {type: string, format: uri}
        is_default: {type: boolean}
    AnalyticsEventInput:
      type: object
      required: [video_id, event_type, session_id, timestamp_ms]
      properties:
        video_id: {type: string, minLength: 12, maxLength: 12}
        event_type:
          type: string
          enum: [play, pause, ended, error, progress_25, progress_50, progress_75, progress_100, quality_change, subtitle_change, fullscreen, ad_start, ad_25, ad_50, ad_75, ad_complete, ad_skip, ad_click, ad_error]
        session_id: {type: string, minLength: 8, maxLength: 64, pattern: '^[A-Za-z0-9_-]+$'}
        timestamp_ms: {type: integer, minimum: 0}
        player_width: {type: [integer, 'null'], minimum: 0, maximum: 65535}
        player_height: {type: [integer, 'null'], minimum: 0, maximum: 65535}
        quality: {type: [string, 'null'], maxLength: 20}
        referrer_domain: {type: [string, 'null'], maxLength: 255}
        consent: {type: boolean, description: Required when GDPR mode is enabled.}
    AdTagInput:
      type: object
      properties:
        name: {type: string, maxLength: 255}
        vast_url: {type: string, maxLength: 2048}
        ad_type: {type: string, enum: [pre-roll, mid-roll, post-roll, overlay, popup]}
        mid_roll_offset: {type: [integer, 'null'], minimum: 0}
        popup_offset: {type: [integer, 'null'], minimum: 0}
        skip_after: {type: [integer, 'null'], minimum: 0}
        is_active: {type: boolean}
        fallback_tag_id: {type: [integer, 'null']}
        targeting:
          type: [object, 'null']
          properties:
            priority: {type: integer, minimum: -100, maximum: 100}
            percentage: {type: integer, minimum: 0, maximum: 100}
            cpm: {type: number, minimum: 0}
            countries: {type: array, items: {type: string, minLength: 2, maxLength: 2}}
            exclude_countries: {type: array, items: {type: string, minLength: 2, maxLength: 2}}
            devices: {type: array, items: {type: string, enum: [mobile, desktop, tablet, tv, unknown]}}
            browsers: {type: array, items: {type: string}}
            providers: {type: array, items: {type: string}}
            video_types: {type: array, items: {type: string, enum: [mp4, webm, hls, dash, live]}}
            referrer_domains: {type: array, items: {type: string}}
            video_ids: {type: array, items: {type: string, minLength: 12, maxLength: 12}}
            min_duration: {type: integer, minimum: 0}
            max_duration: {type: integer, minimum: 0}
            is_live: {type: boolean}
    ApiKeyInput:
      type: object
      properties:
        name: {type: string, maxLength: 255}
        permissions:
          type: array
          items: {type: string, enum: ['*', 'videos:read', 'videos:write', 'subtitles:write', 'webhooks:manage']}
        rate_limit: {type: integer, minimum: 1, maximum: 10000}
        is_active: {type: boolean}
    WebhookInput:
      type: object
      properties:
        url: {type: string, format: uri}
        events:
          type: array
          items: {type: string, enum: ['*', 'video.created', 'video.ready', 'video.failed', 'subtitle.created', 'subtitle.deleted']}
        is_active: {type: boolean}
