# Canonical source: keep this file identical to the published docs copy.
openapi: 3.1.0
info:
  title: Braiv Public API
  version: 1.0.0
  summary: Automate Braiv video workflows
  license:
    name: Proprietary
    url: https://braiv.co/terms-and-conditions
  description: |
    A job-oriented API for importing media and creating transcripts, translated
    captions, dubs, shorts, thumbnails, packaging metadata, and downloads.

    Authenticate with either `Authorization: Bearer <key>` or `X-Api-Key: <key>`.
    POST requests may include an `Idempotency-Key`; successful and error
    responses are replayed for 24 hours
    when the same key, path, and canonical JSON body are reused.
servers:
  - url: https://api.braiv.co/v1
    description: Braiv Public API
security:
  - bearerAuth: []
  - apiKeyHeader: []
tags:
  - name: Health
    description: Service availability.
  - name: Videos
    description: Import, upload, inspect, and list library media.
  - name: Jobs
    description: Poll normalized asynchronous work.
  - name: Transcripts
    description: Read source transcripts and subtitle files, or translate captions.
  - name: Dubs
    description: Create and retrieve dubbed audio and target-language transcripts.
  - name: Shorts
    description: Generate short clips, promos, and long-form clips.
  - name: Thumbnails
    description: Generate and inspect AI thumbnail requests.
  - name: Characters
    description: List Studio-saved thumbnail character references for the library.
  - name: Brand Voices
    description: List Studio branding guidelines (Brand Voices) for the library.
  - name: Folders
    description: List and create Studio library folders and subfolders.
  - name: Hosting
    description: Activate Mux professional hosting on an imported video.
  - name: Packaging
    description: Generate titles, descriptions, and zero-prompt thumbnails.
  - name: Downloads
    description: Prepare downloadable media artifacts.
  - name: Credits
    description: Quote and reserve credits before starting work.
  - name: Webhooks
    description: Register HTTPS destinations for lifecycle notifications.

paths:
  /health:
    get:
      operationId: health
      tags: [Health]
      summary: Check API health
      description: Returns the deployed API version. No API key is needed.
      security: []
      responses:
        "200":
          description: The API is available.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Health"}
              example: {ok: true, version: v1}

  /videos:
    post:
      operationId: createVideo
      tags: [Videos]
      summary: Import or reserve a video
      description: |
        Creates a library video from a supported URL, direct HTTPS asset, or
        client upload reservation. URL imports begin immediately. For `upload`,
        use the returned object path and completion route with `createVideoUpload`.
        Optional product recipes are staged to run after ingest prerequisites.
        `products.packaging.thumbnail` accepts `true`/`{}` (faceless Creative)
        or the full zero-prompt object (Creative `thumbnail-v2` or Precision
        `thumbnail-precision`, including opaque key-moment detect).
        Optional `folderId` places the video in an existing Studio folder in the
        same personal or workspace library. Create folders with `POST /folders`
        or in Studio. Unknown or inaccessible folders return 404.
        Required scope: `videos:write`.
      x-required-scope: videos:write
      parameters:
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        required: true
        description: Source media, optional workspace destination, optional Studio folder, and post-import recipe.
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateVideoRequest"}
            examples:
              youtubeWithProducts:
                summary: Import and create a dub, shorts, and packaging
                value:
                  source: {type: youtube, url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"}
                  workspaceId: ws_team_123
                  products:
                    dubs:
                      - {languages: [es, fr], product: plus}
                    shorts: {kind: shorts, numClips: 3, aspectRatio: "9:16"}
                    packaging:
                      title: {style: informational}
                      description: {type: youtube-description}
                      thumbnail:
                        enabled: true
                        pipelineMode: thumbnail-v2
                        characterCount: 1
                        characters: [{characterId: char_01}]
                        brandVoiceId: bv_01
              upload:
                summary: Reserve an upload
                value:
                  source: {type: upload, fileName: launch-demo.mp4, contentType: video/mp4}
              httpsIntoFolder:
                summary: Fetch HTTPS media into an existing Studio folder
                value:
                  source: {type: https, url: "https://cdn.example.com/launch-demo.mp4"}
                  folderId: fold_01JZ8M6A
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "202":
          description: Video accepted. Poll the returned jobs; upload reservations also include upload instructions.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreateVideoResponse"}
              examples:
                accepted:
                  value:
                    videoId: vid_01JZ8M6A
                    status: queued
                    jobs:
                      - {id: "import_vid_01JZ8M6A", type: import, status: queued}
                      - {id: "transcript_vid_01JZ8M6A", type: transcript, status: queued}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409":
          description: The source already exists, or the idempotency key was reused for a different request.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "422":
          description: The source provider is unsupported or its duration cannot be resolved.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "429": {$ref: "#/components/responses/InsufficientCreditsOrRateLimit"}
        "500": {$ref: "#/components/responses/InternalError"}
    get:
      operationId: listVideos
      tags: [Videos]
      summary: List library videos
      description: "Lists accessible videos newest first. Pass the previous response's token to continue. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/Limit"}
        - {$ref: "#/components/parameters/PageToken"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: A page of videos.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/VideoList"}
              example:
                videos:
                  - {id: vid_01JZ8M6A, status: ready, title: Launch demo, duration: 184.2, defaultLanguageCode: en}
                nextPageToken: vid_01JZ7ZZZ
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}:
    get:
      operationId: getVideo
      tags: [Videos]
      summary: Get a video
      description: "Returns the normalized library record and current import/transcript projections. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: The accessible video resource.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Video"}
              example:
                id: vid_01JZ8M6A
                status: ready
                title: Launch demo
                duration: 184.2
                mediaType: video
                defaultLanguageCode: en
                import: {status: ready}
                transcript: {status: ready, language: en}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/uploads:
    post:
      operationId: createVideoUpload
      tags: [Videos]
      summary: Create a resumable upload session
      description: |
        Creates a 30-minute signed Google Cloud Storage resumable-session URL
        for a previously reserved upload video. Start the session by POSTing to
        `url` with `x-goog-resumable: start`, then upload bytes using the session URL.
        Required scope: `videos:write`.
      x-required-scope: videos:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateVideoUploadRequest"}
            example: {fileName: launch-demo.mp4, contentType: video/mp4}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Signed resumable-upload initiation URL and import job identifier.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreateVideoUploadResponse"}
              example:
                url: "https://storage.googleapis.com/braiv-media/..."
                videoId: vid_01JZ8M6A
                storagePath: "user/videos/vid_01JZ8M6A/originals/launch-demo.mp4"
                jobId: "import_vid_01JZ8M6A"
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/quote:
    post:
      operationId: quoteVideo
      tags: [Credits]
      summary: Quote a product bundle
      description: "Calculates the exact credits required without debiting the account. Duration is resolved from the video unless supplied. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        description: One or more billable actions. Empty bundles return a zero quote.
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreditBundleRequest"}
            example:
              dubs: [{product: premium, languages: [es]}]
              shorts: {kind: shorts, numClips: 3, includeWatermark: false}
              zeroPromptThumbnail:
                enabled: true
                characterCount: 1
                referenceProducts: [thumbnail-style-reference]
                translateLanguageCodes: [es]
                autoDetectContentMoments: true
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Current pricing, balance, shortfall, and per-action breakdown.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreditQuote"}
              example:
                total: 126
                durationSeconds: 184
                pricingVersion: 2026-09-10-captions-paid
                balance: 300
                usageBillingActive: false
                shortfall: 0
                sufficient: true
                breakdown:
                  - {actionKey: dub, product: cloning-dubbing, targetLanguageCode: es, credits: 46, description: "Dub to es"}
                  - {actionKey: shorts-clip, product: shorts-clip, credits: 60, description: "Generate 3 short clip(s)"}
                  - {actionKey: zero-prompt-thumbnail, product: thumbnail-create, credits: 20, description: Generate thumbnail}
                  - {actionKey: content-moment-detect, product: content-moment-detect, credits: 20, description: Detect key moments}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "422":
          description: Duration-priced work cannot be quoted because media duration is unknown.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/reservations:
    post:
      operationId: reserveVideoCredits
      tags: [Credits]
      summary: Reserve credits for a bundle
      description: "Atomically debits the full quoted bundle into escrow. Pass reservation and line-item IDs to supported product creation calls. Required scope: `videos:write`."
      x-required-scope: videos:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        description: At least one billable action is required.
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreditBundleRequest"}
            example:
              durationSeconds: 184
              captionLanguageCodes: [es, fr]
              shorts: {kind: promo, includeSoundtrack: true}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "201":
          description: Credits reserved and line items created.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreditReservation"}
              example:
                reservationId: rsv_01JZ9A
                total: 132
                durationSeconds: 184
                lineItems:
                  - {lineItemId: li_01, actionKey: captions, product: translate-captions-only, targetLanguageCode: es, credits: 46}
                  - {lineItemId: li_02, actionKey: promo, product: promo, credits: 40}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "422":
          description: Media duration is unknown for duration-priced work.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "429": {$ref: "#/components/responses/InsufficientCreditsOrRateLimit"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/transcript:
    get:
      operationId: getTranscript
      tags: [Transcripts]
      summary: Get the source transcript
      description: "Returns processing state, or the complete segmented transcript and optional word-alignment artifact. Required scope: `transcripts:read`."
      x-required-scope: transcripts:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Transcript is ready.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Transcript"}
              example:
                status: ready
                userVideoId: vid_01JZ8M6A
                language: en
                segments: [{start: 0.4, end: 3.8, speaker: speaker_0, text: "Welcome to our launch.", deleted: false}]
                speakers: [{id: speaker_0, label: Ben}]
                wordAlignmentUrl: "https://storage.googleapis.com/..."
                wordAlignmentStoragePath: "user/videos/vid_01JZ8M6A/transcript/words.json"
        "202":
          description: Transcript is not ready; retry later.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Transcript"}
              example: {status: processing, userVideoId: vid_01JZ8M6A, processingStatus: transcribing}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "422":
          description: Transcription reached a terminal failed state.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Transcript"}
              example: {status: failed, userVideoId: vid_01JZ8M6A, processingStatus: failed}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/subtitles/{lang}:
    get:
      operationId: getSubtitles
      tags: [Transcripts]
      summary: Get a subtitle file
      description: "Generates a temporary download URL for an SRT or WebVTT file in the requested source or translated language. Required scope: `transcripts:read`."
      x-required-scope: transcripts:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/Lang"}
        - {$ref: "#/components/parameters/SubtitleFormat"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Subtitle file is ready.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Subtitles"}
              example: {status: ready, videoId: vid_01JZ8M6A, language: es, format: srt, downloadUrl: "https://storage.googleapis.com/..."}
        "202":
          description: Source transcription is still processing.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/SubtitlesProcessing"}
              example: {status: processing, videoId: vid_01JZ8M6A}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404":
          description: Video or requested subtitle file was not found.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "422":
          description: Transcription failed, so subtitles cannot be produced.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/captions:
    post:
      operationId: translateCaptions
      tags: [Transcripts]
      summary: Translate captions
      description: "Creates one caption translation job per target language. Jobs appear with the `captions_` prefix in the video job list. Required scope: `dubs:write`."
      x-required-scope: dubs:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/TranslateCaptionsRequest"}
            example:
              languages: [es, fr]
              reservationId: rsv_01JZ9A
              reservationLineItemIdByLanguage: {es: li_01, fr: li_02}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "202":
          description: Translation jobs queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/TranslateCaptionsResponse"}
              example:
                jobs:
                  - {id: "captions_vid_01JZ8M6A_es", type: captions, status: queued, language: es}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "429": {$ref: "#/components/responses/InsufficientCreditsOrRateLimit"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/dubs:
    post:
      operationId: createDubs
      tags: [Dubs]
      summary: Create dubs
      description: "Queues one dub job per target language using the selected voice product and source voiceover track. Required scope: `dubs:write`."
      x-required-scope: dubs:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateDubsRequest"}
            example:
              languages: [es, de]
              product: premium
              sourceVoiceover: original
              reservationId: rsv_01JZ9A
              reservationLineItemIdByLanguage: {es: li_01, de: li_02}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "202":
          description: Dub jobs queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreateDubsResponse"}
              example:
                videoId: vid_01JZ8M6A
                jobs:
                  - {id: "dub_vid_01JZ8M6A_es_premium", type: dub, status: queued, language: es, product: premium}
                balance: 254
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "429": {$ref: "#/components/responses/InsufficientCreditsOrRateLimit"}
        "500": {$ref: "#/components/responses/InternalError"}
    get:
      operationId: listDubs
      tags: [Dubs]
      summary: List dubs for a video
      description: "Lists all dubbing request projections for the video. Use `getDub` for downloadable audio and transcript segments. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Dubbing requests and current internal completion state.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/DubList"}
              example:
                dubs:
                  - {id: dubreq_01, language: es, product: premium, status: complete, completion: complete, jobId: "dub_vid_01JZ8M6A_es_premium"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/dubs/{lang}:
    get:
      operationId: getDub
      tags: [Dubs]
      summary: Get a dub
      description: "Returns readiness, downloadable dubbed audio, and target-language transcript data for a language/product pair. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/Lang"}
        - {$ref: "#/components/parameters/DubProductQuery"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Dub audio is ready.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Dub"}
              example:
                status: ready
                userVideoId: vid_01JZ8M6A
                languageCode: es
                dubbingRequestId: dubreq_01
                dubbingStatus: complete
                completionStatus: complete
                audioUrl: "https://storage.googleapis.com/.../es.mp3"
                audioTrackId: track_es
                segments: [{start: 0.4, end: 3.8, speaker: speaker_0, text: "Bienvenidos.", deleted: false}]
                speakers: [{id: speaker_0, label: Ben}]
                jobId: "dub_vid_01JZ8M6A_es_plus"
        "202":
          description: A dub exists but is still processing, or its stored pipeline state is failed; inspect `status`.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Dub"}
              example: {status: processing, userVideoId: vid_01JZ8M6A, languageCode: es, audioUrl: null, audioTrackId: null, jobId: "dub_vid_01JZ8M6A_es_plus"}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404":
          description: Video or requested language/product dub was not found.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/shorts:
    post:
      operationId: createShorts
      tags: [Shorts]
      summary: Create shorts, a promo, or clips
      description: |
        Queues the selected shorts-family workflow. The request is discriminated
        by `kind`; fields accepted by one variant are not forwarded to another.
        Poll `getShorts` until it returns 200 or 422.
        Required scope: `shorts:write`.
      x-required-scope: shorts:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateShortsRequest"}
            examples:
              shorts:
                value: {kind: shorts, numClips: 3, minDuration: 20, maxDuration: 60, aspectRatio: "9:16", captionStyle: highlight, captionPosition: bottom}
              promo:
                value: {kind: promo, totalDuration: 60, aspectRatio: "16:9", captionStyle: modern, captionPosition: bottom, includeSoundtrack: true, soundtrackGenre: cinematic}
              clips:
                value: {kind: clips, numClips: 2, captionStyle: modern, captionPosition: bottom, clipSelectionPrompt: "Prioritize practical product demonstrations."}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "202":
          description: Shorts-family request queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreateShortsResponse"}
              example: {shortsRequestId: req_01JZA, jobId: "shorts_req_01JZA", creditsSpent: 60, balance: 240}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "429": {$ref: "#/components/responses/InsufficientCreditsOrRateLimit"}
        "500": {$ref: "#/components/responses/InternalError"}
    get:
      operationId: listShorts
      tags: [Shorts]
      summary: List shorts requests
      description: "Lists lightweight request projections for a video. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Shorts requests for the video.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/ShortsList"}
              example:
                requests:
                  - {id: req_01JZA, status: complete, outputType: shorts, jobId: "shorts_req_01JZA"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /shorts/{shortsRequestId}:
    get:
      operationId: getShorts
      tags: [Shorts]
      summary: Get shorts outputs
      description: "Returns up to five non-deleted outputs. `partial_failed` returns 200 because usable outputs may be present. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/ShortsRequestId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Request completed fully or partially; inspect `status` and `outputs`.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Shorts"}
              example:
                shortsRequestId: req_01JZA
                status: partial_failed
                clipsExpected: 3
                jobId: "shorts_req_01JZA"
                outputs:
                  - {outputId: out_01, type: shorts, videoUrl: "https://cdn.braiv.co/out_01.mp4", thumbnailUrl: "https://cdn.braiv.co/out_01.jpg", duration: 42.5, title: "Three launch lessons"}
        "202":
          description: Request is pending or processing; outputs may be incomplete.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Shorts"}
              example: {shortsRequestId: req_01JZA, status: processing, clipsExpected: 3, jobId: "shorts_req_01JZA", outputs: []}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "422":
          description: Request failed with no successful terminal result.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Shorts"}
              example: {shortsRequestId: req_01JZA, status: failed, clipsExpected: 3, jobId: "shorts_req_01JZA", outputs: []}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/thumbnails:
    post:
      operationId: createThumbnail
      tags: [Thumbnails]
      summary: Create an AI thumbnail
      description: |
        Creates a prompt-directed thumbnail (`mode: prompt`) or a zero-prompt
        thumbnail (`mode: zero_prompt`) derived from transcript and visual
        context. Pipeline `thumbnail-v2` is Creative (character subjects).
        `thumbnail-precision` uses one or two content plates and rejects
        characters. Auto-detect (`autoDetectCharacters` or
        `autoDetectContentReferences`) is zero-prompt only and returns `202`
        with a packaging job id. Prompt Precision requires explicit content
        image URLs. Required scope: `thumbnails:write`.
      x-required-scope: thumbnails:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateThumbnailRequest"}
            examples:
              prompt:
                value: {mode: prompt, prompt: "A dramatic creator reveal with bright studio lighting"}
              zeroPromptCreative:
                summary: Faceless Creative zero-prompt
                value: {mode: zero_prompt, characterCount: 0, promoteToPrimary: true}
              zeroPromptPrecision:
                summary: Precision with opaque key-moment detect
                value:
                  mode: zero_prompt
                  pipelineMode: thumbnail-precision
                  autoDetectContentReferences: true
                  contentSlotCount: 2
                  brandVoiceId: bv_01
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "200":
          description: Request created; synchronous setup may already return generated URLs.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreateThumbnailResponse"}
              example: {thumbnailRequestId: thumb_01, jobId: "thumbnail_thumb_01", outputImageUrls: ["https://cdn.braiv.co/thumb_01.jpg"], balance: 280}
        "202":
          description: Zero-prompt automation queued because character or content-moment detect must finish first. Poll the packaging job.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreateThumbnailResponse"}
              example: {jobId: "packaging_vid_01JZ8M6A_zeroPromptThumbnail", status: queued, registered: true}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "429": {$ref: "#/components/responses/InsufficientCreditsOrRateLimit"}
        "500": {$ref: "#/components/responses/InternalError"}
    get:
      operationId: listThumbnails
      tags: [Thumbnails]
      summary: List thumbnail requests
      description: "Returns up to 50 thumbnail request projections for a video. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Thumbnail requests.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/ThumbnailList"}
              example: {thumbnails: [{id: thumb_01, status: complete, outputImageUrls: ["https://cdn.braiv.co/thumb_01.jpg"], jobId: "thumbnail_thumb_01"}]}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /thumbnails/{thumbnailRequestId}:
    get:
      operationId: getThumbnail
      tags: [Thumbnails]
      summary: Get a thumbnail request
      description: "Returns the current request status and generated image URLs. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/ThumbnailRequestId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Thumbnail request projection.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Thumbnail"}
              example: {id: thumb_01, status: complete, outputImageUrls: ["https://cdn.braiv.co/thumb_01.jpg"], jobId: "thumbnail_thumb_01"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /characters:
    get:
      operationId: listCharacters
      tags: [Characters]
      summary: List saved characters
      description: |
        Returns Studio-saved thumbnail character profiles for the API key's
        personal or workspace library (up to 100). Create characters in Studio;
        this API does not create or update them. Pass `id` as `characterId` on
        thumbnail create. Required scope: `videos:read`.
      x-required-scope: videos:read
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Saved characters in the caller's library.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CharacterList"}
              example:
                characters:
                  - id: char_01
                    name: Presenter
                    readyForThumbnailGeneration: true
                    characterType: thumbnail-reference
                    referenceImageIds: [img_01]
                    referenceImages: {img_01: "https://cdn.braiv.co/char_01.jpg"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "500": {$ref: "#/components/responses/InternalError"}

  /brand-voices:
    get:
      operationId: listBrandVoices
      tags: [Brand Voices]
      summary: List branding guidelines
      description: |
        Returns Studio branding guidelines (Brand Voices) for the API key's
        personal or workspace library (up to 100). Summary fields only — use
        `id` later as `brandVoiceId` on packaging/thumbnail flows. Create
        guidelines in Studio; this API does not create or update them.
        Required scope: `videos:read`.
      x-required-scope: videos:read
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Brand Voices in the caller's library.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/BrandVoiceList"}
              example:
                brandVoices:
                  - id: bv_01
                    name: Acme Brand
                    isDefault: true
                    logoUrls: ["https://cdn.braiv.co/logo.png"]
                    palette: [{name: Primary, hex: "#112233"}]
                    hasThumbnailStyle: true
        "403": {$ref: "#/components/responses/Forbidden"}
        "500": {$ref: "#/components/responses/InternalError"}

  /folders:
    get:
      operationId: listFolders
      tags: [Folders]
      summary: List library folders
      description: |
        Returns Studio folders in the API key or OAuth library (up to 200).
        Soft-deleted folders and the reserved All Videos folder are omitted.
        Each item includes `parentId` and a name `path` so agents can rebuild
        the tree. Optional `parentId` lists direct children of that folder.
        Required scope: `videos:read`.
      x-required-scope: videos:read
      parameters:
        - name: parentId
          in: query
          required: false
          schema: {type: string, minLength: 1}
          description: When set, only folders whose parent is this id.
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Folders in the caller's library.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/FolderList"}
              example:
                folders:
                  - id: fold_client
                    name: Client A
                    parentId: null
                    path: [Client A]
                  - id: fold_q1
                    name: Q1
                    parentId: fold_client
                    path: [Client A, Q1]
        "403": {$ref: "#/components/responses/Forbidden"}
        "500": {$ref: "#/components/responses/InternalError"}
    post:
      operationId: createFolder
      tags: [Folders]
      summary: Create a folder or subfolder
      description: |
        Creates a Studio folder in the bound personal or workspace library.
        Omit `parentId` for a root folder. Nesting is limited to 3 levels.
        The reserved name All Videos cannot be created or used as a parent.
        Required scope: `videos:write`.
      x-required-scope: videos:write
      parameters:
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateFolderRequest"}
            examples:
              root:
                summary: Root folder
                value: {name: Client A}
              nested:
                summary: Subfolder
                value: {name: Q1, parentId: fold_client}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "201":
          description: Folder created.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Folder"}
              example:
                id: fold_q1
                name: Q1
                parentId: fold_client
                path: [Client A, Q1]
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /folders/{folderId}:
    get:
      operationId: getFolder
      tags: [Folders]
      summary: Get a library folder
      description: |
        Returns one Studio folder in the bound library, including `parentId`
        and name `path`. Required scope: `videos:read`.
      x-required-scope: videos:read
      parameters:
        - name: folderId
          in: path
          required: true
          schema: {type: string, minLength: 1}
          description: Studio folder id.
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: The folder.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Folder"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/professional-hosting:
    get:
      operationId: getProfessionalHosting
      tags: [Hosting]
      summary: Get professional hosting status
      description: |
        Returns whether the video is on Shaka or Mux professional hosting, and
        whether the caller can activate Mux. Required scope: `videos:read`.
        Pro and Business plans can activate. Poll this after
        `activateProfessionalHosting` until `status` is `ready`.
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Current hosting state.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/ProfessionalHosting"}
              example:
                videoId: vid_abc
                status: inactive
                canActivate: true
                packagingProvider: shaka
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}
    post:
      operationId: activateProfessionalHosting
      tags: [Hosting]
      summary: Activate professional hosting
      description: |
        Uploads the current source to Mux and queues transcode, the same as
        Studio Activate Pro Hosting. Idempotent when Mux is already live or
        processing. Required scope: `videos:write`. Requires a Pro or Business
        plan. Empty JSON body is valid.
      x-required-scope: videos:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ActivateProfessionalHostingRequest"}
            example: {}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Already on Mux or Mux transcode is in progress.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/ProfessionalHosting"}
              example:
                videoId: vid_abc
                status: ready
                canActivate: false
                packagingProvider: mux
        "202":
          description: Mux ingest queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/ProfessionalHosting"}
              example:
                videoId: vid_abc
                status: queued
                canActivate: false
                packagingProvider: mux
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "422": {$ref: "#/components/responses/InvalidRequest"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/packaging:
    post:
      operationId: createPackaging
      tags: [Packaging]
      summary: Generate packaging assets
      description: |
        Enables one or more title, description, or zero-prompt-thumbnail
        automations. Send fields directly or under `automationOptions`; the
        nested form is recommended. Already-pending automation types are
        reported separately and are not duplicated.
        Required scope: `packaging:write`.
      x-required-scope: packaging:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreatePackagingRequest"}
            example:
              automationOptions:
                generateTitle: {enabled: true, titleType: informational, overwriteMode: fill-if-empty}
                generateDescription: {enabled: true, descriptionType: youtube-description, overwriteMode: fill-if-empty}
                zeroPromptThumbnail:
                  enabled: true
                  characterCount: 0
                  pipelineMode: thumbnail-precision
                  autoDetectContentReferences: true
                  contentSlotCount: 1
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "202":
          description: New automation jobs queued; pending duplicates identified.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreatePackagingResponse"}
              example:
                registered: true
                alreadyPending: []
                jobs:
                  - {id: "packaging_vid_01JZ8M6A_generateTitle", type: packaging, status: queued, automationType: generateTitle}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "429": {$ref: "#/components/responses/InsufficientCreditsOrRateLimit"}
        "500": {$ref: "#/components/responses/InternalError"}
    get:
      operationId: getPackaging
      tags: [Packaging]
      summary: Get packaging state
      description: "Returns current video metadata plus all associated packaging automation runs. Required scope: `videos:read`."
      x-required-scope: videos:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Current packaging fields and run statuses.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Packaging"}
              example:
                title: Five ways to launch faster
                description: A practical walkthrough of our launch workflow.
                posterUrl: "https://cdn.braiv.co/poster.jpg"
                runs: [{id: run_01, automationType: generateTitle, status: complete, jobId: "packaging_vid_01JZ8M6A_generateTitle"}]
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/downloads:
    post:
      operationId: createDownload
      tags: [Downloads]
      summary: Prepare a download
      description: "Queues preparation of a language video, source asset, or bundle. Account policy determines full, sample, or unavailable download tier. Required scope: `downloads:write`."
      x-required-scope: downloads:write
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateDownloadRequest"}
            example: {artifact: language_video, language: es}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "202":
          description: Download preparation queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CreateDownloadResponse"}
              example: {downloadRequestId: dl_01, jobId: "download_dl_01", artifact: language_video, language: es, tier: full, status: queued}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "422":
          description: Downloads are unavailable, or the selected dub is only a preview.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "500": {$ref: "#/components/responses/InternalError"}

  /downloads/{downloadRequestId}:
    get:
      operationId: getDownload
      tags: [Downloads]
      summary: Get a prepared download
      description: "Returns a URL when ready. Failed download preparation is a terminal 200 response with `status: failed` and an error object. Required scope: `downloads:write`."
      x-required-scope: downloads:write
      parameters:
        - {$ref: "#/components/parameters/DownloadRequestId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Download is ready or terminally failed.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Download"}
              examples:
                ready:
                  value: {id: dl_01, status: ready, tier: full, truncated: false, maxDurationSeconds: null, url: "https://storage.googleapis.com/...zip", error: null, jobId: "download_dl_01"}
                failed:
                  value: {id: dl_01, status: failed, tier: full, truncated: false, maxDurationSeconds: null, url: null, error: {message: Render failed}, jobId: "download_dl_01"}
        "202":
          description: Download is still queued or processing.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Download"}
              example: {id: dl_01, status: processing, tier: full, truncated: false, maxDurationSeconds: null, url: null, error: null, jobId: "download_dl_01"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /jobs/{jobId}:
    get:
      operationId: getJob
      tags: [Jobs]
      summary: Get a job
      description: "Resolves a deterministic job ID into the common queued, processing, ready, or failed projection. Required scope: `jobs:read`."
      x-required-scope: jobs:read
      parameters:
        - {$ref: "#/components/parameters/JobId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Current job projection.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
              example: {id: "dub_vid_01JZ8M6A_es_plus", type: dub, status: ready, videoId: vid_01JZ8M6A, result: {audioUrl: "https://storage.googleapis.com/es.mp3", completion: complete}, details: {language: es, product: plus}}
        "400":
          description: Job ID is malformed or names an unknown job type.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /videos/{videoId}/jobs:
    get:
      operationId: listVideoJobs
      tags: [Jobs]
      summary: List jobs for a video
      description: "Includes import, transcript, dub, captions, shorts, thumbnail, packaging, and download projections. Required scope: `jobs:read`."
      x-required-scope: jobs:read
      parameters:
        - {$ref: "#/components/parameters/VideoId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: All currently discoverable jobs for the video.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/JobList"}
              example:
                jobs:
                  - {id: "import_vid_01JZ8M6A", type: import, status: ready}
                  - {id: "transcript_vid_01JZ8M6A", type: transcript, status: ready}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

  /webhook-endpoints:
    post:
      operationId: createWebhookEndpoint
      tags: [Webhooks]
      summary: Create a webhook endpoint
      description: "Registers an HTTPS destination. Store the signing secret immediately; it is returned only once. Required scope: `webhooks:write`."
      x-required-scope: webhooks:write
      parameters:
        - {$ref: "#/components/parameters/IdempotencyKey"}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateWebhookEndpointRequest"}
            example: {url: "https://example.com/webhooks/braiv"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "201":
          description: Endpoint created and one-time signing secret returned.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/WebhookEndpointCreated"}
              example: {id: wh_01, url: "https://example.com/webhooks/braiv", secret: whsec_YWJjMTIz, events: [video.ready, video.failed, job.ready, job.failed]}
        "400": {$ref: "#/components/responses/InvalidRequest"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "409": {$ref: "#/components/responses/IdempotencyConflict"}
        "500": {$ref: "#/components/responses/InternalError"}
    get:
      operationId: listWebhookEndpoints
      tags: [Webhooks]
      summary: List webhook endpoints
      description: "Lists active endpoints owned by the caller. Signing secrets are never included. Required scope: `webhooks:write`."
      x-required-scope: webhooks:write
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "200":
          description: Active webhook endpoints.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/WebhookEndpointList"}
              example:
                endpoints:
                  - {id: wh_01, url: "https://example.com/webhooks/braiv", createdAt: {_seconds: 1788221722, _nanoseconds: 0}}
        "403": {$ref: "#/components/responses/Forbidden"}
        "500": {$ref: "#/components/responses/InternalError"}

  /webhook-endpoints/{endpointId}:
    delete:
      operationId: deleteWebhookEndpoint
      tags: [Webhooks]
      summary: Delete a webhook endpoint
      description: "Soft-deletes an endpoint. Future events will no longer be delivered to it. Required scope: `webhooks:write`."
      x-required-scope: webhooks:write
      parameters:
        - {$ref: "#/components/parameters/EndpointId"}
      responses:
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "204":
          description: Endpoint deleted; no response body.
        "403":
          description: The scope is missing or the endpoint belongs to another principal.
          content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
        "404": {$ref: "#/components/responses/NotFound"}
        "500": {$ref: "#/components/responses/InternalError"}

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: "Scoped Braiv Public API key supplied as `Authorization: Bearer <key>`."
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Scoped Braiv Public API key supplied directly in the `X-Api-Key` header.

  parameters:
    VideoId:
      name: videoId
      in: path
      required: true
      description: Braiv library video document ID.
      schema: {type: string, minLength: 1}
      example: vid_01JZ8M6A
    Lang:
      name: lang
      in: path
      required: true
      description: Target language code used by the video, such as `es` or `pt-BR`.
      schema: {$ref: "#/components/schemas/LanguageCode"}
      example: es
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum videos to return. Values above 100 are capped at 100.
      schema: {type: integer, minimum: 1, maximum: 100, default: 20}
    PageToken:
      name: pageToken
      in: query
      required: false
      description: Last video ID from the previous page. An unknown token is ignored.
      schema: {type: string, minLength: 1}
    SubtitleFormat:
      name: format
      in: query
      required: false
      description: |
        Subtitle file format: `srt` returns a SubRip file for editors and
        general playback tools; `vtt` returns a WebVTT file for HTML5 players
        and web applications.
      schema:
        type: string
        enum: [srt, vtt]
        default: srt
        description: "`srt` returns SubRip text; `vtt` returns WebVTT text."
    DubProductQuery:
      name: product
      in: query
      required: false
      description: Voice product used when the dub was created.
      schema: {$ref: "#/components/schemas/DubProduct"}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Caller-generated key for safe POST retries. The API retains the first
        JSON response for 24 hours. Reusing a key with another path or body
        returns `409 IDEMPOTENCY_REPLAY`.
      schema: {type: string, minLength: 1}
      example: import-launch-demo-20260901
    ShortsRequestId:
      name: shortsRequestId
      in: path
      required: true
      description: Shorts request document ID returned by `createShorts`.
      schema: {type: string, minLength: 1}
      example: shorts_01JZA
    ThumbnailRequestId:
      name: thumbnailRequestId
      in: path
      required: true
      description: Thumbnail request document ID returned by `createThumbnail`.
      schema: {type: string, minLength: 1}
    DownloadRequestId:
      name: downloadRequestId
      in: path
      required: true
      description: Download request document ID returned by `createDownload`.
      schema: {type: string, minLength: 1}
    JobId:
      name: jobId
      in: path
      required: true
      description: Deterministic job ID returned by a create operation or job listing.
      schema: {type: string, minLength: 3}
      example: "dub_vid_01JZ8M6A_es_plus"
    EndpointId:
      name: endpointId
      in: path
      required: true
      description: Webhook endpoint document ID.
      schema: {type: string, minLength: 1}

  responses:
    InvalidRequest:
      description: Request fields are missing, malformed, out of bounds, or mutually inconsistent.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          example: {error: {code: INVALID_ARGUMENT, message: "The request contains invalid or missing fields.", details: {}}}
    Unauthorized:
      description: A supported authentication header is missing or contains an invalid Public API key.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          examples:
            missingKey:
              summary: Authentication header missing
              value: {error: {code: UNAUTHENTICATED, message: "Missing API key.", details: {}}}
            invalidKey:
              summary: API key invalid
              value: {error: {code: INVALID_API_KEY, message: "Invalid API key", details: {}}}
    RateLimited:
      description: The API key exceeded its allowed concurrency or request rate. Retry after in-flight work completes or the limit resets.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          example: {error: {code: RATE_LIMITED, message: "API key concurrency limit reached", details: {}}}
    Forbidden:
      description: Authentication succeeded, but the required scope, workspace role, or resource permission is missing.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          examples:
            scope: {value: {error: {code: INSUFFICIENT_SCOPE, message: "The API key lacks the required scope.", details: {}}}}
    NotFound:
      description: The resource does not exist or is not accessible to the caller.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          example: {error: {code: NOT_FOUND, message: "Resource not found.", details: {}}}
    IdempotencyConflict:
      description: The Idempotency-Key was reused with a different path or request body.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          example: {error: {code: IDEMPOTENCY_REPLAY, message: "Idempotency-Key was reused with a different request.", details: {pathMatch: true, hashMatch: false}}}
    InsufficientCreditsOrRateLimit:
      description: The account has insufficient credits, or the request exceeded a rate limit.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          example: {error: {code: INSUFFICIENT_CREDITS, message: "Not enough credits.", details: {required: 60, available: 20, shortfall: 40}}}
    InternalError:
      description: An unexpected server error occurred.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          example: {error: {code: INTERNAL, message: "An unexpected error occurred.", details: {}}}

  schemas:
    LanguageCode:
      type: string
      minLength: 2
      maxLength: 16
      pattern: "^[A-Za-z]{2,3}(?:-[A-Za-z]{2,4})?$"
      description: ISO-style language or locale code accepted by the underlying product.
      examples: [en, es, pt-BR]
    Health:
      type: object
      additionalProperties: false
      required: [ok, version]
      properties:
        ok: {type: boolean, description: Whether the HTTP service is running.}
        version: {type: string, description: Deployed public API version.}
    JobStatus:
      type: string
      enum: [queued, processing, ready, failed]
      description: |
        Normalized lifecycle shared by jobs and video projections:
        `queued` is waiting to start, `processing` is running, `ready` completed
        successfully, and `failed` reached a terminal error.
    DubProduct:
      type: string
      enum: [plus, premium]
      default: plus
      description: |
        Dubbing voice product:

        - `plus` — expressive multilingual dubbing using a Braiv voice.
        - `premium` — dubbing with a cloned voice created from each speaker's source samples.
    Job:
      type: object
      required: [id, type, status]
      properties:
        id: {type: string, description: "Deterministic, pollable job ID."}
        type:
          type: string
          enum: [import, transcript, dub, captions, shorts, thumbnail, packaging, download]
          description: Product workflow represented by the job.
        status: {$ref: "#/components/schemas/JobStatus", description: Current normalized job lifecycle state.}
        videoId: {type: string, description: Parent video when applicable.}
        language: {$ref: "#/components/schemas/LanguageCode", description: Target language for a dub or caption job.}
        product: {$ref: "#/components/schemas/DubProduct", description: Dubbing product used by a dub job.}
        automationType: {type: string, enum: [generateTitle, generateDescription, zeroPromptThumbnail], description: Packaging automation when type is packaging.}
        result: {type: object, additionalProperties: true, description: Type-specific ready-state artifacts.}
        error: {$ref: "#/components/schemas/JobError", description: Terminal failure details when status is failed.}
        details: {type: object, additionalProperties: true, description: Type-specific identifiers and context.}
    JobError:
      type: object
      required: [code, message]
      properties:
        code: {type: string, description: Machine-readable job failure code.}
        message: {type: string, description: Human-readable job failure explanation.}
    JobList:
      type: object
      additionalProperties: false
      required: [jobs]
      properties:
        jobs: {type: array, items: {$ref: "#/components/schemas/Job"}, description: Job projections associated with the request or video.}
    VideoStatusRef:
      type: object
      required: [status]
      properties:
        status: {$ref: "#/components/schemas/JobStatus", description: Current lifecycle state for this video subresource.}
        language: {$ref: "#/components/schemas/LanguageCode", description: Language represented by the subresource.}
    Video:
      type: object
      required: [id, status]
      properties:
        id: {type: string, description: Library video ID.}
        status: {$ref: "#/components/schemas/JobStatus", description: Overall normalized video processing state.}
        title: {type: string, description: Current video title.}
        description: {type: string, description: Current video description.}
        duration: {type: number, minimum: 0, description: Media duration in seconds.}
        posterUrl: {type: string, format: uri, description: Current poster image URL.}
        provider: {type: string, description: Internal source provider label.}
        mediaType: {type: string, enum: [video, audio], description: Ingested media kind.}
        defaultLanguageCode: {$ref: "#/components/schemas/LanguageCode", description: Detected or selected source language.}
        url: {type: string, format: uri, description: Primary playback URL when available.}
        urlMulti:
          type: object
          description: Playback URLs keyed by language code.
          additionalProperties: {type: string, format: uri}
        workspaceId: {type: string, description: "Owning workspace, if any."}
        folderIds:
          type: array
          items: {type: string}
          description: Studio folder ids this video belongs to (`UserVideos.tags`).
        createdAt: {type: string, format: date-time, description: ISO creation timestamp.}
        import: {$ref: "#/components/schemas/VideoStatusRef", description: Import pipeline projection.}
        transcript: {$ref: "#/components/schemas/VideoStatusRef", description: Source transcription projection.}
    VideoList:
      type: object
      additionalProperties: false
      required: [videos, nextPageToken]
      properties:
        videos: {type: array, items: {$ref: "#/components/schemas/Video"}, description: Videos in descending creation order.}
        nextPageToken:
          description: Last video ID to pass to the next request, or null at the end.
          type: [string, "null"]
    CreateVideoRequest:
      type: object
      additionalProperties: false
      required: [source]
      properties:
        source: {$ref: "#/components/schemas/VideoSource", description: Media location and ingest adapter.}
        workspaceId: {type: string, minLength: 1, description: Destination workspace. Omit for the caller's personal library.}
        folderId:
          type: string
          minLength: 1
          description: "Existing Studio folder id in the same library as the import. The video is added to that folder (`UserVideos.tags`). Create folders with POST /folders or in Studio."
        products: {$ref: "#/components/schemas/ImportProducts", description: Optional workflows staged after ingest.}
    VideoSource:
      oneOf:
        - {$ref: "#/components/schemas/UploadVideoSource"}
        - {$ref: "#/components/schemas/ProviderVideoSource"}
        - {$ref: "#/components/schemas/HttpsVideoSource"}
      discriminator:
        propertyName: type
        mapping:
          upload: "#/components/schemas/UploadVideoSource"
          youtube: "#/components/schemas/ProviderVideoSource"
          dropbox: "#/components/schemas/ProviderVideoSource"
          google_drive: "#/components/schemas/ProviderVideoSource"
          https: "#/components/schemas/HttpsVideoSource"
    UploadVideoSource:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          type: string
          const: upload
          description: Reserves server-authoritative object storage for a client upload.
        fileName:
          type: string
          maxLength: 255
          description: Upload display/object filename; defaults to `upload.mp4`. Path separators are rejected.
        contentType:
          type: string
          description: Upload MIME type; `audio/*` selects the audio ingest lane.
    ProviderVideoSource:
      type: object
      additionalProperties: false
      required: [type, url]
      properties:
        type:
          type: string
          enum: [youtube, dropbox, google_drive]
          description: |
            Provider that owns the URL:

            - `youtube` — a YouTube watch or share URL.
            - `dropbox` — a publicly accessible Dropbox share URL.
            - `google_drive` — a publicly accessible Google Drive share URL.

            The value must match the provider represented by `url`.
        url:
          type: string
          format: uri
          description: Provider share or watch URL.
    HttpsVideoSource:
      type: object
      additionalProperties: false
      required: [type, url]
      properties:
        type:
          type: string
          const: https
          description: Direct HTTPS fetch.
        url:
          type: string
          format: uri
          pattern: "^https://"
          description: Publicly fetchable HTTPS media URL.
    ImportProducts:
      type: object
      additionalProperties: false
      description: Work staged to begin after the source reaches the required ingest state.
      properties:
        dubs:
          type: array
          items: {$ref: "#/components/schemas/ImportDubRecipe"}
          description: Dub recipes grouped by product and target languages.
        shorts: {$ref: "#/components/schemas/ImportShortsRecipe", description: Shorts-family recipe staged after transcription.}
        packaging: {$ref: "#/components/schemas/ImportPackagingRecipe", description: Metadata and thumbnail recipe staged after ingest.}
    ImportDubRecipe:
      type: object
      additionalProperties: false
      required: [languages]
      properties:
        languages: {type: array, minItems: 1, uniqueItems: true, items: {$ref: "#/components/schemas/LanguageCode"}, description: Unique target languages to dub.}
        product: {$ref: "#/components/schemas/DubProduct", description: Voice product for every target language.}
    ImportShortsRecipe:
      type: object
      additionalProperties: false
      properties:
        kind:
          type: string
          enum: [shorts, promo, clips]
          default: shorts
          description: |
            Output to generate after transcription:
            `shorts` creates short highlights, `promo` creates one 45–60 second
            promotional edit, and `clips` creates 5–25 minute landscape excerpts.
        numClips:
          type: number
          minimum: 1
          maximum: 10
          default: 3
          description: Number of outputs requested. The import parser rounds this value; shorts requests support 1–10, while clips execution clamps it to 1–4. Promo requests always create one output.
        minDuration: {type: number, minimum: 15, maximum: 180, default: 15, description: Minimum length of each `shorts` output in seconds. It must be no greater than `maxDuration`; promo and clips workflows use their own duration rules.}
        maxDuration: {type: number, minimum: 15, maximum: 180, default: 180, description: Maximum length of each `shorts` output in seconds. It must be no less than `minDuration`; promo and clips workflows use their own duration rules.}
        aspectRatio:
          type: string
          enum: ["9:16", "16:9", "1:1"]
          default: "9:16"
          description: |
            Output frame shape: `9:16` is vertical, `16:9` is landscape, and
            `1:1` is square. Promos support `9:16` and `16:9`; clips are
            always generated in `16:9`.
        captionStyle:
          $ref: "#/components/schemas/CaptionStyle"
          description: |
            Caption appearance. Valid values are `default`, `karaoke`, `modern`,
            `impact`, `subtle`, `highlight`, and `pro_highlight`; see their
            visual behavior in the Create shorts request.
        clipSelectionPrompt:
          type: string
          maxLength: 2000
          example: Prioritise the practical walkthrough and exclude introductions.
          description: Natural-language guidance describing topics, speakers, or moments to prioritise when selecting clips.
    ImportPackagingRecipe:
      type: object
      additionalProperties: false
      properties:
        title: {$ref: "#/components/schemas/ImportTitleRecipe", description: Title generation settings; presence enables the job.}
        description: {$ref: "#/components/schemas/ImportDescriptionRecipe", description: Description generation settings; presence enables the job.}
        thumbnail:
          description: |
            Enables a zero-prompt thumbnail after ingest. `true` or `{}` is
            faceless Creative (`thumbnail-v2`, no characters). A full object
            uses the same fields as packaging `zeroPromptThumbnail`; `enabled`
            defaults to true. Creative uses character subjects; Precision
            (`pipelineMode: thumbnail-precision`) uses content plates or
            `autoDetectContentReferences` (opaque key-moment detect).
          anyOf:
            - type: boolean
              const: true
              description: "Faceless Creative zero-prompt (`characterCount: 0`)."
            - type: object
              additionalProperties: false
              maxProperties: 0
              description: "Empty object — same as `{enabled: true}`."
            - {$ref: "#/components/schemas/PackagingThumbnailAutomation"}
    ImportTitleRecipe:
      type: object
      additionalProperties: false
      properties:
        style:
          type: string
          enum: [funny, scary, informational, mysterious, clickbait]
          default: informational
          description: |
            Tone used for the generated title:

            - `funny` — light, clever, or relatable framing.
            - `scary` — warning, risk, or mistake-to-avoid framing.
            - `informational` — direct benefit, how-to, or structured-value framing.
            - `mysterious` — curiosity-gap framing that withholds the payoff.
            - `clickbait` — bold, high-energy framing using authority or superlatives.
    ImportDescriptionRecipe:
      type: object
      additionalProperties: false
      properties:
        type:
          type: string
          enum: [youtube-description, social-post, how-to-guide, blog-post, video-summary]
          default: youtube-description
          description: |
            Format used for generated copy:

            - `youtube-description` — an SEO-oriented YouTube description with chapters, hashtags, and tags.
            - `social-post` — a concise social post with a hook, takeaways, and hashtags.
            - `how-to-guide` — a step-by-step Markdown guide with timestamped instructions.
            - `blog-post` — a long-form Markdown article based on the video.
            - `video-summary` — a timestamped narrative summary with speaker attribution.
    CreateVideoResponse:
      type: object
      required: [videoId, status, jobs]
      properties:
        videoId: {type: string, description: Created library video ID.}
        status: {$ref: "#/components/schemas/JobStatus", description: Initial import state.}
        video: {$ref: "#/components/schemas/Video", description: Imported video projection when immediately available.}
        upload:
          type: object
          description: Upload reservation instructions returned only for upload sources.
          additionalProperties: false
          required: [objectPath, complete]
          properties:
            objectPath: {type: string, description: Server-authoritative destination object path.}
            complete: {type: string, description: Relative route used to create the upload session.}
        jobs: {type: array, items: {$ref: "#/components/schemas/Job"}, description: "Import, transcript, and recipe jobs to poll."}
    CreateVideoUploadRequest:
      type: object
      additionalProperties: false
      required: [fileName]
      properties:
        fileName: {type: string, minLength: 1, maxLength: 255, description: Filename without path separators.}
        contentType: {type: string, description: MIME type applied to the signed upload session.}
    CreateVideoUploadResponse:
      type: object
      additionalProperties: false
      required: [url, videoId, storagePath, jobId]
      properties:
        url: {type: string, format: uri, description: Signed URL used to initiate the resumable session; expires in 30 minutes.}
        videoId: {type: string, description: Reserved library video ID.}
        storagePath: {type: string, description: Server-authoritative upload object path.}
        jobId: {type: string, description: Import job ID to poll after upload.}
    CreditBundleRequest:
      type: object
      additionalProperties: false
      properties:
        durationSeconds: {type: number, exclusiveMinimum: 0, description: Settled billing duration; otherwise resolved from the source video.}
        dubs: {type: array, items: {$ref: "#/components/schemas/CreditDubSelection"}, description: Dubs included in the quote or reservation.}
        captionLanguageCodes: {type: array, uniqueItems: true, items: {$ref: "#/components/schemas/LanguageCode"}, description: Caption translation languages to price.}
        transcriptOnly: {type: boolean, default: false, description: Reserve source transcription as a standalone action.}
        shorts: {$ref: "#/components/schemas/CreditShortsSelection", description: Shorts-family bundle to price.}
        zeroPromptThumbnail: {$ref: "#/components/schemas/CreditThumbnailSelection", description: Zero-prompt thumbnail bundle to price.}
    CreditDubSelection:
      type: object
      additionalProperties: false
      required: [languages]
      properties:
        product: {$ref: "#/components/schemas/DubProduct", description: Dubbing product to price.}
        languages: {type: array, minItems: 1, uniqueItems: true, items: {$ref: "#/components/schemas/LanguageCode"}, description: Target languages priced separately.}
    CreditShortsSelection:
      type: object
      additionalProperties: false
      required: [kind]
      properties:
        kind:
          type: string
          enum: [shorts, promo, clips]
          description: |
            Shorts-family product to price:
            `shorts` prices short highlights, `promo` prices one promotional
            edit, and `clips` prices long-form excerpts.
        numClips: {type: integer, minimum: 1, default: 1, description: Number of clips priced; defaults to one.}
        includeWatermark: {type: boolean, default: false, description: Affects clip pricing and subscriber rebates.}
        isSubscriber: {type: boolean, default: false, description: Pricing hint consumed by the bundle pricer.}
        includeSoundtrack: {type: boolean, default: false, description: Adds the promo soundtrack product.}
        includeGeneratedThumbnail: {type: boolean, default: false, description: Adds generated thumbnail pricing to each long clip.}
    CreditThumbnailSelection:
      type: object
      additionalProperties: false
      required: [enabled]
      properties:
        enabled: {type: boolean, description: Only `true` creates thumbnail line items.}
        referenceProducts: {type: array, items: {type: string}, description: Additional catalog products used as references.}
        characterCount: {type: integer, minimum: 0, maximum: 3, default: 0, description: Character reference slots added to pricing.}
        translateLanguageCodes: {type: array, uniqueItems: true, items: {$ref: "#/components/schemas/LanguageCode"}, description: Thumbnail translation languages priced individually.}
        autoDetectContentMoments:
          type: boolean
          default: false
          description: |
            Adds the `content-moment-detect` line item. Set this when the
            generation recipe uses Precision `autoDetectContentReferences`.
            Quote field name stays `autoDetectContentMoments` for pricing.
    CreditQuoteLine:
      type: object
      required: [actionKey, product, credits]
      properties:
        actionKey: {type: string, enum: [dub, captions, transcript, zero-prompt-thumbnail, thumbnail-translate, content-moment-detect, lipsync, shorts-clip, promo, longclip], description: Billable action consumed by downstream work.}
        product: {type: string, description: Internal catalog product used for pricing.}
        targetLanguageCode: {$ref: "#/components/schemas/LanguageCode", description: Language charged by this line, when applicable.}
        credits: {type: number, minimum: 0, description: Exact credits quoted or reserved.}
        description: {type: string, description: Human-readable line-item summary.}
    CreditQuote:
      type: object
      additionalProperties: false
      required: [total, durationSeconds, pricingVersion, balance, usageBillingActive, shortfall, sufficient, breakdown]
      properties:
        total: {type: number, minimum: 0, description: Total credits required by all line items.}
        durationSeconds: {type: number, minimum: 0, description: Settled duration used for pricing.}
        pricingVersion: {type: string, description: Pricing ruleset identifier.}
        balance: {type: number, description: Current account credit balance.}
        usageBillingActive: {type: boolean, description: "If true, wallet shortfall does not block the operation."}
        shortfall: {type: number, minimum: 0, description: Additional wallet credits required; zero with usage billing.}
        sufficient: {type: boolean, description: Whether billing can cover the quoted total.}
        breakdown: {type: array, items: {$ref: "#/components/schemas/CreditQuoteLine"}, description: Per-action pricing lines.}
    CreditReservationLine:
      allOf:
        - {$ref: "#/components/schemas/CreditQuoteLine"}
        - type: object
          required: [lineItemId]
          properties:
            lineItemId: {type: string, description: Escrow line item passed to the matching create operation.}
    CreditReservation:
      type: object
      additionalProperties: false
      required: [reservationId, total, durationSeconds, lineItems]
      properties:
        reservationId: {type: string, description: Escrow identifier supplied to product requests.}
        total: {type: number, minimum: 0, description: Total credits moved into escrow.}
        durationSeconds: {type: number, minimum: 0, description: Settled duration used to reserve credits.}
        lineItems: {type: array, minItems: 1, items: {$ref: "#/components/schemas/CreditReservationLine"}, description: Escrow lines consumed by matching product actions.}
    TranscriptSegment:
      type: object
      required: [start, end, speaker, text, deleted]
      properties:
        start: {type: number, minimum: 0, description: Segment start in seconds.}
        end: {type: number, minimum: 0, description: Segment end in seconds; not earlier than start.}
        speaker: {type: string, description: Speaker ID assigned to the segment.}
        text: {type: string, description: Transcript text for this time range.}
        deleted: {type: boolean, description: Whether the segment is excluded from active playback.}
    Speaker:
      type: object
      required: [id, label]
      properties:
        id: {type: string, description: Speaker identifier referenced by segments.}
        label: {type: string, description: Display label for the speaker.}
    Transcript:
      type: object
      required: [status, userVideoId]
      properties:
        status: {type: string, enum: [not_found, processing, ready, failed], description: Source transcript lifecycle.}
        userVideoId: {type: string, description: Parent library video ID.}
        language: {$ref: "#/components/schemas/LanguageCode", description: Detected source language.}
        segments: {type: array, items: {$ref: "#/components/schemas/TranscriptSegment"}, description: Ordered transcript segments when ready.}
        speakers: {type: array, items: {$ref: "#/components/schemas/Speaker"}, description: Speaker labels referenced by segments.}
        wordAlignmentUrl: {type: [string, "null"], format: uri, description: Temporary word-level alignment download URL.}
        wordAlignmentStoragePath: {type: [string, "null"], description: Canonical storage path for word alignment.}
        processingStatus: {type: string, description: More detailed internal pipeline status when present.}
    Subtitles:
      type: object
      additionalProperties: false
      required: [status, videoId, language, format, downloadUrl]
      properties:
        status: {type: string, const: ready, description: Confirms the subtitle artifact is ready.}
        videoId: {type: string, description: Parent library video ID.}
        language: {$ref: "#/components/schemas/LanguageCode", description: Subtitle language.}
        format: {type: string, enum: [srt, vtt], description: Returned subtitle serialization.}
        downloadUrl: {type: string, format: uri, description: Temporary subtitle file URL.}
    SubtitlesProcessing:
      type: object
      additionalProperties: false
      required: [status, videoId]
      properties:
        status: {type: string, const: processing, description: Indicates transcription prerequisites are incomplete.}
        videoId: {type: string, description: Parent library video ID.}
    TranslateCaptionsRequest:
      type: object
      additionalProperties: false
      required: [languages]
      properties:
        languages: {type: array, minItems: 1, uniqueItems: true, items: {$ref: "#/components/schemas/LanguageCode"}, description: Unique caption translation targets.}
        reservationId: {type: string, description: Optional credit reservation to consume.}
        reservationLineItemIdByLanguage:
          type: object
          description: Reservation line-item IDs keyed by requested language.
          additionalProperties: {type: string}
    TranslateCaptionsResponse:
      type: object
      additionalProperties: false
      required: [jobs]
      properties:
        jobs: {type: array, items: {$ref: "#/components/schemas/Job"}, description: One queued caption job per language.}
    CreateDubsRequest:
      type: object
      additionalProperties: false
      required: [languages]
      properties:
        languages: {type: array, minItems: 1, uniqueItems: true, items: {$ref: "#/components/schemas/LanguageCode"}, description: Unique dub target languages.}
        product: {$ref: "#/components/schemas/DubProduct", description: Voice product used for all targets.}
        sourceVoiceover:
          type: string
          enum: [original, optimized]
          default: original
          description: |
            Source narration used to produce the dub:
            `original` uses the video's source transcript and narration;
            `optimized` uses the latest ready Optimize/redub transcript.
            Choose `optimized` only after that workflow is ready.
        reservationId: {type: string, description: Optional credit escrow to consume.}
        reservationLineItemIdByLanguage:
          type: object
          additionalProperties: {type: string}
          description: Reservation line-item IDs keyed by requested language.
    CreateDubsResponse:
      type: object
      required: [videoId, jobs]
      properties:
        videoId: {type: string, description: Parent library video ID.}
        jobs: {type: array, items: {$ref: "#/components/schemas/Job"}, description: One queued dub job per language.}
        balance: {type: number, description: Remaining credit balance when returned by the dubbing service.}
    DubListItem:
      type: object
      required: [id, language, product, status, jobId]
      properties:
        id: {type: string, description: Internal dubbing request ID.}
        language: {$ref: "#/components/schemas/LanguageCode", description: Requested dub language.}
        product: {type: string, description: Public plus/premium slug when recognized; otherwise the stored internal product string.}
        status: {type: string, description: Stored dubbing request status.}
        completion: {type: string, description: "Stored completion status, such as preview_ready or complete."}
        jobId: {type: string, description: Pollable dub job ID.}
    Dub:
      type: object
      required: [status, userVideoId, languageCode, jobId]
      properties:
        status: {type: string, enum: [processing, ready, failed], description: Public dub artifact lifecycle.}
        userVideoId: {type: string, description: Parent library video ID.}
        languageCode: {$ref: "#/components/schemas/LanguageCode", description: Dub target language.}
        dubbingRequestId: {type: string, description: Internal dubbing request ID.}
        dubbingStatus: {type: string, description: Detailed pipeline request status.}
        completionStatus: {type: string, description: Coverage state such as preview_ready or complete.}
        audioUrl: {type: [string, "null"], format: uri, description: Downloadable dubbed audio URL when ready.}
        audioTrackId: {type: [string, "null"], description: Backing audio track ID when created.}
        segments: {type: array, items: {$ref: "#/components/schemas/TranscriptSegment"}, description: Target-language transcript segments when available.}
        speakers: {type: array, items: {$ref: "#/components/schemas/Speaker"}, description: Target-language speaker labels.}
        jobId: {type: string, description: Pollable dub job ID.}
    DubList:
      type: object
      additionalProperties: false
      required: [dubs]
      properties:
        dubs: {type: array, items: {$ref: "#/components/schemas/DubListItem"}, description: Dubbing request projections for the video.}
    CaptionStyle:
      type: string
      enum: [default, karaoke, modern, impact, subtle, highlight, pro_highlight]
      default: default
      description: |
        Visual preset used to burn captions into the generated video.

        - `default` — uppercase text with an outline and soft shadow.
        - `karaoke` — uppercase text that progressively highlights words as they are spoken.
        - `modern` — uppercase text that highlights the currently spoken word.
        - `impact` — one large uppercase word at a time with a hard shadow.
        - `subtle` — smaller mixed-case text on a semi-transparent dark background.
        - `highlight` — the current word appears on a solid highlight-colour box while surrounding words are dimmed.
        - `pro_highlight` — a compact, outline-free single-word highlight treatment.
    CaptionPosition:
      type: string
      enum: [top, middle, bottom]
      default: bottom
      description: |
        Vertical caption placement within the video frame:
        `top` uses the upper safe area, `middle` centres the captions, and
        `bottom` uses the lower safe area.
    SoundtrackGenre:
      type: string
      enum: [orchestral, cinematic, synth, acoustic, pop, hip hop, ambient, trip hop, electronic, rock, jazz, classical]
      default: cinematic
      description: |
        Music style used when `includeSoundtrack` is `true`. Send the lowercase
        value exactly as shown; `hip hop` and `trip hop` contain spaces.

        - `orchestral` — grand arrangements with strings, brass, woodwinds, and percussion.
        - `cinematic` — dramatic, emotional film-score music that builds tension.
        - `synth` — modern synthesizers, electronic textures, and digital production.
        - `acoustic` — warm, organic guitar, piano, and string instrumentation.
        - `pop` — catchy melodies, modern drums, bass, and accessible high-energy production.
        - `hip hop` — beats, bass, drums, samples, and urban rhythm.
        - `ambient` — spacious atmospheric pads with minimal rhythm.
        - `trip hop` — moody downtempo beats, deep bass, and atmospheric synths.
        - `electronic` — synthesizers, electronic drums, digital effects, and EDM-style production.
        - `rock` — energetic electric guitars, drums, and bass.
        - `jazz` — saxophone, piano, double bass, drums, and a swing or improvisational feel.
        - `classical` — refined traditional orchestral composition.

        An unrecognised value falls back to `cinematic`.
    CreateShortsRequest:
      anyOf:
        - {$ref: "#/components/schemas/CreateClipRequest"}
        - {$ref: "#/components/schemas/CreatePromoRequest"}
        - {$ref: "#/components/schemas/CreateLongClipRequest"}
      discriminator:
        propertyName: kind
        mapping:
          shorts: "#/components/schemas/CreateClipRequest"
          promo: "#/components/schemas/CreatePromoRequest"
          clips: "#/components/schemas/CreateLongClipRequest"
    CreateClipRequest:
      title: Shorts
      description: Generate short highlight videos with configurable duration, framing, and captions.
      type: object
      additionalProperties: false
      properties:
        kind: {type: string, const: shorts, default: shorts, description: Selects short highlight generation. You may omit this field because `shorts` is the default.}
        numClips: {type: integer, minimum: 1, maximum: 10, default: 3, description: Number of separate highlight videos to generate. Send a JSON integer from 1 to 10.}
        minDuration: {type: number, minimum: 15, maximum: 180, default: 15, description: Shortest acceptable clip length in seconds. Send a JSON number from 15 to 180 that is no greater than `maxDuration`.}
        maxDuration: {type: number, minimum: 15, maximum: 180, default: 180, description: Longest acceptable clip length in seconds. Send a JSON number from 15 to 180 that is no less than `minDuration`.}
        aspectRatio:
          type: string
          enum: ["9:16", "16:9", "1:1"]
          default: "9:16"
          description: |
            Output frame shape: `9:16` is vertical for Shorts, Reels, and
            TikTok; `16:9` is landscape; and `1:1` is square.
        captionStyle:
          $ref: "#/components/schemas/CaptionStyle"
          description: |
            Visual preset used to burn captions into each clip.

            - `default` — uppercase text with an outline and soft shadow.
            - `karaoke` — uppercase text that progressively highlights words as they are spoken.
            - `modern` — uppercase text that highlights the currently spoken word.
            - `impact` — one large uppercase word at a time with a hard shadow.
            - `subtle` — smaller mixed-case text on a semi-transparent dark background.
            - `highlight` — the current word appears on a solid highlight-colour box while surrounding words are dimmed.
            - `pro_highlight` — a compact, outline-free single-word highlight treatment.
        captionPosition:
          $ref: "#/components/schemas/CaptionPosition"
          description: |
            Vertical caption placement: `top` uses the upper safe area,
            `middle` centres the captions, and `bottom` uses the lower safe area.
        clipSelectionPrompt:
          type: string
          maxLength: 2000
          example: Prioritise practical explanations from the main speaker and avoid introductions.
          description: |
            Optional natural-language guidance for choosing moments from the
            source video. Describe topics, speakers, or moments to prioritise.
            The value is trimmed, must be no more than 2,000 characters, and an
            empty string is treated as omitted.
    CreatePromoRequest:
      title: Promo
      description: Generate one 45–60 second promotional edit with optional soundtrack.
      type: object
      additionalProperties: false
      required: [kind]
      properties:
        kind: {type: string, const: promo, description: Selects promotional video generation.}
        totalDuration: {type: number, minimum: 45, maximum: 60, default: 60, description: Target duration of the finished promo in seconds. Send a JSON number from 45 to 60.}
        aspectRatio:
          type: string
          enum: ["9:16", "16:9"]
          default: "16:9"
          description: "Output frame shape: `9:16` is vertical for mobile feeds and `16:9` is landscape."
        captionStyle:
          description: |
            Visual preset used to burn captions into the promo. Send `null` to
            generate the promo without captions.

            - `default` — uppercase text with an outline and soft shadow.
            - `karaoke` — uppercase text that progressively highlights words as they are spoken.
            - `modern` — uppercase text that highlights the currently spoken word.
            - `impact` — one large uppercase word at a time with a hard shadow.
            - `subtle` — smaller mixed-case text on a semi-transparent dark background.
            - `highlight` — the current word appears on a solid highlight-colour box while surrounding words are dimmed.
            - `pro_highlight` — a compact, outline-free single-word highlight treatment.
          oneOf:
            - {$ref: "#/components/schemas/CaptionStyle"}
            - {type: "null"}
          default: modern
        captionPosition:
          $ref: "#/components/schemas/CaptionPosition"
          description: |
            Vertical caption placement when captions are enabled: `top` uses
            the upper safe area, `middle` centres the captions, and `bottom`
            uses the lower safe area.
        includeSoundtrack: {type: boolean, default: true, description: Set to `true` to generate and mix background music beneath the dialogue. A soundtrack adds 20 credits to the promo request.}
        soundtrackGenre:
          $ref: "#/components/schemas/SoundtrackGenre"
          description: |
            Music style used when `includeSoundtrack` is `true`. Send one of
            these lowercase values; `hip hop` and `trip hop` contain spaces.

            - `orchestral` — grand strings, brass, woodwinds, and percussion.
            - `cinematic` — dramatic, emotional film-score music.
            - `synth` — synthesizers and modern electronic textures.
            - `acoustic` — warm guitar, piano, and string instrumentation.
            - `pop` — catchy melodies and accessible high-energy production.
            - `hip hop` — beats, bass, drums, samples, and urban rhythm.
            - `ambient` — spacious atmospheric pads with minimal rhythm.
            - `trip hop` — moody downtempo beats, deep bass, and atmospheric synths.
            - `electronic` — electronic drums, digital effects, and EDM-style production.
            - `rock` — energetic electric guitars, drums, and bass.
            - `jazz` — saxophone, piano, double bass, drums, and swing rhythms.
            - `classical` — refined traditional orchestral composition.

            An unrecognised value falls back to `cinematic`.
    CreateLongClipRequest:
      title: Clips
      description: Generate one to four 5–25 minute landscape excerpts selected from long source media.
      type: object
      additionalProperties: false
      required: [kind]
      properties:
        kind: {type: string, const: clips, description: Selects long-form clip generation.}
        numClips: {type: integer, minimum: 1, maximum: 4, default: 2, description: Number of separate long-form excerpts to generate. Send a JSON integer from 1 to 4. Each excerpt is selected within the fixed 5–25 minute duration range; values outside 1–4 are clamped.}
        captionStyle:
          description: |
            Visual preset used to burn captions into each long clip. Send
            `null` to generate clips without captions.

            - `default` — uppercase text with an outline and soft shadow.
            - `karaoke` — uppercase text that progressively highlights words as they are spoken.
            - `modern` — uppercase text that highlights the currently spoken word.
            - `impact` — one large uppercase word at a time with a hard shadow.
            - `subtle` — smaller mixed-case text on a semi-transparent dark background.
            - `highlight` — the current word appears on a solid highlight-colour box while surrounding words are dimmed.
            - `pro_highlight` — a compact, outline-free single-word highlight treatment.
          oneOf:
            - {$ref: "#/components/schemas/CaptionStyle"}
            - {type: "null"}
          default: modern
        captionPosition:
          $ref: "#/components/schemas/CaptionPosition"
          description: |
            Vertical caption placement when captions are enabled: `top` uses
            the upper safe area, `middle` centres the captions, and `bottom`
            uses the lower safe area.
        clipSelectionPrompt:
          type: string
          maxLength: 2000
          example: Focus on the complete walkthrough and exclude the opening promotion.
          description: |
            Optional natural-language guidance for choosing long-form excerpts.
            Describe the topic, speaker, or narrative section to prioritise. The
            value is trimmed, must be no more than 2,000 characters, and an empty
            string is treated as omitted.
    CreateShortsResponse:
      type: object
      additionalProperties: false
      required: [shortsRequestId, jobId, creditsSpent, balance]
      properties:
        shortsRequestId: {type: string, description: Created shorts-family request ID.}
        jobId: {type: string, description: Pollable shorts job ID.}
        creditsSpent: {type: number, minimum: 0, description: Credits charged or consumed from escrow.}
        balance: {type: number, description: Remaining account credit balance.}
    ShortsRequestStatus:
      type: string
      enum: [pending, queued, processing, complete, partial_failed, failed]
      description: |
        Shorts request lifecycle: `pending` and `queued` are waiting to run,
        `processing` is generating outputs, `complete` produced all planned
        outputs, `partial_failed` produced at least one usable output, and
        `failed` produced no usable result. Complete and partial failure
        responses use HTTP 200; a terminal failure uses HTTP 422.
    ShortsListItem:
      type: object
      required: [id, status, outputType, jobId]
      properties:
        id: {type: string, description: Shorts request ID.}
        status: {$ref: "#/components/schemas/ShortsRequestStatus", description: Stored request lifecycle.}
        outputType: {type: string, enum: [shorts, promo, clips], description: Generated output family.}
        jobId: {type: string, description: Pollable shorts job ID.}
    ShortsList:
      type: object
      additionalProperties: false
      required: [requests]
      properties:
        requests: {type: array, items: {$ref: "#/components/schemas/ShortsListItem"}, description: Shorts requests associated with the video.}
    ShortsOutput:
      type: object
      required: [outputId]
      properties:
        outputId: {type: string, description: Generated output ID.}
        type: {type: string, enum: [shorts, promo, clips], description: Output family.}
        videoUrl: {type: string, format: uri, description: Playback URL for the generated video.}
        thumbnailUrl: {type: string, format: uri, description: Generated output thumbnail URL.}
        duration: {type: number, minimum: 0, description: Output duration in seconds.}
        title: {type: string, description: Generated output title.}
        description: {type: string, description: Generated output summary.}
    Shorts:
      type: object
      required: [shortsRequestId, status, jobId, outputs]
      properties:
        shortsRequestId: {type: string, description: Shorts request ID.}
        status: {$ref: "#/components/schemas/ShortsRequestStatus", description: Current request lifecycle.}
        clipsExpected: {type: integer, minimum: 0, description: Planned number of outputs.}
        jobId: {type: string, description: Pollable shorts job ID.}
        outputs: {type: array, items: {$ref: "#/components/schemas/ShortsOutput"}, description: All available non-deleted generated outputs for the request.}
    ThumbnailCharacter:
      type: object
      additionalProperties: false
      properties:
        characterId: {type: string, description: Saved character profile ID.}
        referenceImageIds: {type: array, maxItems: 10, items: {type: string}, description: Saved reference image IDs.}
        imageUrl: {type: string, format: uri, description: Primary character reference URL.}
        imageUrls: {type: array, maxItems: 3, items: {type: string, format: uri}, description: Additional character reference URLs.}
        name: {type: string, description: Character display name.}
        position: {type: string, description: Desired composition position.}
        action: {type: string, description: Desired character pose or action.}
    Character:
      type: object
      additionalProperties: false
      required: [id, name, readyForThumbnailGeneration, referenceImageIds, referenceImages]
      properties:
        id: {type: string, description: Saved character profile ID (use as `characterId` on thumbnail create).}
        name: {type: string, description: Display name from Studio.}
        readyForThumbnailGeneration: {type: boolean, description: True when the profile has enough reference images for generation.}
        characterType:
          type: string
          enum: [thumbnail-reference, lora, hybrid]
          description: Studio character storage mode when known.
        referenceImageIds: {type: array, items: {type: string}, description: Ordered reference image ids stored on the profile.}
        referenceImages:
          type: object
          additionalProperties: {type: string, format: uri}
          description: Map of reference image id to stored download URL.
    CharacterList:
      type: object
      additionalProperties: false
      required: [characters]
      properties:
        characters: {type: array, maxItems: 100, items: {$ref: "#/components/schemas/Character"}, description: Saved characters in the caller's library.}
    BrandVoicePaletteSwatch:
      type: object
      additionalProperties: false
      required: [name, hex]
      properties:
        name: {type: string, description: Palette swatch label.}
        hex: {type: string, description: Colour as a hex string (for example `#112233`).}
    BrandVoice:
      type: object
      additionalProperties: false
      required: [id, name, isDefault, logoUrls, palette, hasThumbnailStyle]
      properties:
        id: {type: string, description: Brand Voice id (use later as `brandVoiceId`).}
        name: {type: string, description: Display name from Studio.}
        isDefault: {type: boolean, description: True when this is the library default Brand Voice.}
        logoUrls: {type: array, items: {type: string, format: uri}, description: Logo URLs from website assets when present.}
        palette: {type: array, items: {$ref: "#/components/schemas/BrandVoicePaletteSwatch"}, description: Brand colour swatches from website assets when present.}
        hasThumbnailStyle: {type: boolean, description: True when the Brand Voice has thumbnail style references or an analyzed kit.}
    BrandVoiceList:
      type: object
      additionalProperties: false
      required: [brandVoices]
      properties:
        brandVoices: {type: array, maxItems: 100, items: {$ref: "#/components/schemas/BrandVoice"}, description: Brand Voices in the caller's library.}
    Folder:
      type: object
      additionalProperties: false
      required: [id, name, parentId, path]
      properties:
        id: {type: string, description: Studio folder id. Pass as folderId on createVideo.}
        name: {type: string, description: Display name.}
        parentId:
          type: [string, "null"]
          description: Parent folder id, or null at the library root.
        path:
          type: array
          items: {type: string}
          description: Ancestor names from the root through this folder.
        workspaceId: {type: string, description: Present when the folder belongs to a workspace library.}
    FolderList:
      type: object
      additionalProperties: false
      required: [folders]
      properties:
        folders: {type: array, maxItems: 200, items: {$ref: "#/components/schemas/Folder"}, description: Folders in the caller's library.}
    CreateFolderRequest:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name: {type: string, minLength: 1, maxLength: 120, description: Folder display name. All Videos is reserved.}
        parentId: {type: string, minLength: 1, description: Existing folder id in the same library. Omit to create a root folder. Max nesting is 3 levels.}
    ProfessionalHostingStatus:
      type: string
      enum: [inactive, queued, processing, ready]
      description: inactive is Shaka or not yet activated. queued and processing are Mux ingest. ready is live Mux playback.
    ProfessionalHostingReason:
      type: string
      enum: [plan_required, missing_input]
      description: Why canActivate is false when status is inactive.
    ProfessionalHosting:
      type: object
      additionalProperties: false
      required: [videoId, status, canActivate, packagingProvider]
      properties:
        videoId: {type: string, description: Library video id.}
        status: {$ref: "#/components/schemas/ProfessionalHostingStatus"}
        canActivate: {type: boolean, description: True when the plan allows Mux and a source file is ready to upload.}
        packagingProvider:
          type: string
          enum: [shaka, mux]
          description: Current playback packager.
        reason: {$ref: "#/components/schemas/ProfessionalHostingReason"}
    ActivateProfessionalHostingRequest:
      type: object
      additionalProperties: false
      properties: {}
      description: Empty object. Idempotency-Key is optional.
    CreateThumbnailRequest:
      anyOf:
        - {$ref: "#/components/schemas/PromptThumbnailRequest"}
        - {$ref: "#/components/schemas/ZeroPromptThumbnailRequest"}
      discriminator:
        propertyName: mode
        mapping:
          prompt: "#/components/schemas/PromptThumbnailRequest"
          zero_prompt: "#/components/schemas/ZeroPromptThumbnailRequest"
    PromptThumbnailRequest:
      type: object
      additionalProperties: false
      required: [prompt]
      properties:
        mode: {type: string, const: prompt, default: prompt, description: Uses your `prompt` as the creative direction for thumbnail generation. This is the default mode.}
        prompt:
          type: string
          minLength: 1
          example: A confident presenter beside a dramatic before-and-after split screen, high contrast, clean composition.
          description: Natural-language description of the thumbnail scene, composition, subjects, and mood. The value must contain non-whitespace text.
        characters: {type: array, maxItems: 3, items: {$ref: "#/components/schemas/ThumbnailCharacter"}, description: Character references to preserve. Not valid with `thumbnail-precision`.}
        styleReferenceImageUrl: {type: string, format: uri, description: Public image URL whose visual style and layout should influence the result.}
        brandReferenceImageUrl:
          type: string
          format: uri
          description: Public image URL providing brand colours, typography, or identity cues.
        designInfluenceImageUrl: {type: string, format: uri, description: Composition influence image.}
        brandInfluence: {type: number, description: Relative brand-style influence passed to generation.}
        referenceSchemaVersion: {type: integer, enum: [1, 2], description: Reference payload model. Precision uses `2`.}
        pipelineMode:
          type: string
          enum: [thumbnail-v2, thumbnail-precision]
          default: thumbnail-v2
          description: |
            `thumbnail-v2` is Creative (character subjects). `thumbnail-precision`
            requires `contentReferenceImageUrl` or `contentReferenceImageUrls`.
            Prompt mode does not accept auto-detect flags.
        contentReferenceImageUrl: {type: string, format: uri, description: Primary Precision content plate.}
        contentReferenceImageUrls: {type: array, items: {type: string, format: uri}, description: Precision content plates (1–2).}
        brandVoiceId: {type: string, maxLength: 128, description: Studio Brand Voice id from `listBrandVoices`.}
        textOverlay: {type: string, description: Optional Precision headline.}
        omitTextOverlay: {type: boolean, description: Precision only — omit generated headline text.}
        promoteToPrimary: {type: boolean, default: false, description: Make the first generated image the video poster.}
    ZeroPromptThumbnailRequest:
      type: object
      additionalProperties: false
      required: [mode]
      properties:
        mode: {type: string, const: zero_prompt, description: Builds the thumbnail direction automatically from the video's transcript and visual context instead of requiring a creative prompt.}
        enabled: {type: boolean, const: true, description: "Optional; implied by `mode: zero_prompt`."}
        characterCount: {type: integer, minimum: 0, maximum: 3, default: 0, description: Number of people or characters to preserve from `characters`. Send a JSON integer from 0 to 3 that matches the supplied array length.}
        characters: {type: array, maxItems: 3, items: {$ref: "#/components/schemas/ThumbnailCharacter"}, description: Character references matching characterCount.}
        promoteToPrimary: {type: boolean, default: false, description: Set to `true` to make the generated thumbnail the video's primary poster when generation completes.}
        referenceSchemaVersion: {type: integer, enum: [1, 2], default: 1, description: Reference payload model. `2` is required for Precision.}
        pipelineMode:
          type: string
          enum: [thumbnail-v2, thumbnail-precision]
          default: thumbnail-v2
          description: |
            Creative (`thumbnail-v2`) vs Precision (`thumbnail-precision`). There
            is no `creative` enum value. Precision rejects characters and needs
            content plates or `autoDetectContentReferences`.
        contentReferenceImageUrl: {type: string, format: uri, description: Primary content plate when not auto-detecting.}
        contentReferenceImageUrls: {type: array, items: {type: string, format: uri}, description: Precision content plates.}
        designInfluenceImageUrl: {type: string, format: uri, description: Composition influence image.}
        styleReferenceImageUrl: {type: string, format: uri, description: Style influence image.}
        brandReferenceImageUrl: {type: string, format: uri, description: Brand influence image.}
        brandInfluence: {type: number, description: Relative brand-style influence passed to generation.}
        autoDetectCharacters: {type: boolean, default: false, description: Creative only. Returns 202 and queues character detect.}
        characterDetectApprovalMode: {type: string, enum: [auto, review], default: auto, description: Character detection workflow.}
        autoDetectContentReferences: {type: boolean, default: false, description: Precision only. Opaque key-moment detect; returns 202.}
        contentSlotCount: {type: integer, enum: [1, 2], default: 1, description: Number of Precision plates to select when auto-detecting.}
        contentDetectApprovalMode: {type: string, enum: [auto, review], default: auto, description: Content-reference detection workflow.}
        contentDetectPrompt: {type: string, description: Optional focus text that steers opaque key-moment detect.}
        textOverlay: {type: string, description: Optional Precision headline.}
        omitTextOverlay: {type: boolean, description: Precision only — omit generated headline text.}
        autoTranslateThumbnail: {type: boolean, default: false, description: Effective only with non-empty thumbnailTranslationLanguageCodes.}
        thumbnailTranslationLanguageCodes: {type: array, uniqueItems: true, items: {$ref: "#/components/schemas/LanguageCode"}, description: Languages for translated thumbnail variants.}
        brandVoiceId: {type: string, maxLength: 128, description: "Brand Voice id from `listBrandVoices`."}
    CreateThumbnailResponse:
      type: object
      required: [jobId]
      properties:
        thumbnailRequestId: {type: string, description: Created thumbnail request ID when generation starts synchronously.}
        jobId: {type: string, description: "Pollable thumbnail job ID, or packaging job ID when detect is queued (`202`)."}
        status: {type: string, description: "`queued` when a detect-backed automation was registered."}
        registered: {type: boolean, description: "Whether a new automation run was created for a `202` response."}
        thumbnailUrl: {type: string, format: uri, description: Zero-prompt result when returned synchronously.}
        outputImageUrls: {type: array, items: {type: string, format: uri}, description: Prompt-mode generated images.}
        balance: {type: number, description: Remaining account credit balance.}
    Thumbnail:
      type: object
      required: [id, status, jobId]
      properties:
        id: {type: string, description: Thumbnail request ID.}
        status: {type: string, description: Stored thumbnail request status.}
        outputImageUrls: {type: array, items: {type: string, format: uri}, description: Generated image URLs available so far.}
        jobId: {type: string, description: Pollable thumbnail job ID.}
    ThumbnailList:
      type: object
      additionalProperties: false
      required: [thumbnails]
      properties:
        thumbnails: {type: array, maxItems: 50, items: {$ref: "#/components/schemas/Thumbnail"}, description: Thumbnail requests for the video.}
    CreatePackagingRequest:
      oneOf:
        - type: object
          additionalProperties: false
          required: [automationOptions]
          properties:
            automationOptions: {$ref: "#/components/schemas/AutomationOptions", description: Packaging automations to enable.}
        - {$ref: "#/components/schemas/AutomationOptions"}
      description: "Nested `automationOptions` or the same fields at the root. At least one automation must have `enabled: true`."
    AutomationOptions:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        brandVoiceId: {type: string, maxLength: 128, description: Shared fallback brand voice for description and thumbnail automation.}
        generateTitle: {$ref: "#/components/schemas/GenerateTitleAutomation", description: Title generation configuration.}
        generateDescription: {$ref: "#/components/schemas/GenerateDescriptionAutomation", description: Description generation configuration.}
        zeroPromptThumbnail: {$ref: "#/components/schemas/PackagingThumbnailAutomation", description: Context-derived thumbnail configuration.}
    GenerateTitleAutomation:
      type: object
      additionalProperties: false
      required: [enabled, titleType]
      properties:
        enabled: {type: boolean, const: true, description: Enables title generation.}
        titleType:
          type: string
          enum: [funny, scary, informational, mysterious, clickbait]
          description: |
            Tone used for the generated title:
            `funny` is light or relatable; `scary` emphasizes warnings or
            mistakes; `informational` is direct and useful; `mysterious` creates
            a curiosity gap; and `clickbait` uses bold, high-energy framing.
        overwriteMode:
          type: string
          enum: [fill-if-empty, always, never]
          default: fill-if-empty
          description: |
            Generation policy. `fill-if-empty` is the default automation mode;
            `always` explicitly allows replacement of the current title; and
            `never` records the automation as skipped without generating a title.
    GenerateDescriptionAutomation:
      type: object
      additionalProperties: false
      required: [enabled, descriptionType]
      properties:
        enabled: {type: boolean, const: true, description: Enables description generation.}
        descriptionType:
          type: string
          enum: [youtube-description, social-post, how-to-guide, blog-post, video-summary]
          description: |
            Generated output format:
            `youtube-description` creates SEO copy with chapters and tags;
            `social-post` creates concise social copy; `how-to-guide` creates
            timestamped Markdown steps; `blog-post` creates a long-form Markdown
            article; and `video-summary` creates a timestamped narrative summary.
        overwriteMode:
          type: string
          enum: [fill-if-empty, always, never]
          default: fill-if-empty
          description: |
            Generation policy. `fill-if-empty` is the default automation mode;
            `always` explicitly allows replacement of the active description;
            and `never` records the automation as skipped without generating copy.
        brandVoiceId: {type: string, maxLength: 128, description: Brand voice profile used for copy.}
    PackagingThumbnailAutomation:
      type: object
      additionalProperties: false
      required: [enabled]
      properties:
        enabled: {type: boolean, const: true, description: Enables zero-prompt thumbnail generation.}
        characterCount: {type: integer, minimum: 0, maximum: 3, default: 0, description: Number of character references to use.}
        characters: {type: array, maxItems: 3, items: {$ref: "#/components/schemas/ThumbnailCharacter"}, description: Character references matching characterCount.}
        referenceSchemaVersion:
          type: integer
          enum: [1, 2]
          default: 1
          description: |
            Reference payload model. `1` uses the legacy aesthetic, layout, and
            design-influence interpretation. `2` separates style recreation
            from brand references and enables `thumbnail-precision`.
        pipelineMode:
          type: string
          enum: [thumbnail-v2, thumbnail-precision]
          default: thumbnail-v2
          description: |
            Thumbnail generation workflow. `thumbnail-v2` supports character,
            style, and brand references. `thumbnail-precision` uses one or two
            content-plate references and does not accept character references.
        contentReferenceImageUrl: {type: string, format: uri, description: Primary content plate reference.}
        contentReferenceImageUrls: {type: array, items: {type: string, format: uri}, description: Precision-mode content plates.}
        designInfluenceImageUrl: {type: string, format: uri, description: Composition influence image.}
        styleReferenceImageUrl: {type: string, format: uri, description: Style influence image.}
        brandReferenceImageUrl: {type: string, format: uri, description: Brand influence image.}
        brandInfluence: {type: number, description: Relative brand-style influence passed to generation.}
        autoDetectCharacters: {type: boolean, default: false, description: Not available in precision mode.}
        characterDetectApprovalMode: {type: string, enum: [auto, review], default: auto, description: Character detection workflow. `auto` accepts detected references and continues; `review` waits for approval before generation.}
        autoDetectContentReferences: {type: boolean, default: false, description: Precision only. When omitted and no content plate URLs are sent, Precision defaults to true (opaque key-moment detect).}
        contentSlotCount: {type: integer, enum: [1, 2], default: 1, description: Number of distinct content-plate references selected for precision mode. Use `1` for one reference frame or `2` for two complementary frames.}
        contentDetectApprovalMode: {type: string, enum: [auto, review], default: auto, description: Content-reference detection workflow. `auto` accepts detected frames and continues; `review` waits for approval before generation.}
        contentDetectPrompt: {type: string, description: Optional focus text that steers opaque key-moment detect.}
        textOverlay: {type: string, description: Optional Precision headline burned into the thumbnail.}
        omitTextOverlay: {type: boolean, description: When true, Precision omits generated headline text.}
        autoTranslateThumbnail: {type: boolean, default: false, description: Effective only with non-empty thumbnailTranslationLanguageCodes.}
        thumbnailTranslationLanguageCodes: {type: array, uniqueItems: true, items: {$ref: "#/components/schemas/LanguageCode"}, description: Languages for translated thumbnail variants.}
        brandVoiceId: {type: string, maxLength: 128, description: Brand voice profile used by thumbnail copy.}
    CreatePackagingResponse:
      type: object
      required: [registered, alreadyPending, jobs]
      properties:
        registered: {type: boolean, description: Whether at least one new run was created.}
        alreadyPending:
          type: array
          description: Requested automation types already in progress.
          items:
            type: string
            enum: [generateTitle, generateDescription, zeroPromptThumbnail]
            description: "`generateTitle` creates title copy, `generateDescription` creates descriptive copy, and `zeroPromptThumbnail` creates a context-derived thumbnail."
        jobs: {type: array, items: {$ref: "#/components/schemas/Job"}, description: Newly queued packaging jobs.}
    PackagingRun:
      type: object
      required: [id, automationType, status, jobId]
      properties:
        id: {type: string, description: Automation run ID.}
        automationType: {type: string, enum: [generateTitle, generateDescription, zeroPromptThumbnail], description: Generated packaging asset.}
        status: {type: string, description: Stored automation run lifecycle.}
        jobId: {type: string, description: Pollable packaging job ID.}
    Packaging:
      type: object
      required: [runs]
      properties:
        title: {type: string, description: Current video title.}
        description: {type: string, description: Current video description.}
        posterUrl: {type: string, format: uri, description: Current primary poster URL.}
        runs: {type: array, items: {$ref: "#/components/schemas/PackagingRun"}, description: Packaging automation history.}
    CreateDownloadRequest:
      type: object
      additionalProperties: false
      required: [artifact]
      properties:
        artifact:
          type: string
          enum: [language_video, source, bundle]
          description: |
            Download to prepare:
            `language_video` renders a video in the requested `language`;
            `source` returns the original source media; and `bundle` packages
            the video's available deliverables together.
        language: {$ref: "#/components/schemas/LanguageCode", description: Language video target; otherwise optional.}
      allOf:
        - if: {properties: {artifact: {const: language_video}}}
          then:
            description: Language defaults to the video's source language when omitted; it must resolve to a non-empty code.
    CreateDownloadResponse:
      type: object
      required: [downloadRequestId, jobId, artifact, language, tier, status]
      properties:
        downloadRequestId: {type: string, description: Created download request ID.}
        jobId: {type: string, description: Pollable download job ID.}
        artifact: {type: string, enum: [language_video, source, bundle], description: Requested download package.}
        language: {oneOf: [{$ref: "#/components/schemas/LanguageCode"}, {type: "null"}], description: Resolved language or null when not applicable.}
        tier: {type: string, enum: [full, sample], description: Download entitlement selected for this request.}
        status: {$ref: "#/components/schemas/JobStatus", description: Initial download preparation state.}
    Download:
      type: object
      required: [id, status, truncated, maxDurationSeconds, url, error, jobId]
      properties:
        id: {type: string, description: Download request ID.}
        status: {type: string, description: Normalized to ready or failed at terminal state; otherwise stored queue status.}
        tier: {type: string, enum: [full, sample], description: Download entitlement applied.}
        truncated: {type: boolean, description: True for sample-tier artifacts.}
        maxDurationSeconds: {type: [number, "null"], minimum: 0, description: Sample duration cap or null for full downloads.}
        url: {type: [string, "null"], format: uri, description: Prepared artifact URL when ready.}
        error: {oneOf: [{type: object, additionalProperties: true}, {type: "null"}], description: Terminal worker failure details.}
        jobId: {type: string, description: Pollable download job ID.}
    CreateWebhookEndpointRequest:
      type: object
      additionalProperties: false
      required: [url]
      properties:
        url: {type: string, format: uri, pattern: "^https://", description: Public HTTPS delivery destination.}
    WebhookEventName:
      type: string
      enum: [video.ready, video.failed, job.ready, job.failed]
      description: |
        Event delivered to the endpoint:
        `video.ready` and `video.failed` report the parent video's terminal
        import state; `job.ready` and `job.failed` report terminal product jobs
        such as transcription, dubbing, captions, shorts, or downloads.
    WebhookEndpointCreated:
      type: object
      additionalProperties: false
      required: [id, url, secret, events]
      properties:
        id: {type: string, description: Created webhook endpoint ID.}
        url: {type: string, format: uri, description: Registered HTTPS delivery destination.}
        secret: {type: string, description: Signing secret returned once at creation.}
        events: {type: array, items: {$ref: "#/components/schemas/WebhookEventName"}, description: Event names delivered to this endpoint.}
    WebhookEndpoint:
      type: object
      additionalProperties: false
      required: [id, url, createdAt]
      properties:
        id: {type: string, description: Webhook endpoint ID.}
        url: {type: string, format: uri, description: Registered HTTPS delivery destination.}
        createdAt: {$ref: "#/components/schemas/FirestoreTimestamp", description: Endpoint creation time serialized by Firestore.}
    FirestoreTimestamp:
      type: object
      additionalProperties: false
      required: [_seconds, _nanoseconds]
      description: Firestore timestamp as serialized by the live Express handler.
      properties:
        _seconds: {type: integer, description: Whole seconds since the Unix epoch.}
        _nanoseconds: {type: integer, minimum: 0, maximum: 999999999, description: Fractional nanoseconds within the second.}
    WebhookEndpointList:
      type: object
      additionalProperties: false
      required: [endpoints]
      properties:
        endpoints: {type: array, items: {$ref: "#/components/schemas/WebhookEndpoint"}, description: Active endpoints owned by the caller.}
    Error:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          description: Structured public API error envelope.
          additionalProperties: false
          required: [code, message, details]
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
              enum: [UNAUTHENTICATED, INVALID_API_KEY, INSUFFICIENT_SCOPE, PERMISSION_DENIED, ROLE_CANNOT_PRODUCE, NOT_FOUND, INVALID_ARGUMENT, UNSUPPORTED_SOURCE, DURATION_UNKNOWN, INSUFFICIENT_CREDITS, CONFLICT, ALREADY_EXISTS, IDEMPOTENCY_REPLAY, RATE_LIMITED, PREVIEW_NOT_DOWNLOADABLE, DOWNLOAD_TIER_NONE, FAILED_PRECONDITION, GONE, METHOD_NOT_ALLOWED, INTERNAL]
            message: {type: string, description: Human-readable error summary.}
            details: {type: object, additionalProperties: true, description: Machine-readable operation-specific context; empty when unavailable.}
