openapi: 3.1.0
info:
  title: Vid Kraken — YouTube Download API
  version: "2.0"
  description: |
    REST API to download videos from YouTube and to fetch direct download
    links from YouTube, Instagram, TikTok, and Facebook.


    ## Getting Started

    - Sign up at [vidkraken.com](https://vidkraken.com)

    - Grab your API key from the [dashboard](https://vidkraken.com/dashboard)

    - Pass it on every request as: `Authorization: Bearer YOUR_API_KEY`


    ## Usage & billing

    You pay for bandwidth only. Every plan includes a monthly bandwidth
    allowance; each download counts its actual file size against it,
    with a 20 MB minimum per download. There are no per-download fees.
    Downloads and metadata lookups are counted against generous
    per-plan quotas that are sized so bandwidth always runs out first.
    Failures are never charged.


    | Endpoint | Counts against |
    |---|---|
    | `POST /download` | Bandwidth at actual file size (20 MB minimum) |
    | `POST /info` | 1 info request |
    | `POST /list-channel` | Account add-on — contact us to enable |
    | `POST /client-download` | Account add-on — contact us to enable |


    Polling endpoints (`GET /download/{jobId}`, `GET /info/{jobId}`,
    `GET /client-download/{jobId}`) are free, as is `GET /me` for
    checking your plan and remaining balances. Failed jobs, auth
    failures (`401`) and limit errors (`403`, `429`) are not charged.


    Each account may have at most 100 downloads queued or in progress at
    once; `POST /download` returns `429` beyond that until some finish.
  contact:
    name: Support
    url: https://vidkraken.com
    email: hello@vidkraken.com
servers:
  - url: https://vidkraken.com/api/v2
    description: Production
  - url: https://youtube-download-api.org/api/v2
    description: Production (legacy domain — continues to work)
  - url: https://dalvo.io/api/v2
    description: Production (legacy domain — continues to work)
security:
  - bearerAuth: []
tags:
  - name: Info
    description: Fetch metadata for a YouTube video without queueing a download.
  - name: Download
    description: Server-side YouTube downloads — submit a job, poll for the CDN URL.
  - name: Channel
    description: List recent videos from a YouTube channel.
  - name: Account
    description: Your account details, current plan, and remaining balances.
paths:
  /info:
    post:
      operationId: submit-video-info
      summary: Submit a video-info job
      description: |
        Queue a job that fetches metadata for a YouTube video — title and
        duration. Returns a `jobId` that you poll on `GET /info/{jobId}`.


        Set `includeAudioTracks: true` to also resolve the list of available
        audio tracks (original + dubbed languages, if any). Audio tracks
        require a full extraction, which is slower and can occasionally fail
        on hard-to-reach videos — only request them when you need to pick an
        audio language ahead of a download.


        Useful for showing video details in your UI before queueing a
        download, or for picking an audio language ahead of time.


        Job state is kept in Redis with a 1-hour TTL — make sure to
        poll within that window.


        **Cost:** 1 info request from your plan's info quota, on
        success only. No bandwidth cost. Failures are not charged.
      tags:
        - Info
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GetVideoInfoRequest"
            example:
              url: https://youtu.be/dQw4w9WgXcQ
      responses:
        "200":
          description: Job enqueued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitVideoInfoResponse"
              example:
                jobId: 6f9c1234-5e8a-4b2f-9c1e-0a7b69cfc45e
                status: pending
        "400":
          $ref: "#/components/responses/400 - Bad Request"
        "401":
          $ref: "#/components/responses/401 - Unauthorized"
        "500":
          $ref: "#/components/responses/500 - Internal Server Error"
      security:
        - bearerAuth: []
  /info/{jobId}:
    get:
      operationId: get-video-info-status
      summary: Get video-info job status
      description: |
        Poll the status of a previously-submitted video-info job.


        - `pending` — still fetching metadata


        - `success` — `result` contains the resolved `VideoInfo`


        - `failed` — `error` carries a human-readable message and, for recognised
          video conditions, `errorCode` identifies it (see `DownloadErrorCode`)


        Job state expires from Redis 1 hour after submission; polling
        after that returns `404`.


        **Cost:** Free — polling is never charged.
      tags:
        - Info
      parameters:
        - in: path
          name: jobId
          required: true
          description: The `jobId` returned from `POST /info`.
          schema:
            type: string
      responses:
        "200":
          description: Current job status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VideoInfoJobStatusResponse"
              examples:
                pending:
                  summary: Still fetching
                  value:
                    jobId: 6f9c1234-5e8a-4b2f-9c1e-0a7b69cfc45e
                    status: pending
                success:
                  summary: Metadata resolved
                  value:
                    jobId: 6f9c1234-5e8a-4b2f-9c1e-0a7b69cfc45e
                    status: success
                    result:
                      title: Rick Astley - Never Gonna Give You Up
                      duration: 213
                      url: https://youtu.be/dQw4w9WgXcQ
                successWithAudioTracks:
                  summary: "Metadata resolved (submitted with `includeAudioTracks: true`)"
                  value:
                    jobId: 6f9c1234-5e8a-4b2f-9c1e-0a7b69cfc45e
                    status: success
                    result:
                      title: Rick Astley - Never Gonna Give You Up
                      duration: 213
                      url: https://youtu.be/dQw4w9WgXcQ
                      audio:
                        - type: original
                          language: en
                        - type: dubbed
                          language: es
                failed:
                  summary: Resolution failed
                  value:
                    jobId: 6f9c1234-5e8a-4b2f-9c1e-0a7b69cfc45e
                    status: failed
                    error: "Video unavailable"
        "401":
          $ref: "#/components/responses/401 - Unauthorized"
        "404":
          $ref: "#/components/responses/404 - Not Found"
        "500":
          $ref: "#/components/responses/500 - Internal Server Error"
      security:
        - bearerAuth: []
  /download:
    post:
      operationId: submit-download
      summary: Submit a download
      description: |
        Queue a YouTube video for download. Returns a `jobId` that you poll on
        `GET /download/{jobId}` until status is `COMPLETED` (or `FAILED`).


        Prefer push over polling? Pass a `webhookUrl` and we POST the final
        status to it instead — the body is byte-for-byte the
        `GET /download/{jobId}` response. See the `download.finished`
        webhook below for headers, signature verification and retries.


        Higher-quality formats may be capped down for very long videos to fit
        your plan's per-file size limit; the actually-delivered format is
        reported as `actualFormat` on the completed status response.


        **Cost:** bandwidth at the actual file size delivered (20 MB
        minimum), charged when the background job completes —
        submitting the job is free. There is no per-download fee.
        Failures are not charged.
      tags:
        - Download
      requestBody:
        required: true
        description: Download parameters.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubmitDownloadRequest"
            examples:
              audio:
                summary: Download the audio track
                value:
                  url: https://youtu.be/dQw4w9WgXcQ
                  format: audio
              audio-mp3:
                summary: Download the audio track as an MP3
                value:
                  url: https://youtu.be/dQw4w9WgXcQ
                  format: audio-mp3
              clip-1080p:
                summary: Trim a 60-second 1080p clip
                value:
                  url: https://youtu.be/dQw4w9WgXcQ
                  format: "1080"
                  startTime: 30
                  endTime: 90
              dubbed-english:
                summary: Force the English-dubbed audio track
                value:
                  url: https://www.youtube.com/watch?v=omW5PrTMz-c
                  format: "1080"
                  language: en-US
              with-webhook:
                summary: Get notified by webhook instead of polling
                value:
                  url: https://youtu.be/dQw4w9WgXcQ
                  format: "720"
                  webhookUrl: https://example.com/hooks/vidkraken
      responses:
        "200":
          description: Job enqueued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitDownloadResponse"
              example:
                jobId: 69cfc45e8a3b4d2f9c1e0a7b
                title: Rick Astley - Never Gonna Give You Up
                duration: 213
                status: IN_QUEUE
                message: Download job enqueued. Poll GET /api/v2/download/{jobId} for status.
        "400":
          $ref: "#/components/responses/400 - Bad Request"
        "401":
          $ref: "#/components/responses/401 - Unauthorized"
        "403":
          $ref: "#/components/responses/403 - Forbidden"
        "429":
          $ref: "#/components/responses/429 - Too Many Requests"
        "500":
          $ref: "#/components/responses/500 - Internal Server Error"
      security:
        - bearerAuth: []
  /download/{jobId}:
    get:
      operationId: get-download-status
      summary: Get download status
      description: |
        Poll the status of a previously-submitted download job. Once `status`
        is `COMPLETED`, the response carries a `downloadUrl` you can fetch
        the file from. If `status` is `FAILED`, an `errorCode` identifies
        why.


        **Cost:** Free — polling is never charged.
      tags:
        - Download
      parameters:
        - in: path
          name: jobId
          required: true
          description: The `jobId` returned from `POST /download`.
          schema:
            type: string
      responses:
        "200":
          description: Current job status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DownloadJobStatusResponse"
              examples:
                in-progress:
                  summary: Job still running
                  value:
                    jobId: 69cfc45e8a3b4d2f9c1e0a7b
                    status: IN_PROGRESS
                    title: Rick Astley - Never Gonna Give You Up
                    duration: 213
                    url: https://youtu.be/dQw4w9WgXcQ
                    format: "1080"
                completed:
                  summary: Job completed — file ready
                  value:
                    jobId: 69cfc45e8a3b4d2f9c1e0a7b
                    status: COMPLETED
                    url: https://youtu.be/dQw4w9WgXcQ
                    format: "1080"
                    actualFormat: "1080"
                    language: null
                    downloadUrl: https://proxy.vidkraken.com/media/69cfc45e8a3b4d2f9c1e0a7b.mp4
                    title: Rick Astley - Never Gonna Give You Up
                    duration: 213
                    fileSize: 32240544
                failed:
                  summary: Job failed with a known error code
                  value:
                    jobId: 69cfc45e8a3b4d2f9c1e0a7b
                    status: FAILED
                    errorCode: AGE_RESTRICTED
                    title: Some Restricted Video
                    duration: 412
                    url: https://youtu.be/example
                    format: "1080"
        "401":
          $ref: "#/components/responses/401 - Unauthorized"
        "404":
          $ref: "#/components/responses/404 - Not Found"
        "500":
          $ref: "#/components/responses/500 - Internal Server Error"
      security:
        - bearerAuth: []
  /list-channel:
    post:
      operationId: list-channel-videos
      summary: List channel videos
      description: |
        List recent videos for a YouTube channel. Returns the raw
        `yt-dlp --flat-playlist -J` output, including channel metadata
        and an `entries` array with one item per video.


        `maxVideos` controls how many videos are returned (1–1000,
        default 5). Higher values take proportionally longer.


        **Cost:** account add-on — not included in standard plans;
        contact hello@vidkraken.com to enable it. Counts 1 unit per 10
        items returned (rounded up). No bandwidth cost; failures are
        not charged.
      tags:
        - Channel
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListChannelRequest"
            examples:
              default:
                summary: First 5 videos of a channel
                value:
                  channelUrl: https://www.youtube.com/@RickAstleyYT
              with-limit:
                summary: First 25 videos
                value:
                  channelUrl: https://www.youtube.com/@RickAstleyYT
                  maxVideos: 25
      responses:
        "200":
          description: |
            Raw yt-dlp channel listing. The exact field set depends on
            yt-dlp's output for the given channel — most callers care
            about `entries[]`, where each entry is a video summary.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListChannelResponse"
        "400":
          $ref: "#/components/responses/400 - Bad Request"
        "401":
          $ref: "#/components/responses/401 - Unauthorized"
        "500":
          $ref: "#/components/responses/500 - Internal Server Error"
      security:
        - bearerAuth: []
  /me:
    get:
      operationId: get-account
      summary: Get account info & balances
      description: |
        Returns the account behind the API key — name, email, current
        plan, and the live balance on every metered feature. Use it to
        mirror your remaining quota into your own systems, alert before
        you run out, or verify which plan a key is on.


        `balances` is keyed by feature ID. Every account has
        `download_count`, `bandwidth_mb` and `info`; `client_download`
        and `channel_list` appear only on accounts where the add-on is
        enabled.


        **Cost:** free. Never counts against any quota and has no call
        limit — poll it as often as you like.
      tags:
        - Account
      responses:
        "200":
          description: Account details with live balances.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountResponse"
              example:
                name: Jane Developer
                email: jane@example.com
                plan:
                  id: starter
                  renewsAt: 1758326400000
                  canceledAt: null
                balances:
                  download_count:
                    granted: 1000
                    used: 123
                    remaining: 877
                    unlimited: false
                    resetsAt: 1758326400000
                  bandwidth_mb:
                    granted: 400000
                    used: 52480
                    remaining: 347520
                    unlimited: false
                    resetsAt: 1758326400000
                  info:
                    granted: 5000
                    used: 310
                    remaining: 4690
                    unlimited: false
                    resetsAt: 1758326400000
        "401":
          $ref: "#/components/responses/401 - Unauthorized"
        "500":
          $ref: "#/components/responses/500 - Internal Server Error"
      security:
        - bearerAuth: []

webhooks:
  download.finished:
    post:
      operationId: webhook-download-finished
      summary: Download finished (webhook)
      description: |
        Sent to the `webhookUrl` you passed on `POST /download` when that job
        reaches a terminal state. Fires once per job — either `download.completed`
        or `download.failed` — and the JSON body is exactly what
        `GET /download/{jobId}` returns for the job at that moment.


        **Acknowledge** with any `2xx` within 15 seconds. Anything else (a
        non-2xx status, a redirect, a timeout, a connection error) is retried
        with exponential backoff: 30s, 1m, 2m, 4m, 8m, 16m, 32m, 64m, 128m —
        10 attempts over roughly 4 hours. Reply `410 Gone` to stop retries
        early. Delivery is at-least-once; treat `jobId` as your idempotency key.


        **Verify the signature.** Every request carries
        `X-VidKraken-Signature: t=<unix seconds>,v1=<hex>` where `v1` is
        `HMAC-SHA256(key = your API key, message = "<t>.<raw request body>")`.
        Compute it over the raw bytes (before JSON parsing), compare in
        constant time, and reject timestamps older than a few minutes to
        defeat replays.


        ```js
        import { createHmac, timingSafeEqual } from "node:crypto";

        function verify(rawBody, signatureHeader, apiKey) {
          const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
          const expected = createHmac("sha256", apiKey).update(`${parts.t}.${rawBody}`).digest("hex");
          const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
          return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
        }
        ```
      tags:
        - Download
      parameters:
        - in: header
          name: X-VidKraken-Event
          required: true
          schema:
            type: string
            enum:
              - download.completed
              - download.failed
          description: Which terminal state the job reached. Mirrors `status` in the body.
        - in: header
          name: X-VidKraken-Job-Id
          required: true
          schema:
            type: string
          description: The `jobId` this delivery is about.
        - in: header
          name: X-VidKraken-Attempt
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 10
          description: Delivery attempt number, starting at 1.
        - in: header
          name: X-VidKraken-Signature
          required: true
          schema:
            type: string
          description: "`t=<unix seconds>,v1=<hex HMAC-SHA256>` — see the description above."
          example: t=1756742400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e7c4c1d1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/DownloadJobCompleted"
                - $ref: "#/components/schemas/DownloadJobFailed"
              discriminator:
                propertyName: status
                mapping:
                  COMPLETED: "#/components/schemas/DownloadJobCompleted"
                  FAILED: "#/components/schemas/DownloadJobFailed"
            examples:
              completed:
                summary: download.completed
                value:
                  jobId: 69cfc45e8a3b4d2f9c1e0a7b
                  status: COMPLETED
                  url: https://youtu.be/dQw4w9WgXcQ
                  format: "1080"
                  actualFormat: "1080"
                  language: null
                  downloadUrl: https://proxy.vidkraken.com/media/69cfc45e8a3b4d2f9c1e0a7b.mp4
                  title: Rick Astley - Never Gonna Give You Up
                  duration: 213
                  fileSize: 32240544
              failed:
                summary: download.failed
                value:
                  jobId: 69cfc45e8a3b4d2f9c1e0a7b
                  status: FAILED
                  errorCode: AGE_RESTRICTED
                  title: Some Restricted Video
                  duration: 412
                  url: https://youtu.be/example
                  format: "1080"
                  language: null
      responses:
        "2XX":
          description: Acknowledged — we stop retrying.
        "410":
          description: Endpoint gone — we stop retrying without further attempts.
        default:
          description: Treated as a failed attempt and retried with backoff.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key issued from the dashboard, sent as `Authorization: Bearer YOUR_API_KEY`."
  schemas:
    MediaFormat:
      type: string
      description: |
        Target media format. Numeric values are vertical resolutions in
        pixels, delivered as MP4.

        `audio` delivers the best audio stream YouTube serves, in whatever
        container YouTube gives it — usually M4A (AAC) or WebM (Opus), at
        roughly 130–160 kbps. Nothing is re-encoded, so it is the fastest
        option and the highest quality available. Use this unless your
        pipeline needs one specific container. Note that YouTube does not
        serve higher audio bitrates for regular videos, so no option here
        produces better audio than this one.

        `audio-aac` prefers YouTube's AAC stream (M4A, ~128 kbps) over Opus —
        the most widely compatible audio codec, and useful when a pipeline was
        calibrated on AAC. Nothing is re-encoded. If YouTube offers no AAC
        stream for a video, the best available audio is delivered instead and
        `actualFormat` reports `audio` rather than `audio-aac`, so the
        substitution is visible.

        `audio-mp3` always delivers an MP3, encoded at 320 kbps CBR — the
        highest rate the MP3 format defines — using LAME's best-quality
        analysis. The source's sample rate and channel layout are preserved
        rather than forced: 48 kHz audio stays 48 kHz with no resampling, and
        a mono source stays mono.
      enum:
        - "1080"
        - "720"
        - "480"
        - "360"
        - audio
        - audio-aac
        - audio-mp3
    DownloadJobStatus:
      type: string
      enum:
        - IN_QUEUE
        - IN_PROGRESS
        - COMPLETED
        - FAILED
    DownloadErrorCode:
      type: string
      description: |
        Machine-readable failure reason. Except for `NETWORK_ERROR`, every
        value below describes the video or our policy, not a fault on our
        side — retrying will return the same result, so treat them as final.

        Other values may appear for transient infra failures — retry after a
        short backoff before treating them as fatal.

        - `AGE_RESTRICTED` — sign-in required by YouTube for this video
        - `MEMBERS_ONLY` — restricted to channel members
        - `PAYMENT_REQUIRED` — purchase or rental required
        - `PRIVATE_VIDEO` — the video is private
        - `VIDEO_PREMIERE` — a scheduled premiere that has not aired yet; retry after the premiere starts
        - `GEO_RESTRICTED` — not available in the server region
        - `VIDEO_PROCESSING` — YouTube is still processing the upload
        - `VIDEO_UNAVAILABLE` — unavailable for an unspecified reason
        - `DRM_PROTECTED` — DRM protected
        - `VIDEO_NOT_FOUND` — no such video, or it has been deleted
        - `FORMAT_UNAVAILABLE` — the requested format/quality is not offered
        - `LIVE_STREAM_IN_PROGRESS` — still live; retry once it has ended
        - `COPYRIGHTED_MUSIC` — copyrighted music, not downloadable here
        - `NO_DOWNLOAD_OPT_OUT` — the creator opted out via `#nodownload`
        - `NETWORK_ERROR` — transient network failure on our side; retry after a short backoff
      enum:
        - AGE_RESTRICTED
        - MEMBERS_ONLY
        - PAYMENT_REQUIRED
        - PRIVATE_VIDEO
        - VIDEO_PREMIERE
        - GEO_RESTRICTED
        - VIDEO_PROCESSING
        - VIDEO_UNAVAILABLE
        - DRM_PROTECTED
        - VIDEO_NOT_FOUND
        - FORMAT_UNAVAILABLE
        - LIVE_STREAM_IN_PROGRESS
        - COPYRIGHTED_MUSIC
        - NO_DOWNLOAD_OPT_OUT
        - NETWORK_ERROR
    SubmitDownloadRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          description: |
            YouTube video URL. Supported patterns:

            - `https://youtube.com/watch?v=ID`

            - `https://youtu.be/ID`

            - `https://youtube.com/live/ID`
          example: https://youtu.be/dQw4w9WgXcQ
        format:
          allOf:
            - $ref: "#/components/schemas/MediaFormat"
          default: audio
          description: Target format. Defaults to `audio`. Higher video qualities may be capped down for very long videos to fit your plan's per-file size limit.
        startTime:
          type: integer
          minimum: 0
          description: Trim start in seconds (inclusive). Must be in `[0, duration)`. Omit to start from the beginning.
        endTime:
          type: integer
          minimum: 1
          description: Trim end in seconds (exclusive). Must be in `(0, duration]` and greater than `startTime`. Omit to go to the end.
        language:
          type: string
          pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$"
          description: |
            Preferred audio track language as a BCP-47 tag (e.g. `en`,
            `en-US`, `th`). Use `POST /info` with `includeAudioTracks: true`
            to discover what's available for a given video —
            `audio[].language` lists the languages and `audio[].type`
            indicates `original` vs. `dubbed`.


            **Fallback chain:** if the requested language isn't available
            on the video, the download falls back to (1) any track whose
            language tag starts with the requested value (e.g. `en`
            matches `en-US`), then to (2) the video's original/default
            audio. Omit this field to always use the original audio.
          example: th
        webhookUrl:
          type: string
          format: uri
          maxLength: 2048
          description: |
            Optional. A public `https://` (or `http://`) URL we POST the job's
            final status to once it reaches `COMPLETED` or `FAILED`. The body is
            identical to the `GET /download/{jobId}` response, so one handler
            can serve both. Requests are signed with your API key and retried
            with exponential backoff for up to 10 attempts (~4 hours); see the
            `download.finished` webhook for details.


            Loopback, private-network and single-label hostnames (e.g.
            `localhost`, `10.x`, `192.168.x`, `intranet`) are rejected with
            `400`. Redirects are not followed — respond `2xx` directly.
          example: https://example.com/hooks/vidkraken
    SubmitDownloadResponse:
      type: object
      required:
        - jobId
        - title
        - duration
        - status
        - message
      properties:
        jobId:
          type: string
          description: Unique ID for the download job. Use this to poll the status endpoint.
        title:
          type: string
          description: Video title as reported by YouTube.
        duration:
          type: integer
          description: Full video duration in seconds.
        status:
          type: string
          enum:
            - IN_QUEUE
        message:
          type: string
          description: Human-readable next-step hint.
    DownloadJobStatusResponse:
      oneOf:
        - $ref: "#/components/schemas/DownloadJobPending"
        - $ref: "#/components/schemas/DownloadJobCompleted"
        - $ref: "#/components/schemas/DownloadJobFailed"
      discriminator:
        propertyName: status
        mapping:
          IN_QUEUE: "#/components/schemas/DownloadJobPending"
          IN_PROGRESS: "#/components/schemas/DownloadJobPending"
          COMPLETED: "#/components/schemas/DownloadJobCompleted"
          FAILED: "#/components/schemas/DownloadJobFailed"
    DownloadJobPending:
      type: object
      required:
        - jobId
        - status
        - title
        - duration
        - url
        - format
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - IN_QUEUE
            - IN_PROGRESS
        title:
          type: string
        duration:
          type: integer
          description: Full video duration in seconds.
        url:
          type: string
          description: The original YouTube URL submitted.
        format:
          $ref: "#/components/schemas/MediaFormat"
        language:
          type:
            - string
            - "null"
          description: Audio language requested in the original POST, or `null` if none was specified.
        createdAt:
          type: string
          format: date-time
          description: When the job was submitted (ISO 8601).
        startedAt:
          type:
            - string
            - "null"
          format: date-time
          description: When processing started, or `null` if still queued.
        completedAt:
          type:
            - string
            - "null"
          format: date-time
          description: When the job reached a terminal state, or `null`.
    DownloadJobCompleted:
      type: object
      required:
        - jobId
        - status
        - url
        - format
        - actualFormat
        - downloadUrl
        - title
        - duration
        - fileSize
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - COMPLETED
        url:
          type: string
          description: The original YouTube URL submitted.
        format:
          allOf:
            - $ref: "#/components/schemas/MediaFormat"
          description: The format requested in the original POST.
        actualFormat:
          allOf:
            - $ref: "#/components/schemas/MediaFormat"
          description: Format actually delivered. May differ from the requested format if it was capped down for size.
        language:
          type:
            - string
            - "null"
          description: Audio language requested in the original POST, or `null` if none was specified. The actual language used may differ if a fallback was applied.
        downloadUrl:
          type: string
          format: uri
          description: CDN URL to download the produced file from.
        title:
          type: string
        duration:
          type: integer
          description: Full video duration in seconds.
        fileSize:
          type: integer
          description: Size of the produced file in bytes.
        createdAt:
          type: string
          format: date-time
          description: When the job was submitted (ISO 8601).
        startedAt:
          type:
            - string
            - "null"
          format: date-time
          description: When processing started, or `null` if still queued.
        completedAt:
          type:
            - string
            - "null"
          format: date-time
          description: When the job reached a terminal state, or `null`.
    DownloadJobFailed:
      type: object
      required:
        - jobId
        - status
        - title
        - duration
        - url
        - format
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - FAILED
        errorCode:
          $ref: "#/components/schemas/DownloadErrorCode"
        title:
          type: string
        duration:
          type: integer
        url:
          type: string
        format:
          $ref: "#/components/schemas/MediaFormat"
        language:
          type:
            - string
            - "null"
          description: Audio language requested in the original POST, or `null` if none was specified.
        createdAt:
          type: string
          format: date-time
          description: When the job was submitted (ISO 8601).
        startedAt:
          type:
            - string
            - "null"
          format: date-time
          description: When processing started, or `null` if still queued.
        completedAt:
          type:
            - string
            - "null"
          format: date-time
          description: When the job reached a terminal state, or `null`.
    GetVideoInfoRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          description: |
            YouTube video URL. Supported patterns:

            - `https://youtube.com/watch?v=ID`

            - `https://youtu.be/ID`

            - `https://youtube.com/live/ID`
          example: https://youtu.be/dQw4w9WgXcQ
        includeAudioTracks:
          type: boolean
          default: false
          description: |
            When `true`, the result also lists the video's available audio
            tracks (`audio`) — original + dubbed languages. This requires a
            full extraction, so it is slower than a plain metadata request.
            When omitted or `false`, `audio` is absent from the result.
    AudioTrack:
      type: object
      required:
        - type
        - language
      properties:
        type:
          type: string
          enum:
            - original
            - dubbed
        language:
          type:
            - string
            - "null"
          description: ISO language code, or `null` if unknown.
    VideoInfo:
      type: object
      required:
        - title
        - duration
        - url
      properties:
        title:
          type: string
        duration:
          type: integer
          description: Video duration in seconds.
        url:
          type: string
        audio:
          type: array
          description: "Available audio tracks — only present when the job was submitted with `includeAudioTracks: true`. The first entry with `type: original` is the original soundtrack; `dubbed` entries are language-overdubbed alternatives."
          items:
            $ref: "#/components/schemas/AudioTrack"
    SubmitVideoInfoResponse:
      type: object
      required:
        - jobId
        - status
      properties:
        jobId:
          type: string
          format: uuid
          description: Unique ID for the video-info job. Use this to poll the status endpoint.
        status:
          type: string
          enum:
            - pending
    VideoInfoJobStatusResponse:
      oneOf:
        - $ref: "#/components/schemas/VideoInfoJobPending"
        - $ref: "#/components/schemas/VideoInfoJobSuccess"
        - $ref: "#/components/schemas/VideoInfoJobFailed"
      discriminator:
        propertyName: status
        mapping:
          pending: "#/components/schemas/VideoInfoJobPending"
          success: "#/components/schemas/VideoInfoJobSuccess"
          failed: "#/components/schemas/VideoInfoJobFailed"
    VideoInfoJobPending:
      type: object
      required:
        - jobId
        - status
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - pending
    VideoInfoJobSuccess:
      type: object
      required:
        - jobId
        - status
        - result
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - success
        result:
          $ref: "#/components/schemas/VideoInfo"
    VideoInfoJobFailed:
      type: object
      required:
        - jobId
        - status
        - error
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - failed
        error:
          type: string
          description: Human-readable failure reason.
        errorCode:
          $ref: "#/components/schemas/DownloadErrorCode"
          description: >-
            Machine-readable reason, present when the failure is a recognised
            condition — of the video itself (private, age-restricted,
            geo-blocked, not found) or a transient one like `NETWORK_ERROR`.
            Absent for unrecognised failures on our side; those are worth
            retrying.
    ListChannelRequest:
      type: object
      required:
        - channelUrl
      properties:
        channelUrl:
          type: string
          description: YouTube channel URL (e.g. `https://www.youtube.com/@channelHandle`).
          example: https://www.youtube.com/@RickAstleyYT
        maxVideos:
          type: integer
          minimum: 1
          maximum: 1000
          default: 5
          description: Maximum number of videos to return.
    ListChannelResponse:
      type: object
      description: |
        Raw yt-dlp `--flat-playlist -J` output. Includes channel
        metadata at the top level and an `entries` array with one
        item per video. Field availability depends on yt-dlp's
        output for the given channel.
      additionalProperties: true
      properties:
        id:
          type: string
        title:
          type: string
        channel:
          type: string
        channel_id:
          type: string
        channel_url:
          type: string
        entries:
          type: array
          description: Flat list of videos. Items are yt-dlp's flat-playlist video summaries.
          items:
            type: object
            additionalProperties: true
            properties:
              id:
                type: string
                description: Video ID.
              title:
                type: string
              url:
                type: string
              duration:
                type: number
                description: Duration in seconds.
              view_count:
                type: integer
              uploader:
                type: string
    SubmitClientDownloadRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          description: |
            Source URL. Supported platforms:

            - **YouTube** — `youtube.com/watch?v=ID`, `youtu.be/ID`, `youtube.com/live/ID`, `youtube.com/shorts/ID`

            - **Instagram** — `instagram.com/p/ID`, `/reel/ID`, `/reels/ID`, `/tv/ID`

            - **TikTok** — `tiktok.com/@user/video/ID`, `vm.tiktok.com/ID`

            - **Facebook** — `facebook.com/.../{videos,watch,reel}/ID`, `fb.watch/ID`
          example: https://youtu.be/dQw4w9WgXcQ
    SubmitClientDownloadResponse:
      type: object
      required:
        - jobId
        - status
      properties:
        jobId:
          type: string
          format: uuid
          description: Unique ID for the client-download job. Use this to poll the status endpoint.
        status:
          type: string
          enum:
            - pending
    ClientDownloadLink:
      type: object
      required:
        - url
        - type
        - hasAudio
      properties:
        url:
          type: string
          format: uri
          description: Direct media URL. Fetch this from your client.
        type:
          type: string
          enum:
            - video
            - audio
        width:
          type: integer
          description: Video width in pixels (video links only).
        height:
          type: integer
          description: Video height in pixels (video links only).
        hasAudio:
          type: boolean
          description: For `video` links, whether the stream has the audio track muxed in.
        audioBitrate:
          type: integer
          description: Audio bitrate in kbps (audio links only).
        sizeBytes:
          type: integer
          description: File size in bytes, when known.
    ClientDownloadInfo:
      type: object
      required:
        - title
        - url
        - duration
        - downloadLinks
      properties:
        title:
          type: string
        description:
          type: string
        url:
          type: string
          description: The original source URL submitted.
        author:
          type: string
        authorUrl:
          type: string
        duration:
          type: number
          description: Duration in seconds.
        thumbnailUrl:
          type: string
          format: uri
        downloadLinks:
          type: array
          items:
            $ref: "#/components/schemas/ClientDownloadLink"
    ClientDownloadJobStatusResponse:
      oneOf:
        - $ref: "#/components/schemas/ClientDownloadJobPending"
        - $ref: "#/components/schemas/ClientDownloadJobSuccess"
        - $ref: "#/components/schemas/ClientDownloadJobFailed"
      discriminator:
        propertyName: status
        mapping:
          pending: "#/components/schemas/ClientDownloadJobPending"
          success: "#/components/schemas/ClientDownloadJobSuccess"
          failed: "#/components/schemas/ClientDownloadJobFailed"
    ClientDownloadJobPending:
      type: object
      required:
        - jobId
        - status
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - pending
    ClientDownloadJobSuccess:
      type: object
      required:
        - jobId
        - status
        - result
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - success
        result:
          $ref: "#/components/schemas/ClientDownloadInfo"
    ClientDownloadJobFailed:
      type: object
      required:
        - jobId
        - status
        - error
      properties:
        jobId:
          type: string
        status:
          type: string
          enum:
            - failed
        error:
          type: string
          description: Human-readable failure reason.
    FeatureBalance:
      type: object
      description: Live balance for one metered feature.
      required:
        - granted
        - used
        - remaining
        - unlimited
        - resetsAt
      properties:
        granted:
          type: number
          description: Total units included in the current cycle (plan quota plus any extra grants).
        used:
          type: number
          description: Units consumed in the current cycle.
        remaining:
          type: number
          description: Units left before the limit (or overage pricing) kicks in.
        unlimited:
          type: boolean
          description: When `true`, usage is not capped and the numeric fields can be ignored.
        resetsAt:
          type: [number, "null"]
          description: Unix timestamp (ms) when the balance resets, or `null` for one-off grants that never reset (e.g. the free plan).
    AccountResponse:
      type: object
      required:
        - name
        - email
        - plan
        - balances
      properties:
        name:
          type: string
          description: Account holder's name.
        email:
          type: string
          description: Account email address.
        plan:
          type: object
          required:
            - id
            - renewsAt
            - canceledAt
          properties:
            id:
              type: string
              description: Current plan ID — `free`, `starter`, `growth`, `scale`, or `custom`.
              example: starter
            renewsAt:
              type: [number, "null"]
              description: Unix timestamp (ms) when the current billing period ends and quotas renew. `null` on the free plan.
            canceledAt:
              type: [number, "null"]
              description: When set (Unix ms), the plan is scheduled to cancel at the end of the current period.
        balances:
          type: object
          description: |
            Live balances keyed by feature ID. Always present:
            `download_count`, `bandwidth_mb`, `info`. Present only when
            the add-on is enabled on the account: `client_download`,
            `channel_list`.
          additionalProperties:
            $ref: "#/components/schemas/FeatureBalance"
    ApiError:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message.
        details:
          type: string
          description: Optional extra context — e.g. yt-dlp's error message on `500`s.
  responses:
    400 - Bad Request:
      description: Invalid input — missing or malformed field.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
          examples:
            missing-url:
              value:
                error: "Missing required field: url"
            bad-format:
              value:
                error: "Invalid format. Must be one of: 1080, 720, 480, 360, audio"
            live-in-progress:
              value:
                error: Live on-going videos are not supported. Try again after the live stream ends.
    401 - Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
          examples:
            missing:
              value:
                error: "Missing API key. Provide it as 'Bearer YOUR_API_KEY'"
            invalid:
              value:
                error: Invalid API key
    403 - Forbidden:
      description: |
        Usage limit reached for the current plan — the bandwidth
        allowance (plus any overage allowance) or a request quota is
        exhausted. Upgrade or wait for the next billing cycle. The
        error string is `Credit limit reached` for backwards
        compatibility.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
          example:
            error: Credit limit reached
    429 - Too Many Requests:
      description: |
        You already have 100 downloads queued or in progress. Wait for
        some to reach `COMPLETED` / `FAILED` (poll or use a `webhookUrl`),
        then resubmit. Not charged.
      headers:
        X-Pending-Downloads:
          description: Your downloads currently queued or in progress.
          schema:
            type: integer
          example: 100
        X-Pending-Downloads-Limit:
          description: Maximum allowed queued or in progress at once.
          schema:
            type: integer
          example: 100
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
          example:
            error: "Too many pending downloads (100). Wait for some of your jobs to finish before submitting more (limit: 100 queued or in progress)."
    404 - Not Found:
      description: Job not found, expired, or belongs to another user.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
          example:
            error: Download job not found
    500 - Internal Server Error:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
          example:
            error: Failed to enqueue download job
externalDocs:
  url: https://vidkraken.com/docs
  description: Hosted documentation
