openapi: 3.0.0
info:
  title: YouTube Transcript API (V2)
  version: 2.1.0
  description: |
    V2 API with a simplified, user-owned transcript model.
    Fast, single-call lookups with consistent response shapes.

    **Capabilities**
    - Single-video and batch transcription (captions + optional AI ASR)
    - Direct audio/video file uploads transcribed through the ASR pipeline
    - Rich ASR configuration (asr_options): language, translation, speaker
      diarization, speech understanding, and formatting
    - ASR intelligence in results (asr_intelligence): summary, topics,
      content safety, sentiment, entities, highlights, chapters
    - Transparent credit estimates before you commit (base + add-ons)
    - Transcript management: list, fetch, delete, translate, languages
    - Playlist and channel resolution + batch history

    **Authentication**
    All endpoints (except /ping) require an API key:
    `Authorization: Bearer yt_sk_live_...`

    **Credit costs**
    - Native captions: 1 credit
    - Translation: ~1 credit / 2,500 chars
    - ASR base: 1 credit / 90 seconds (~40 credits/hour)
    - ASR add-ons: prorated per hour (see asr_options), e.g. diarization +6,
      sentiment +6, profanity +3, summarization/custom-formatting +8,
      translation +16, PII redaction +22, topic detection / content
      moderation +40 credits per hour

    **Rate limits**
    - Free: 1 req/s · Basic: 25 req/s · Pro: 50 req/s · Business: 100 req/s

    **Batch limits**
    - Free: 10 · Basic: 500 · Pro: 1,500 · Business: 3,000 videos per batch

  contact:
    name: API Support
    email: support@youtubetranscript.dev
    url: https://www.youtubetranscript.dev
  x-logo:
    url: https://www.youtubetranscript.dev/logo.svg

externalDocs:
  description: Full API Documentation
  url: https://www.youtubetranscript.dev/api-docs

servers:
  - url: https://www.youtubetranscript.dev/api/v2
    description: Production server

security:
  - BearerAuth: []

tags:
  - name: Transcribe
    description: Single video transcription and cost estimates
  - name: Uploads
    description: Upload audio/video files for ASR transcription
  - name: Batch
    description: Batch transcription (up to 3,000 videos)
  - name: Jobs
    description: List and poll async transcription jobs
  - name: Transcripts
    description: Manage stored transcripts (list, fetch, delete, translate)
  - name: Playlists
    description: Resolve playlist videos and inspect playlist batches
  - name: Channels
    description: Resolve channel uploads and inspect channel batches
  - name: Account
    description: Credits and service health

paths:
  /transcribe:
    post:
      summary: Transcribe a single YouTube video
      operationId: transcribeVideo
      tags: [Transcribe]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/TranscribeRequest'}
      responses:
        '200': {description: Completed (or requires_asr_confirmation / failed), content: {application/json: {schema: {$ref: '#/components/schemas/TranscribeResponse'}}}}
        '202': {description: Processing started (async), content: {application/json: {schema: {$ref: '#/components/schemas/TranscribeResponse'}}}}
        '400': {description: Invalid request, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '401': {description: Invalid API key, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '402': {description: Insufficient credits, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '404': {description: No captions available, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '429': {description: Rate limit exceeded, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /transcribe/estimate:
    post:
      summary: Estimate credit cost (dry run)
      description: Returns the ASR base cost plus any add-on surcharge for the requested asr_options. Does not create a job or charge credits.
      operationId: estimateTranscribe
      tags: [Transcribe]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/EstimateRequest'}
      responses:
        '200': {description: Estimate, content: {application/json: {schema: {$ref: '#/components/schemas/EstimateResponse'}}}}
        '400': {description: Invalid request, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '401': {description: Invalid API key, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /uploads:
    post:
      summary: Register a media file upload
      description: |
        Step 1 of transcribing your own audio/video file. Registers the file
        and returns a signed URL; PUT the raw file bytes to that URL with the
        file's Content-Type header (max 2GB, max 8 hours).

        Full flow:
        1. POST /uploads with the file metadata -> signed_url + upload_id
        2. PUT the file bytes to signed_url (Content-Type = the file's mime type)
        3. POST /uploads/{upload_id}/complete to verify the upload
        4. POST /transcribe with upload_id (+ webhook_url) and source="asr"
      operationId: createUpload
      tags: [Uploads]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/UploadCreateRequest'}
      responses:
        '200': {description: Upload registered, content: {application/json: {schema: {$ref: '#/components/schemas/UploadCreateResponse'}}}}
        '400': {description: Invalid file type, size or duration, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '401': {description: Invalid API key, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '429': {description: Rate limit exceeded, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /uploads/{upload_id}/complete:
    post:
      summary: Finalize a media file upload
      description: Verifies the file landed in storage and marks the upload ready for transcription. Call after the PUT to signed_url succeeds.
      operationId: completeUpload
      tags: [Uploads]
      parameters:
        - {name: upload_id, in: path, required: true, schema: {type: string}, description: Upload id returned by POST /uploads}
      responses:
        '200': {description: Upload finalized, content: {application/json: {schema: {$ref: '#/components/schemas/UploadCompleteResponse'}}}}
        '400': {description: File not found in storage, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '401': {description: Invalid API key, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '404': {description: Upload not found, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '409': {description: Upload already consumed, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /batch:
    post:
      summary: Transcribe multiple videos
      operationId: transcribeBatch
      tags: [Batch]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/BatchRequest'}
      responses:
        '200': {description: Batch completed or partial, content: {application/json: {schema: {$ref: '#/components/schemas/BatchResponse'}}}}
        '202': {description: Batch processing started, content: {application/json: {schema: {$ref: '#/components/schemas/BatchResponse'}}}}
        '400': {description: Invalid request, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '402': {description: Insufficient credits, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /batch/{batch_id}:
    get:
      summary: Poll a V2 batch job
      operationId: pollV2Batch
      tags: [Batch]
      parameters:
        - {name: batch_id, in: path, required: true, schema: {type: string, format: uuid}}
      responses:
        '200': {description: Batch completed or partial, content: {application/json: {schema: {$ref: '#/components/schemas/BatchResponse'}}}}
        '202': {description: Batch still processing, content: {application/json: {schema: {$ref: '#/components/schemas/BatchResponse'}}}}
        '404': {description: Batch not found, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /jobs:
    get:
      summary: List transcription jobs
      operationId: listV2Jobs
      tags: [Jobs]
      parameters:
        - {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
        - {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 50}}
        - {name: status, in: query, schema: {type: string, enum: [completed, processing, failed, requires_asr_confirmation]}}
      responses:
        '200': {description: Job list, content: {application/json: {schema: {$ref: '#/components/schemas/JobListResponse'}}}}

  /jobs/{job_id}:
    get:
      summary: Poll a V2 transcription job
      operationId: pollV2Job
      tags: [Jobs]
      parameters:
        - {name: job_id, in: path, required: true, schema: {type: string, format: uuid}}
        - {name: include_segments, in: query, schema: {type: boolean, default: true}}
        - {name: include_paragraphs, in: query, schema: {type: boolean, default: false}}
        - {name: include_words, in: query, schema: {type: boolean, default: false}}
      responses:
        '200': {description: Completed, failed, or requires_asr_confirmation, content: {application/json: {schema: {$ref: '#/components/schemas/JobResponse'}}}}
        '202': {description: Job still processing, content: {application/json: {schema: {$ref: '#/components/schemas/JobResponse'}}}}
        '404': {description: Job not found, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /transcripts:
    get:
      summary: List stored transcripts
      operationId: listTranscripts
      tags: [Transcripts]
      parameters:
        - {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
        - {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 50}}
        - {name: search, in: query, schema: {type: string}}
        - {name: status, in: query, schema: {type: string}}
        - {name: language, in: query, schema: {type: string}}
        - {name: date_from, in: query, schema: {type: string, format: date-time}}
        - {name: date_to, in: query, schema: {type: string, format: date-time}}
        - {name: include_segments, in: query, schema: {type: boolean, default: false}}
      responses:
        '200': {description: Transcript list, content: {application/json: {schema: {$ref: '#/components/schemas/TranscriptListResponse'}}}}

  /transcripts/{video_id}:
    get:
      summary: Fetch a stored transcript
      description: Returns the best owned transcript for a video, including asr_intelligence, summary, and mind_map when available.
      operationId: getTranscript
      tags: [Transcripts]
      parameters:
        - {name: video_id, in: path, required: true, schema: {type: string}}
        - {name: id, in: query, description: Specific transcript/job id, schema: {type: string, format: uuid}}
        - {name: language, in: query, schema: {type: string}}
        - {name: source, in: query, schema: {type: string, enum: [auto, manual, asr]}}
        - {name: include_timestamps, in: query, schema: {type: boolean, default: true}}
      responses:
        '200': {description: Transcript, content: {application/json: {schema: {$ref: '#/components/schemas/TranscriptResource'}}}}
        '403': {description: No access to requested language, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '404': {description: Transcript not found, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
    delete:
      summary: Delete a stored transcript
      description: Deletes owned transcript(s) for a video. Optional language/source narrow the deletion; otherwise all languages/sources are removed.
      operationId: deleteTranscript
      tags: [Transcripts]
      parameters:
        - {name: video_id, in: path, required: true, schema: {type: string}}
        - {name: language, in: query, schema: {type: string}}
        - {name: source, in: query, schema: {type: string, enum: [auto, manual, asr]}}
      responses:
        '200': {description: Deleted, content: {application/json: {schema: {$ref: '#/components/schemas/DeleteResponse'}}}}
        '404': {description: Transcript not found, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /transcripts/{video_id}/languages:
    get:
      summary: List available languages for a video
      description: Languages the caller owns plus available YouTube translation targets.
      operationId: getTranscriptLanguages
      tags: [Transcripts]
      parameters:
        - {name: video_id, in: path, required: true, schema: {type: string}}
        - {name: include_youtube_default, in: query, schema: {type: boolean, default: true}}
      responses:
        '200': {description: Languages, content: {application/json: {schema: {$ref: '#/components/schemas/LanguagesResponse'}}}}

  /transcripts/{video_id}/translate:
    post:
      summary: Translate a stored transcript
      operationId: translateTranscript
      tags: [Transcripts]
      parameters:
        - {name: video_id, in: path, required: true, schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/TranslateRequest'}
      responses:
        '200': {description: Translation created (or already owned), content: {application/json: {schema: {$ref: '#/components/schemas/TranslateResponse'}}}}
        '400': {description: Unsupported target language, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
        '402': {description: Insufficient credits, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /transcripts/bulk-delete:
    post:
      summary: Delete transcripts by id
      operationId: bulkDeleteTranscripts
      tags: [Transcripts]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids: {type: array, items: {type: string, format: uuid}}
      responses:
        '200': {description: Deleted, content: {application/json: {schema: {$ref: '#/components/schemas/DeleteResponse'}}}}
        '403': {description: No matching transcripts owned, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}

  /credits:
    get:
      summary: Get credit balance and plan
      operationId: getCredits
      tags: [Account]
      responses:
        '200': {description: Credit balance, content: {application/json: {schema: {$ref: '#/components/schemas/CreditsResponse'}}}}

  /ping:
    get:
      summary: Health check
      operationId: ping
      tags: [Account]
      security: []
      responses:
        '200': {description: Service healthy, content: {application/json: {schema: {type: object, properties: {status: {type: string}, timestamp: {type: string}}}}}}

  /playlists/resolve:
    post:
      summary: Resolve playlist videos
      operationId: resolveV2Playlist
      tags: [Playlists]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/PlaylistResolveRequest'}
      responses:
        '200': {description: Playlist resolved, content: {application/json: {schema: {$ref: '#/components/schemas/PlaylistResolveResponse'}}}}

  /playlists/{playlist_id}:
    get:
      summary: Get playlist batch details
      operationId: getV2PlaylistBatch
      tags: [Playlists]
      parameters:
        - {name: playlist_id, in: path, required: true, schema: {type: string}}
      responses:
        '200': {description: Playlist batch details, content: {application/json: {schema: {$ref: '#/components/schemas/PlaylistBatchResponse'}}}}

  /playlists/history:
    get:
      summary: List playlist batches
      operationId: listV2PlaylistHistory
      tags: [Playlists]
      responses:
        '200': {description: Playlist history, content: {application/json: {schema: {$ref: '#/components/schemas/CollectionHistoryResponse'}}}}

  /channels/resolve:
    post:
      summary: Resolve channel uploads
      operationId: resolveV2Channel
      tags: [Channels]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ChannelResolveRequest'}
      responses:
        '200': {description: Channel resolved, content: {application/json: {schema: {$ref: '#/components/schemas/ChannelResolveResponse'}}}}

  /channels/{channel_id}:
    get:
      summary: Get channel batch details
      operationId: getV2ChannelBatch
      tags: [Channels]
      parameters:
        - {name: channel_id, in: path, required: true, schema: {type: string}}
      responses:
        '200': {description: Channel batch details, content: {application/json: {schema: {$ref: '#/components/schemas/ChannelBatchResponse'}}}}

  /channels/history:
    get:
      summary: List channel batches
      operationId: listV2ChannelHistory
      tags: [Channels]
      responses:
        '200': {description: Channel history, content: {application/json: {schema: {$ref: '#/components/schemas/CollectionHistoryResponse'}}}}

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: 'API key auth via Authorization: Bearer yt_sk_live_...'

  schemas:
    FormatOptions:
      type: object
      description: Which extras to include in the transcript payload.
      properties:
        timestamp: {type: boolean, description: Include timestamped segments}
        paragraphs: {type: boolean, description: Include paragraph groupings}
        words: {type: boolean, description: Include word-level timestamps}

    AsrOptions:
      type: object
      description: |
        Rich ASR configuration (AssemblyAI Universal-2). Only applies when the
        video is transcribed via ASR (source="asr" or ASR fallback). Billable
        add-ons increase credit cost proportional to duration - use
        /transcribe/estimate to preview.
      properties:
        language: {type: string, default: auto, description: Source audio language as an ISO code (any of the 99 supported languages, e.g. en, es, yo), or "auto" to detect}
        codeSwitching: {type: boolean, default: false, description: Transcribe mid-sentence language switches. Only applies when language="auto"}
        medicalMode: {type: boolean, default: false, description: Boost accuracy for medical terminology (EN/ES/DE/FR only)}
        keyTerms:
          type: array
          maxItems: 200
          items: {type: string}
          description: Words/phrases (max 6 words each) to boost recognition accuracy, e.g. product names or jargon. Beta - English audio only (ignored for other languages)
        translateTo: {type: string, default: none, description: Translate transcript to this language, or "none"}
        speakerLabels: {type: boolean, default: false, description: Speaker diarization (billable). Mutually exclusive with multichannel - multichannel wins when both are set}
        speakerId: {type: boolean, default: false, description: Map speaker labels to real names/roles (billable, implies diarization). Not available with multichannel}
        speakerIdType: {type: string, enum: [name, role], default: name, description: Identify speakers by name or by role}
        speakerIdKnownValues:
          type: array
          maxItems: 20
          items: {type: string, maxLength: 35}
          description: Known speaker names or roles to match against. Optional for "name" (inferred from conversation); REQUIRED for "role" - role-type speaker ID is skipped without at least one value
        multichannel: {type: boolean, default: false, description: Per-channel transcription (billable). Mutually exclusive with speakerLabels and speakerId - setting it disables both}
        speakersExpected: {type: integer, nullable: true, minimum: 1, maximum: 20, description: Exact speaker count. Null to auto-detect or use the min/max range}
        speakersExpectedMin: {type: integer, nullable: true, minimum: 1, maximum: 20, description: Hard lower bound on speaker labels (ignored when speakersExpected is set)}
        speakersExpectedMax: {type: integer, nullable: true, minimum: 1, maximum: 20, description: Hard upper bound on speaker labels (ignored when speakersExpected is set)}
        topicDetection: {type: boolean, default: false, description: IAB topic categories (billable)}
        summarization: {type: boolean, default: false, description: Auto summary (billable)}
        contentModeration: {type: boolean, default: false, description: Content safety detection (billable)}
        piiRedaction: {type: boolean, default: false, description: Redact personally identifiable information (billable)}
        profanityFiltering: {type: boolean, default: false, description: Filter profanity (billable)}
        sentimentAnalysis: {type: boolean, default: false, description: Per-segment sentiment (billable)}
        autoPunctuation: {type: boolean, default: true, description: Automatic punctuation}
        textFormatting: {type: boolean, default: true, description: Text formatting}
        fillerWords: {type: boolean, default: true, description: Keep filler words (disfluencies)}
        customFormatting: {type: boolean, default: false, description: Reformat dates/phones/emails using the patterns below (billable)}
        customFormatDate: {type: string, enum: [mm/dd/yyyy, dd/mm/yyyy, yyyy-mm-dd, mm-dd-yyyy, dd.mm.yyyy], default: mm/dd/yyyy, description: Date pattern applied when customFormatting is on}
        customFormatPhone: {type: string, enum: ['(xxx)xxx-xxxx', xxx-xxx-xxxx, xxx.xxx.xxxx, '+x(xxx)xxx-xxxx'], default: '(xxx)xxx-xxxx', description: Phone pattern applied when customFormatting is on}
        customFormatEmail: {type: string, enum: [username@domain.com, firstname.lastname@domain.com], default: username@domain.com, description: Email pattern applied when customFormatting is on}

    AsrIntelligence:
      type: object
      description: ASR analysis returned when the matching asr_options were enabled. Each field is null/absent unless its feature was requested.
      properties:
        summary: {type: string, nullable: true}
        topics:
          nullable: true
          description: IAB topic categories (AssemblyAI iab_categories_result).
          allOf: [{$ref: '#/components/schemas/AsrLabelSummary'}]
        content_safety:
          nullable: true
          description: Content safety labels (AssemblyAI content_safety_labels).
          allOf: [{$ref: '#/components/schemas/AsrLabelSummary'}]
        sentiment: {type: array, nullable: true, items: {$ref: '#/components/schemas/AsrSentimentResult'}}
        entities: {type: array, nullable: true, items: {$ref: '#/components/schemas/AsrEntity'}}
        auto_highlights:
          nullable: true
          description: Key phrases (AssemblyAI auto_highlights_result).
          allOf: [{$ref: '#/components/schemas/AsrAutoHighlights'}]
        chapters: {type: array, nullable: true, items: {$ref: '#/components/schemas/AsrChapter'}}

    AsrLabelSummary:
      type: object
      description: Aggregated label scores with the raw per-segment results.
      properties:
        summary:
          type: object
          description: Map of label -> relevance score (0-1).
          additionalProperties: {type: number}
        results: {type: array, items: {type: object, additionalProperties: true}}

    AsrSentimentResult:
      type: object
      required: [text, sentiment]
      properties:
        text: {type: string}
        sentiment: {type: string, enum: [POSITIVE, NEGATIVE, NEUTRAL]}
        confidence: {type: number}
        start: {type: integer, description: Start time in milliseconds}
        end: {type: integer, description: End time in milliseconds}

    AsrEntity:
      type: object
      required: [text]
      properties:
        text: {type: string}
        entity_type: {type: string}
        start: {type: integer, description: Start time in milliseconds}
        end: {type: integer, description: End time in milliseconds}

    AsrHighlight:
      type: object
      required: [text]
      properties:
        text: {type: string}
        count: {type: integer}
        rank: {type: number, description: Relevance rank (0-1)}

    AsrAutoHighlights:
      type: object
      properties:
        results: {type: array, items: {$ref: '#/components/schemas/AsrHighlight'}}

    AsrChapter:
      type: object
      description: AssemblyAI auto-chapter with time bounds (milliseconds).
      properties:
        summary: {type: string}
        headline: {type: string}
        gist: {type: string}
        start: {type: integer}
        end: {type: integer}

    TranscribeRequest:
      type: object
      description: Provide either video (YouTube) or upload_id (your own media file), not both.
      properties:
        video: {type: string, description: YouTube URL or 11-char ID. Required unless upload_id is set.}
        upload_id:
          type: string
          description: 'Id of a finalized media upload (see POST /uploads). Forces the ASR path: source must be omitted or "asr", and webhook_url is required. The upload id is returned as video_id in results.'
        language:
          type: string
          description: 'Preferred transcript language (e.g. en, es). If unavailable, captions in another language may be returned; transcript.language reports the actual language.'
        source: {type: string, enum: [auto, manual, asr], default: auto}
        allow_asr: {type: boolean, description: Allow ASR fallback when captions are unavailable}
        format: {$ref: '#/components/schemas/FormatOptions'}
        webhook_url: {type: string, format: uri, description: Required for ASR (source="asr", allow_asr=true, or upload_id) unless run from the dashboard}
        asr_options: {$ref: '#/components/schemas/AsrOptions'}

    UploadCreateRequest:
      type: object
      required: [filename, mime_type, size_bytes, duration_sec]
      properties:
        filename: {type: string, description: Original filename (extension is kept for content sniffing)}
        mime_type: {type: string, description: Must start with audio/ or video/, e.g. audio/mpeg, video/mp4}
        size_bytes: {type: integer, description: 'File size in bytes (max 2147483648 = 2GB)'}
        duration_sec: {type: integer, description: 'Media duration in seconds (max 28800 = 8 hours). Used for the ASR credit estimate.'}

    UploadCreateResponse:
      type: object
      properties:
        upload_id: {type: string, description: Pass this to POST /transcribe after finalizing}
        bucket: {type: string}
        path: {type: string}
        token: {type: string, description: Signed upload token (embedded in signed_url)}
        signed_url: {type: string, format: uri, description: PUT the raw file bytes here with the file's Content-Type header}

    UploadCompleteResponse:
      type: object
      properties:
        upload_id: {type: string}
        status: {type: string, enum: [uploaded]}
        filename: {type: string}
        duration_sec: {type: integer, nullable: true}

    EstimateRequest:
      type: object
      required: [video]
      properties:
        video: {type: string}
        source: {type: string, enum: [auto, manual, asr], default: asr}
        asr_options: {$ref: '#/components/schemas/AsrOptions'}

    EstimateResponse:
      type: object
      properties:
        request_id: {type: string}
        video_id: {type: string}
        source: {type: string}
        duration_seconds: {type: number, nullable: true}
        duration_minutes: {type: number, nullable: true}
        max_duration_minutes: {type: number}
        exceeds_max_duration: {type: boolean}
        base_credits: {type: number}
        addon_credits: {type: number}
        estimated_credits: {type: number}

    TranscriptPayload:
      type: object
      properties:
        text: {type: string}
        language: {type: string}
        source: {type: string, enum: [manual, auto, asr]}
        segments: {type: array, items: {$ref: '#/components/schemas/TranscriptSegment'}}
        paragraphs: {type: array, nullable: true, items: {$ref: '#/components/schemas/TranscriptSegment'}}
        words: {type: array, nullable: true, items: {$ref: '#/components/schemas/TranscriptWord'}}

    TranscriptSegment:
      type: object
      required: [text, start, end]
      properties:
        text: {type: string}
        start: {type: integer, description: Start time in milliseconds}
        end: {type: integer, description: End time in milliseconds}
        words:
          type: array
          description: Word-level timestamps embedded from ASR (present only when requested via format.words).
          items: {$ref: '#/components/schemas/TranscriptWord'}

    TranscriptWord:
      type: object
      required: [text, start, end]
      properties:
        text: {type: string}
        start: {type: integer, description: Start time in milliseconds}
        end: {type: integer, description: End time in milliseconds}

    TranscribeData:
      type: object
      properties:
        video_id: {type: string}
        transcript: {$ref: '#/components/schemas/TranscriptPayload'}
        video_title: {type: string, nullable: true}
        asr_intelligence: {$ref: '#/components/schemas/AsrIntelligence'}

    TranscribeResponse:
      type: object
      properties:
        request_id: {type: string}
        status: {type: string, enum: [completed, processing, failed, requires_asr_confirmation]}
        data: {$ref: '#/components/schemas/TranscribeData'}
        error: {type: string}
        credits_used: {type: number}
        job_id: {type: string, nullable: true}
        estimated_credits: {type: number}
        base_credits: {type: number, description: ASR base cost (on requires_asr_confirmation)}
        addon_credits: {type: number, description: ASR add-on surcharge (on requires_asr_confirmation)}
        duration_minutes: {type: number, nullable: true}
        max_duration_minutes: {type: number}
        suggestion: {type: string}

    JobResponse:
      type: object
      properties:
        job_id: {type: string}
        status: {type: string, enum: [completed, processing, failed, requires_asr_confirmation]}
        credits_used: {type: number}
        data: {$ref: '#/components/schemas/TranscribeData'}
        error: {type: string}
        created_at: {type: string, format: date-time}
        started_at: {type: string, format: date-time, nullable: true}
        completed_at: {type: string, format: date-time, nullable: true}

    JobListResponse:
      type: object
      properties:
        jobs:
          type: array
          items:
            type: object
            properties:
              job_id: {type: string}
              video_id: {type: string}
              video_title: {type: string, nullable: true}
              language: {type: string, nullable: true}
              source: {type: string, nullable: true}
              status: {type: string, enum: [completed, processing, failed, requires_asr_confirmation]}
              error: {type: string, nullable: true}
              credits_used: {type: number}
              created_at: {type: string, format: date-time}
              started_at: {type: string, format: date-time, nullable: true}
              completed_at: {type: string, format: date-time, nullable: true}
        count: {type: integer}
        pagination: {$ref: '#/components/schemas/Pagination'}

    BatchRequest:
      type: object
      required: [video_ids]
      properties:
        video_ids: {type: array, items: {type: string}, maxItems: 3000}
        language: {type: string, description: Preferred language for every video}
        source: {type: string, enum: [auto, manual, asr], default: auto}
        allow_asr: {type: boolean, description: Allow ASR fallback (requires webhook_url)}
        format: {$ref: '#/components/schemas/FormatOptions'}
        webhook_url: {type: string, format: uri, description: Required when source="asr" or allow_asr=true}
        asr_options: {$ref: '#/components/schemas/AsrOptions'}

    BatchResponse:
      type: object
      properties:
        batch_id: {type: string, format: uuid}
        status: {type: string, enum: [completed, partial, processing, failed]}
        results:
          type: array
          items:
            type: object
            properties:
              request_id: {type: string}
              video_id: {type: string}
              status: {type: string, enum: [completed, processing, failed, requires_asr_confirmation]}
              job_id: {type: string, nullable: true}
              error: {type: string, nullable: true}
              data: {$ref: '#/components/schemas/TranscribeData'}
              credits_used: {type: number}
        summary:
          type: object
          properties:
            total: {type: integer}
            succeeded: {type: integer}
            failed: {type: integer}
            processing: {type: integer}
        credits_used: {type: number}
        poll_url: {type: string, format: uri}
        webhook_delivered: {type: boolean}
        webhook_failed: {type: boolean}
        webhook_attempts: {type: integer}
        created_at: {type: string, format: date-time}
        started_at: {type: string, format: date-time, nullable: true}
        completed_at: {type: string, format: date-time, nullable: true}

    TranscriptResource:
      type: object
      properties:
        id: {type: string}
        video_id: {type: string}
        language: {type: string}
        text: {type: string}
        segments: {type: array, items: {$ref: '#/components/schemas/TranscriptSegment'}}
        has_timestamps: {type: boolean}
        source_kind: {type: string, enum: [manual, auto, asr]}
        video_title: {type: string, nullable: true}
        summary: {type: object, nullable: true, additionalProperties: true, description: Structured summary (free-form object) when generated.}
        mind_map: {type: object, nullable: true, additionalProperties: true, description: Mind-map data (free-form object) when generated.}
        asr_intelligence: {$ref: '#/components/schemas/AsrIntelligence'}

    TranscriptListResponse:
      type: object
      properties:
        history:
          type: array
          items:
            type: object
            properties:
              id: {type: string}
              video_id: {type: string}
              video_title: {type: string}
              language: {type: string}
              source_kind: {type: string}
              status: {type: string, enum: [queued, processing, succeeded, failed]}
              created_at: {type: string, format: date-time}
              word_count: {type: integer}
              segment_count: {type: integer}
              duration_seconds: {type: integer}
              credits_used: {type: number}
        count: {type: integer}
        pagination: {$ref: '#/components/schemas/Pagination'}

    LanguagesResponse:
      type: object
      properties:
        video_id: {type: string}
        languages: {type: array, items: {type: string}}
        youtube_translation_languages: {type: array, items: {type: string}}

    TranslateRequest:
      type: object
      required: [target_language]
      properties:
        target_language: {type: string}
        source_language: {type: string, nullable: true}
        provider: {type: string, enum: [ai, youtube]}
        allow_ai_fallback: {type: boolean}

    TranslateResponse:
      type: object
      properties:
        success: {type: boolean}
        message: {type: string}
        language: {type: string}
        credits_used: {type: number}
        provider_used: {type: string, nullable: true}
        transcript: {$ref: '#/components/schemas/TranscriptPayload'}

    DeleteResponse:
      type: object
      properties:
        success: {type: boolean}
        deleted: {type: integer}
        message: {type: string}

    CreditsResponse:
      type: object
      properties:
        credits: {type: number}
        plan: {type: string}
        thumbnail_credits: {type: number}
        onboarding_completed: {type: boolean}

    Pagination:
      type: object
      properties:
        page: {type: integer}
        limit: {type: integer}
        total: {type: integer}
        totalPages: {type: integer}
        hasMore: {type: boolean}

    PlaylistResolveRequest:
      type: object
      properties:
        playlist_url: {type: string}
        playlist_id: {type: string}
        limit: {type: integer, minimum: 1, maximum: 100}

    PlaylistResolveResponse:
      type: object
      properties:
        playlist_id: {type: string}
        title: {type: string, nullable: true}
        total: {type: integer}
        truncated: {type: boolean}
        items: {type: array, items: {$ref: '#/components/schemas/CollectionVideoRef'}}

    ChannelResolveRequest:
      type: object
      properties:
        channel_url: {type: string}
        channel_id: {type: string}
        handle: {type: string}
        limit: {type: integer, minimum: 1}

    ChannelResolveResponse:
      type: object
      properties:
        channel_id: {type: string}
        channel_title: {type: string}
        total: {type: integer}
        truncated: {type: boolean}
        items: {type: array, items: {$ref: '#/components/schemas/CollectionVideoRef'}}

    CollectionVideoRef:
      type: object
      description: A video listed by a playlist/channel resolve call (metadata only).
      properties:
        id: {type: string}
        video_id: {type: string}
        title: {type: string, nullable: true}
        url: {type: string, nullable: true}
        published_at: {type: string, nullable: true}

    CollectionJobVideo:
      type: object
      description: Per-video transcription job status inside a playlist/channel batch.
      properties:
        id: {type: string}
        video_id: {type: string}
        video_title: {type: string, nullable: true}
        status: {type: string, nullable: true}
        created_at: {type: string, format: date-time}
        finished_at: {type: string, format: date-time, nullable: true}
        error: {type: string, nullable: true}

    PlaylistBatchInfo:
      type: object
      properties:
        id: {type: string}
        playlist_id: {type: string, nullable: true}
        playlist_title: {type: string, nullable: true}
        playlist_url: {type: string, nullable: true}
        total_videos: {type: integer}
        completed_count: {type: integer}
        failed_count: {type: integer}
        status: {type: string}
        created_at: {type: string, format: date-time}
        started_at: {type: string, format: date-time, nullable: true}
        completed_at: {type: string, format: date-time, nullable: true}
        metadata: {type: object, nullable: true, additionalProperties: true}

    ChannelBatchInfo:
      type: object
      properties:
        id: {type: string}
        channel_id: {type: string, nullable: true}
        channel_title: {type: string, nullable: true}
        channel_url: {type: string, nullable: true}
        total_videos: {type: integer}
        completed_count: {type: integer}
        failed_count: {type: integer}
        status: {type: string}
        created_at: {type: string, format: date-time}
        started_at: {type: string, format: date-time, nullable: true}
        completed_at: {type: string, format: date-time, nullable: true}
        metadata: {type: object, nullable: true, additionalProperties: true}

    PlaylistBatchResponse:
      type: object
      properties:
        playlist: {$ref: '#/components/schemas/PlaylistBatchInfo'}
        videos: {type: array, items: {$ref: '#/components/schemas/CollectionJobVideo'}}

    ChannelBatchResponse:
      type: object
      properties:
        channel: {$ref: '#/components/schemas/ChannelBatchInfo'}
        videos: {type: array, items: {$ref: '#/components/schemas/CollectionJobVideo'}}

    CollectionHistoryResponse:
      type: object
      properties:
        history:
          type: array
          items:
            type: object
            properties:
              id: {type: string}
              playlist_id: {type: string, nullable: true}
              playlist_title: {type: string, nullable: true}
              playlist_url: {type: string, nullable: true}
              channel_id: {type: string, nullable: true}
              channel_title: {type: string, nullable: true}
              total_videos: {type: integer}
              completed_count: {type: integer}
              failed_count: {type: integer}
              status: {type: string}
              created_at: {type: string, format: date-time}
              completed_at: {type: string, format: date-time, nullable: true}
        count: {type: integer}

    ErrorResponse:
      type: object
      properties:
        error: {type: string}
        message: {type: string}
        details: {type: object, additionalProperties: true}
