openapi: 3.1.0
info:
  title: Voomy API
  version: 0.1.0
  description: Minimal hosted video streaming API for asynchronous source uploads and adaptive playback.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://api.voomy.net
security:
  - bearerAuth: []
  - apiKeyAuth: []
paths:
  /health:
    get:
      operationId: getHealth
      summary: Health check
      security: []
      responses:
        "200":
          description: Worker is healthy.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
  /uploads:
    post:
      operationId: uploadSourceVideo
      summary: Upload a source video
      description: Stores a source video and queues asynchronous transcoding.
      parameters:
        - $ref: "#/components/parameters/TitleQuery"
        - $ref: "#/components/parameters/ExpiresInQuery"
        - $ref: "#/components/parameters/ExpiresAtQuery"
        - $ref: "#/components/parameters/VideoTitleHeader"
      requestBody:
        required: true
        content:
          video/mp4:
            schema:
              type: string
              format: binary
          video/quicktime:
            schema:
              type: string
              format: binary
          video/x-msvideo:
            schema:
              type: string
              format: binary
          video/x-matroska:
            schema:
              type: string
              format: binary
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "202":
          description: Upload accepted and queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "415":
          description: Unsupported source content type.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /processing-jobs/{jobId}:
    get:
      operationId: getProcessingJob
      summary: Get processing job status
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: Processing job status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessingJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /videos/{videoId}:
    get:
      operationId: getVideo
      summary: Get public video metadata
      security: []
      parameters:
        - $ref: "#/components/parameters/VideoId"
      responses:
        "200":
          description: Video metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Video"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteVideo
      summary: Delete a video
      parameters:
        - $ref: "#/components/parameters/VideoId"
      responses:
        "200":
          description: Video marked deleted.
          content:
            application/json:
              schema:
                type: object
                required: [id, status]
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    const: deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /videos/{videoId}/manifest:
    get:
      operationId: redirectVideoManifest
      summary: Redirect to active playback manifest
      security: []
      parameters:
        - $ref: "#/components/parameters/VideoId"
      responses:
        "302":
          description: Redirect to the media-domain manifest URL.
          headers:
            Location:
              schema:
                type: string
                format: uri
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /videos/{videoId}/range:
    get:
      operationId: getVideoRange
      summary: Get segment URLs for a sequence range
      security: []
      parameters:
        - $ref: "#/components/parameters/VideoId"
        - name: start
          in: query
          description: "First segment sequence. Alias: from."
          schema:
            type: integer
            minimum: 0
        - name: end
          in: query
          description: "Last segment sequence. Alias: to."
          schema:
            type: integer
            minimum: 0
        - name: from
          in: query
          deprecated: true
          schema:
            type: integer
            minimum: 0
        - name: to
          in: query
          deprecated: true
          schema:
            type: integer
            minimum: 0
      responses:
        "200":
          description: Segment URLs in the requested sequence range.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VideoRange"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /videos/{videoId}/source:
    get:
      operationId: getVideoSource
      summary: Get source download URL
      parameters:
        - $ref: "#/components/parameters/VideoId"
      responses:
        "200":
          description: Source object metadata and URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SourceDownload"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
  parameters:
    JobId:
      name: jobId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    VideoId:
      name: videoId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    TitleQuery:
      name: title
      in: query
      schema:
        type: string
    ExpiresInQuery:
      name: expires_in
      in: query
      description: TTL in seconds.
      schema:
        type: integer
        minimum: 1
    ExpiresAtQuery:
      name: expires_at
      in: query
      description: ISO-8601 expiration timestamp.
      schema:
        type: string
        format: date-time
    VideoTitleHeader:
      name: x-video-title
      in: header
      schema:
        type: string
  responses:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Authenticated API key cannot perform this operation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ServiceUnavailable:
      description: Required service configuration is missing.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
    UploadResponse:
      type: object
      required: [jobId, tenantId, status, videoId, source, statusPath]
      properties:
        jobId:
          type: string
          format: uuid
        tenantId:
          type: string
        status:
          type: string
          const: queued
        videoId:
          type: "null"
        source:
          type: object
          required: [key, contentType]
          properties:
            key:
              type: string
            contentType:
              type: string
        statusPath:
          type: string
    ProcessingJob:
      type: object
      required:
        - jobId
        - status
        - videoId
        - tenantId
        - source
        - error
        - statusPath
        - createdAt
        - updatedAt
        - completedAt
      properties:
        jobId:
          type: string
          format: uuid
        status:
          type: string
          enum: [queued, processing, ready, failed, deleted]
        videoId:
          anyOf:
            - type: string
              format: uuid
            - type: "null"
        tenantId:
          type: string
        source:
          anyOf:
            - $ref: "#/components/schemas/SourceFile"
            - type: "null"
        error:
          anyOf:
            - type: string
            - type: "null"
        statusPath:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        completedAt:
          anyOf:
            - type: string
              format: date-time
            - type: "null"
    SourceFile:
      type: object
      required: [fileId, key, contentType]
      properties:
        fileId:
          anyOf:
            - type: string
              format: uuid
            - type: "null"
        key:
          type: string
        contentType:
          anyOf:
            - type: string
            - type: "null"
    Video:
      type: object
      required:
        - id
        - tenantId
        - ownerId
        - title
        - status
        - r2Prefix
        - manifestKey
        - manifestFileId
        - manifestType
        - manifestPath
        - rangePath
        - expiresAt
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        tenantId:
          type: string
        ownerId:
          anyOf:
            - type: string
            - type: "null"
        title:
          anyOf:
            - type: string
            - type: "null"
        status:
          type: string
          enum: [created, ready, deleted]
        r2Prefix:
          type: string
        manifestKey:
          anyOf:
            - type: string
            - type: "null"
        manifestFileId:
          anyOf:
            - type: string
              format: uuid
            - type: "null"
        manifestType:
          anyOf:
            - type: string
              enum: [hls, dash]
            - type: "null"
        manifestPath:
          anyOf:
            - type: string
            - type: "null"
        rangePath:
          anyOf:
            - type: string
            - type: "null"
        expiresAt:
          anyOf:
            - type: string
              format: date-time
            - type: "null"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    VideoRange:
      type: object
      required: [videoId, start, end, segments]
      properties:
        videoId:
          type: string
          format: uuid
        start:
          type: integer
          minimum: 0
        end:
          type: integer
          minimum: 0
        segments:
          type: array
          items:
            $ref: "#/components/schemas/Segment"
    Segment:
      type: object
      required: [sequence, url, relativeKey, contentType, size, etag]
      properties:
        sequence:
          anyOf:
            - type: integer
              minimum: 0
            - type: "null"
        url:
          anyOf:
            - type: string
              format: uri
            - type: "null"
        relativeKey:
          type: string
        contentType:
          anyOf:
            - type: string
            - type: "null"
        size:
          anyOf:
            - type: integer
              minimum: 0
            - type: "null"
        etag:
          anyOf:
            - type: string
            - type: "null"
    SourceDownload:
      type: object
      required: [videoId, tenantId, url, key, contentType, size, etag]
      properties:
        videoId:
          type: string
          format: uuid
        tenantId:
          type: string
        url:
          type: string
          format: uri
        key:
          type: string
        contentType:
          anyOf:
            - type: string
            - type: "null"
        size:
          anyOf:
            - type: integer
              minimum: 0
            - type: "null"
        etag:
          anyOf:
            - type: string
            - type: "null"
