openapi: 3.0.3
info:
  title: ffmpeg-micro API
  version: "1.0.0"
  description: >
    Simple, opinionated API for creating and tracking transcode jobs.
    Authentication uses an opaque API key sent as `Authorization: Bearer <key>`.
servers:
  - url: https://api.ffmpeg-micro.com
    description: Production
  - url: http://localhost:8080
    description: Local

paths:
  /health-check:
    get:
      summary: Health check endpoint
      description: >
        Returns the health status of the API Gateway service.
        Used for monitoring and load balancer health checks.
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  timestamp:
                    type: string
                    format: date-time
                    example: "2025-10-23T14:30:45.123Z"
                  uptime:
                    type: number
                    description: Server uptime in seconds
                    example: 3600.5
                  supabase:
                    type: object
                    properties:
                      ok:
                        type: boolean
                        example: true
                      status:
                        type: integer
                        example: 200
                      statusText:
                        type: string
                        example: "OK"
                      connected:
                        type: boolean
                        example: true
              example:
                ok: true
                timestamp: "2025-10-23T14:30:45.123Z"
                uptime: 3600.5
                supabase:
                  ok: true
                  status: 200
                  statusText: "OK"
                  connected: true
        "500":
          description: Service is unhealthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: false
                  timestamp:
                    type: string
                    format: date-time
                  uptime:
                    type: number
                  supabase:
                    type: object
                    properties:
                      ok:
                        type: boolean
                        example: false
                      error:
                        type: string
                        example: "Connection failed"

  /v1/upload/presigned-url:
    post:
      summary: Generate presigned upload URL
      description: >
        Generates a presigned URL for direct file upload to Google Cloud Storage.
        This is the first step in the upload workflow - use the returned URL to upload your file.
        
        **Upload Workflow:**
        1. Call this endpoint to get a presigned URL
        2. Upload file directly to GCS using HTTP PUT
        3. Call `/v1/upload/confirm` to verify upload
        4. Use the GCS URL in your transcode request
        
        **Benefits:**
        - No server bandwidth usage
        - Faster uploads (direct to GCS)
        - Secure (presigned URLs expire)
      security:
        - bearerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, contentType, fileSize]
              properties:
                filename:
                  type: string
                  description: Original filename (will be prefixed with timestamp)
                  example: "my-video.mp4"
                contentType:
                  type: string
                  description: MIME type of the file
                  enum: 
                    - "video/mp4"
                    - "video/webm" 
                    - "video/avi"
                    - "video/quicktime"
                    - "video/mkv"
                    - "audio/mpeg"
                    - "audio/wav"
                    - "audio/flac"
                    - "audio/aac"
                  example: "video/mp4"
                fileSize:
                  type: integer
                  description: >
                    File size in bytes as a JSON number (not a string). Must be finite, positive,
                    and within the maximum upload limit. String values (including digit-only strings
                    like "1024") are rejected with 400.
                  minimum: 1
                  maximum: 2048576000
                  example: 10485760
            example:
              filename: "my-video.mp4"
              contentType: "video/mp4"
              fileSize: 10485760
      responses:
        "200":
          description: Presigned URL generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  result:
                    type: object
                    properties:
                      uploadUrl:
                        type: string
                        format: uri
                        description: Presigned URL for HTTP PUT upload
                        example: "https://storage.googleapis.com/bucket/timestamp-my-video.mp4?X-Goog-Algorithm=..."
                      filename:
                        type: string
                        description: Final filename in GCS (with timestamp prefix)
                        example: "20251023-143045-my-video.mp4"
              example:
                success: true
                result:
                  uploadUrl: "https://storage.googleapis.com/my-bucket/20251023-143045-my-video.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=..."
                  filename: "20251023-143045-my-video.mp4"
        "400":
          description: Bad Request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
              examples:
                missingFields:
                  value:
                    error: "Missing required fields: filename, contentType, fileSize"
                invalidFileSize:
                  value:
                    error: "Invalid fileSize. Must be a positive number in bytes (not a formatted string like '1.46mbs')"
                invalidFileType:
                  value:
                    error: "Invalid file type. Only video, audio, or image files are allowed."
                fileTooLarge:
                  value:
                    error: "File size too large. Maximum allowed size is 2GB."
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/upload/confirm:
    post:
      summary: Confirm file upload
      description: >
        Confirms that a file has been successfully uploaded to GCS.
        This is the final step in the upload workflow.
        
        **Use the returned GCS URL in your transcode requests:**
        - Format: `gs://bucket-name/filename`
        - Example: `gs://<YOUR_BUCKET>/20251023-143045-my-video.mp4`
      security:
        - bearerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, fileSize]
              properties:
                filename:
                  type: string
                  description: Filename returned from presigned URL generation
                  example: "20251023-143045-my-video.mp4"
                fileSize:
                  type: integer
                  description: >
                    Actual uploaded size in bytes as a JSON number (must match the object in GCS).
                    Same rules as presign: not a string, must be finite and positive.
                  example: 10485760
                uploadId:
                  type: string
                  description: Optional upload ID for tracking
                  example: "upload-12345"
            example:
              filename: "20251023-143045-my-video.mp4"
              fileSize: 10485760
              uploadId: "upload-12345"
      responses:
        "200":
          description: Upload confirmed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  result:
                    type: object
                    properties:
                      gcsUrl:
                        type: string
                        description: GCS URL for use in transcode requests
                        example: "gs://<YOUR_BUCKET>/20251023-143045-my-video.mp4"
                      publicUrl:
                        type: string
                        format: uri
                        description: Public HTTP URL (if applicable)
                        example: "https://storage.googleapis.com/my-bucket/20251023-143045-my-video.mp4"
                      filename:
                        type: string
                        example: "20251023-143045-my-video.mp4"
                      size:
                        type: integer
                        example: 10485760
              example:
                success: true
                result:
                  gcsUrl: "gs://<YOUR_BUCKET>/20251023-143045-my-video.mp4"
                  filename: "20251023-143045-my-video.mp4"
                  size: 10485760
        "400":
          description: Bad Request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
              examples:
                missingFields:
                  value:
                    error: "Missing required fields: filename, fileSize"
                invalidFileSize:
                  value:
                    error: "Invalid fileSize. Must be a positive number in bytes (not a formatted string like '1.46mbs')"
                fileNotFound:
                  value:
                    error: "File not found in GCS bucket"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/transcodes:
    post:
      summary: Create a transcode or multi-video composition
      description: >
        Queues a new transcode job with support for single or multiple inputs.
        Supports simple presets (quality/resolution) or advanced mode (raw FFmpeg options).
        Billing is based on total input duration (sum of all inputs).
        Returns the created resource with `status=queued` and `billableMinutes`.
      security:
        - bearerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTranscodeRequest"
            examples:
              singleVideoPreset:
                summary: Single video with quality preset (simple mode)
                value:
                  inputs: 
                    - url: "gs://<YOUR_BUCKET>/input.mp4"
                  outputFormat: "mp4"
                  preset:
                    quality: "high"
                    resolution: "1080p"
              multiVideoComposition:
                summary: Stitch multiple videos together
                value:
                  inputs:
                    - url: "gs://<YOUR_BUCKET>/video1.mp4"
                    - url: "gs://<YOUR_BUCKET>/video2.mp4"
                    - url: "gs://<YOUR_BUCKET>/video3.mp4"
                  outputFormat: "mp4"
                  preset:
                    quality: "medium"
              advancedOptions:
                summary: Advanced mode with raw FFmpeg options
                value:
                  inputs:
                    - url: "gs://<YOUR_BUCKET>/input.mp4"
                  outputFormat: "mp4"
                  options:
                    - option: "-c:v"
                      argument: "libx265"
                    - option: "-crf"
                      argument: "28"
                    - option: "-preset"
                      argument: "slow"
              textOverlay:
                summary: Add text overlay with auto word-wrap
                value:
                  inputs:
                    - url: "gs://<YOUR_BUCKET>/input.mp4"
                  outputFormat: "mp4"
                  options:
                    - option: "@text-overlay"
                      argument:
                        text: "I dont know who needs to hear this but building features without talking to users is just guessing"
                        style:
                          charsPerLine: 18
                          fontSize: 60
                          lineSpacing: 15
                          x: "0.15*w"
                          y: "(h-text_h)/2"
                          boxBorderW: 12
      responses:
        "201":
          description: Created
          headers:
            Location:
              description: URL of the new transcode resource
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transcode"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Payment Required (quota exceeded or file too large for plan)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  details:
                    type: object
                    properties:
                      quotaExceeded: { type: boolean }
                      metric: { type: string }
                      required: { type: number }
                      limit: { type: number }
                      remaining: { type: number }
        "422":
          $ref: "#/components/responses/Unprocessable"
        "500":
          $ref: "#/components/responses/ServerError"

    get:
      summary: List transcodes (for the authenticated user)
      security:
        - bearerApiKey: []
      parameters:
        - in: query
          name: status
          schema:
            $ref: "#/components/schemas/TranscodeStatus"
          description: Filter by status
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - in: query
          name: since
          schema: { type: string, format: date-time }
          description: Return transcodes created at/after this timestamp
        - in: query
          name: until
          schema: { type: string, format: date-time }
          description: Return transcodes created at/before this timestamp
      responses:
        "200":
          description: OK
          headers:
            X-Total-Count:
              description: Total number of matching transcodes for pagination
              schema: { type: integer }
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Transcode"
                  page: { type: integer }
                  limit: { type: integer }
                  total: { type: integer }
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/transcodes/{id}:
    get:
      summary: Get a transcode
      security:
        - bearerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transcode"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/transcodes/{id}/cancel:
    patch:
      summary: Cancel a transcode job
      description: >
        Cancels a transcode job that is currently `queued` or `processing`.
        Completed, failed, or already-cancelled jobs cannot be cancelled and return 400.
        If the job has been handed off to the toolkit service, a best-effort cancel
        is also sent to the toolkit — failures there are logged but do not fail this request.
      security:
        - bearerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Job cancelled successfully
          content:
            application/json:
              schema:
                type: object
                required: [success, message, job]
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Job cancelled successfully"
                  job:
                    $ref: "#/components/schemas/Transcode"
        "400":
          description: Job is not in a cancellable state
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
              example:
                error: "Cannot cancel job in status: completed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/transcodes/{id}/download:
    get:
      summary: Get a signed download URL for a completed transcode
      description: >
        Returns a short-lived (10 minute) signed HTTPS URL for the completed output file.

        Because `output_url` on the transcode resource is a `gs://` URL that cannot be
        fetched directly by HTTP clients, use this endpoint to obtain a fetchable URL.
        The client is then responsible for downloading the file directly from the returned
        URL — the gateway does not proxy file bytes.

        The job must be in `completed` status and have a non-null `output_url`.
      security:
        - bearerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Signed HTTPS URL valid for 10 minutes
          content:
            application/json:
              schema:
                type: object
                required: [url]
                properties:
                  url:
                    type: string
                    format: uri
                    description: Signed HTTPS URL valid for 10 minutes
                    example: "https://storage.googleapis.com/my-bucket/output.mp4?X-Goog-Algorithm=..."
        "400":
          description: Transcode not completed or has no output
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
              examples:
                notCompleted:
                  value: { error: "Transcode not completed" }
                noOutput:
                  value: { error: "No output available for this transcode" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/transcribe:
    post:
      summary: Create a transcribe job (audio → SRT)
      description: >
        Generate an SRT subtitle file from an audio or video URL using Whisper.
        Returns a queued job envelope identical in shape to `/v1/transcodes`;
        poll `GET /v1/transcribe/{id}` until `status` is `completed`, then
        fetch the SRT via `GET /v1/transcribe/{id}/download`.

        The resulting SRT is stored in your user-namespaced output bucket and
        can be referenced inside a transcode `subtitles=` filter to burn
        captions into a video (see the docs for the zoom+captions tutorial).
      security:
        - bearerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTranscribeRequest"
      responses:
        "201":
          description: Transcribe job created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transcribe"
        "400":
          description: Invalid input (missing or invalid `media_url`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/transcribe/{id}:
    get:
      summary: Get transcribe job status
      security:
        - bearerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Transcribe job details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transcribe"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/transcribe/{id}/download:
    get:
      summary: Get a signed download URL for the generated SRT
      description: >
        Returns a short-lived (10 minute) signed HTTPS URL for the completed
        SRT file. The job must be in `completed` status.
      security:
        - bearerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Signed HTTPS URL valid for 10 minutes
          content:
            application/json:
              schema:
                type: object
                required: [url]
                properties:
                  url:
                    type: string
                    format: uri
                    example: "https://storage.googleapis.com/…/job-id.srt?X-Goog-Signature=…"
        "400":
          description: Transcribe not completed or has no output
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/transcribes:
    get:
      summary: List transcribe jobs (for the authenticated user)
      security:
        - bearerApiKey: []
      parameters:
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 10 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Transcribe"
                  page: { type: integer }
                  limit: { type: integer }
                  total: { type: integer }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  # /v1/jobs endpoints removed - use /v1/transcodes instead

components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: opaque-api-key

  schemas:
    TranscodeStatus:
      type: string
      enum: [queued, processing, completed, failed]

    VideoMetadata:
      type: object
      properties:
        format: { type: string, example: mp4 }
        duration_seconds: { type: number, example: 123.45 }
        size_bytes: { type: integer, example: 10485760 }

    Transcode:
      type: object
      required: [id, status, output_format, created_at]
      properties:
        id:
          type: string
          format: uuid
          example: "b5f5a9c0-9e33-4e77-8a5b-6a0c2cd9c0b3"
        status:
          $ref: "#/components/schemas/TranscodeStatus"
        inputs:
          type: array
          description: Array of input video metadata
          items:
            type: object
            properties:
              url: { type: string }
              duration: { type: number }
              format: { type: string }
              size: { type: string }
        outputs:
          type: array
          description: Array of output configurations and URLs
          items:
            type: object
        output_format:
          type: string
          enum: [mp4, webm, mov]
        billable_minutes:
          type: integer
          description: Pre-calculated billable minutes (sum of input durations)
          example: 3
        duration_seconds:
          type: number
          description: Total duration across all inputs
          example: 135.0
        ffmpeg_options:
          type: object
          description: Final FFmpeg options used (audit trail)
          properties:
            source:
              type: string
              enum: [preset, raw_options, defaults]
            options:
              type: array
              items:
                type: object
            preset:
              type: object
              nullable: true
        output_url:
          type: string
          nullable: true
          description: Populated when status = completed
        video_metadata:
          $ref: "#/components/schemas/VideoMetadata"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        user_id:
          type: string
          description: Internal user identifier resolved from API key
        # Legacy fields (deprecated, use inputs/outputs instead)
        input_url:
          type: string
          deprecated: true
          description: "[DEPRECATED] Use inputs array instead"

    CreateTranscodeRequest:
      type: object
      required: [inputs, outputFormat]
      properties:
        inputs:
          type: array
          description: Array of input videos (single for transcode, multiple for composition)
          minItems: 1
          maxItems: 10
          items:
            type: object
            required: [url]
            properties:
              url:
                type: string
                description: >
                  GCS URL (gs://bucket/object) from presigned upload, or public HTTP(S) URL.
                  Prefer gs:// to avoid egress charges.
                example: "gs://<YOUR_BUCKET>/video.mp4"
        outputFormat:
          type: string
          enum: [mp4, webm, mov]
          description: Output container format
        preset:
          type: object
          description: Simple mode - quality and resolution presets (ignored if options provided)
          properties:
            quality:
              type: string
              enum: [high, medium, low]
              description: Maps to CRF values (high=18, medium=23, low=28)
            resolution:
              type: string
              enum: [480p, 720p, 1080p, 4k]
              description: Output resolution preset
        options:
          type: array
          description: >
            Advanced mode - FFmpeg options or virtual options (overrides preset if both provided).
            
            **Virtual Options** (prefix with `@`):
            - `@text-overlay`: Add text overlay with auto word-wrap and styling
            
            **FFmpeg Options**: Only allowlisted flags accepted for security.
          items:
            oneOf:
              - $ref: "#/components/schemas/VirtualOption"
              - $ref: "#/components/schemas/FFmpegOption"

    CreateTranscribeRequest:
      type: object
      required: [media_url]
      properties:
        media_url:
          type: string
          description: >
            GCS URL (gs://bucket/object) from a presigned upload, or a public HTTP(S) URL.
            The referenced file can be audio or video — only the audio track is used.
          example: "gs://<YOUR_BUCKET>/narration.mp3"
        language:
          type: string
          description: BCP-47 language code hint (e.g. "en", "es"). Auto-detected when omitted.
          example: "en"
        task:
          type: string
          enum: [transcribe, translate]
          default: transcribe
          description: >
            "transcribe" keeps the source language; "translate" outputs English
            regardless of source language.

    Transcribe:
      type: object
      required: [id, status, output_format, created_at]
      properties:
        id:
          type: string
          format: uuid
          example: "f8e6a1f4-2da3-496e-9e29-4d63502ad90c"
        status:
          $ref: "#/components/schemas/TranscodeStatus"
        media_url:
          type: string
          description: GCS URL of the source audio/video (echoed from the request)
          example: "gs://<YOUR_BUCKET>/narration.mp3"
        output_format:
          type: string
          enum: [srt]
          description: Always `srt` for transcribe jobs
        output_url:
          type: string
          nullable: true
          description: >
            `gs://` URL of the generated SRT once `status` is `completed`.
            Use `/v1/transcribe/{id}/download` to obtain a signed HTTPS URL.
          example: "gs://ffmpeg-micro-user-<user-id>-output/<job-id>.srt"
        error_message:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true

    VirtualOption:
      type: object
      required: [option, argument]
      properties:
        option:
          type: string
          enum: ["@text-overlay"]
          description: Virtual option name (prefixed with @)
        argument:
          oneOf:
            - $ref: "#/components/schemas/TextOverlayConfig"
          description: Configuration object for the virtual option

    FFmpegOption:
      type: object
      required: [option, argument]
      properties:
        option:
          type: string
          description: FFmpeg flag (e.g., -c:v, -crf, -preset)
          example: "-c:v"
        argument:
          type: string
          description: Flag value
          example: "libx264"

    TextOverlayConfig:
      type: object
      required: [text]
      properties:
        text:
          type: string
          description: Text to display (use \n for manual line breaks, or use charsPerLine for auto word-wrap)
          example: "Hello\nWorld"
          maxLength: 10000
        style:
          type: object
          properties:
            charsPerLine:
              type: integer
              description: "Auto word-wrap at this character width (default: no wrap)"
              minimum: 5
              maximum: 100
              example: 18
            font:
              type: string
              description: "Font name (default: Arial)"
              example: "Arial"
            fontSize:
              type: integer
              minimum: 12
              maximum: 200
              description: "Font size in pixels (default: 48)"
              example: 60
            lineSpacing:
              type: integer
              description: "Space between lines in pixels (default: 10)"
              minimum: 0
              maximum: 100
              example: 15
            fontColor:
              type: string
              description: "Font color (default: white)"
              example: "white"
            x:
              type: string
              description: "X position FFmpeg expression (default: (w-text_w)/2 for center)"
              example: "0.15*w"
            y:
              type: string
              description: "Y position FFmpeg expression (default: (h-text_h)/2 for center)"
              example: "(h-text_h)/2"
            boxBorderW:
              type: integer
              description: "Box padding/border width in pixels (default: 20)"
              minimum: 0
              maximum: 100
              example: 12

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error: { type: string }
        details: { type: string, nullable: true }

    Job:
      type: object
      description: Summary job information for list views
      required: [jobId, status, createdAt, updatedAt]
      properties:
        jobId:
          type: string
          format: uuid
          example: "b5f5a9c0-9e33-4e77-8a5b-6a0c2cd9c0b3"
        status:
          $ref: "#/components/schemas/TranscodeStatus"
        createdAt:
          type: string
          format: date-time
          example: "2025-10-12T02:35:36.351Z"
        updatedAt:
          type: string
          format: date-time
          example: "2025-10-12T02:35:39.547Z"
        outputUrl:
          type: string
          nullable: true
          description: GCS path to output file (gs://bucket/key)
          example: "gs://<YOUR_BUCKET>/output.mp4"

    JobDetails:
      type: object
      description: Detailed job information
      required: [success, jobId, status, createdAt, updatedAt]
      properties:
        success: { type: boolean, example: true }
        jobId:
          type: string
          format: uuid
          example: "b5f5a9c0-9e33-4e77-8a5b-6a0c2cd9c0b3"
        status:
          $ref: "#/components/schemas/TranscodeStatus"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        inputUrl:
          type: string
          nullable: true
          example: "gs://<YOUR_BUCKET>/input.mp4"
        outputUrl:
          type: string
          nullable: true
          description: GCS path to output file (gs://bucket/key)
          example: "gs://<YOUR_BUCKET>/output.mp4"
        outputFormat:
          type: string
          nullable: true
          example: "mp4"
        quality:
          type: string
          nullable: true
          example: "high"
        resolution:
          type: string
          nullable: true
          example: "1920x1080"
        processingTimeSeconds:
          type: number
          nullable: true
          description: Actual processing time in seconds
          example: 45.2
        durationSeconds:
          type: number
          nullable: true
          description: Video duration in seconds
          example: 120.5
        metadata:
          type: object
          nullable: true
          description: Additional job metadata
        errorMessage:
          type: string
          nullable: true
          description: Error message if job failed
        completedAt:
          type: string
          format: date-time
          nullable: true
          description: When the job completed

  responses:
    Unauthorized:
      description: Missing/invalid API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          examples:
            example:
              value: { error: "unauthorized" }
    Forbidden:
      description: Authenticated but not allowed to access this resource
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    BadRequest:
      description: Invalid input
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Unprocessable:
      description: Validation failed
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    ServerError:
      description: Unexpected error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
