{
  "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\nlinks from YouTube, Instagram, TikTok, and Facebook.\n\n\n## Getting Started\n\n- Sign up at [vidkraken.com](https://vidkraken.com)\n\n- Grab your API key from the [dashboard](https://vidkraken.com/dashboard)\n\n- Pass it on every request as: `Authorization: Bearer YOUR_API_KEY`\n\n\n## Usage & billing\n\nYou pay for bandwidth only. Every plan includes a monthly bandwidth\nallowance; each download counts its actual file size against it,\nwith a 20 MB minimum per download. There are no per-download fees.\nDownloads and metadata lookups are counted against generous\nper-plan quotas that are sized so bandwidth always runs out first.\nFailures are never charged.\n\n\n| Endpoint | Counts against |\n|---|---|\n| `POST /download` | Bandwidth at actual file size (20 MB minimum) |\n| `POST /info` | 1 info request |\n| `POST /list-channel` | Account add-on — contact us to enable |\n| `POST /client-download` | Account add-on — contact us to enable |\n\n\nPolling endpoints (`GET /download/{jobId}`, `GET /info/{jobId}`,\n`GET /client-download/{jobId}`) are free, as is `GET /me` for\nchecking your plan and remaining balances. Failed jobs, auth\nfailures (`401`) and limit errors (`403`, `429`) are not charged.\n\n\nEach account may have at most 100 downloads queued or in progress at\nonce; `POST /download` returns `429` beyond that until some finish.\nEvery `POST /download` response carries the IETF `RateLimit-Policy`\nand `RateLimit` headers for this cap (`\"pending\";r=37` means 37 more\ndownloads can be queued now), and the `429` adds `Retry-After`.\n\n\n## Safe retries (Idempotency-Key)\n\n`POST /download` and `POST /info` accept an optional `Idempotency-Key`\nheader (any unique string, e.g. a UUID). If a request times out or the\nconnection drops, retry it with the same key: you get the original\njob back instead of starting, and paying for, a second one. Keys are\nkept for 24 hours per account. Reusing a key with a different body\nreturns `422`; retrying while the first request is still running\nreturns `409` (wait `Retry-After` seconds). Failed requests aren't\nstored, so retrying one with the same key runs it again. A replayed\nresponse carries `Idempotent-Replayed: true`.\n\n\n## Errors\n\nEvery error response is JSON with a human-readable `error` and a\nmachine-readable `code` (see `ApiError`); some add `details`. Branch\non `code`, not on the message:\n\n| `code` | Status | What to do |\n|---|---|---|\n| `INVALID_REQUEST` | 400 | Fix the request; retrying it unchanged fails again |\n| `UNAUTHORIZED` | 401 | Send a valid API key |\n| `LIMIT_REACHED` | 403 | The plan's allowance is used up; upgrade or wait for the next cycle |\n| `NOT_FOUND` | 404 | The job does not exist, has expired or is not yours |\n| `VIDEO_NOT_FOUND` | 404 | No such video: private, deleted or a wrong id |\n| `IDEMPOTENCY_CONFLICT` | 409 | A request with this `Idempotency-Key` is still running; retry after `Retry-After` seconds |\n| `IDEMPOTENCY_KEY_REUSED` | 422 | This `Idempotency-Key` was used with a different body; use a new key |\n| `TOO_MANY_PENDING` | 429 | Wait `Retry-After` seconds or until some downloads finish |\n| `INTERNAL_ERROR` | 500 | Our fault; retry after a short backoff |\n| `UNAVAILABLE` | 503 | Temporarily unavailable; retry after a short backoff |\n\nA download that was accepted but then failed is not an HTTP error: its\nstatus response says `FAILED` with an `errorCode` for the video (see\n`DownloadErrorCode`).\n\n\n## Versioning and deprecation\n\nThe API is versioned in the path (`/api/v2`). Within v2 we only make\nbackward-compatible changes: new endpoints, new optional request\nfields, new response fields and new enum values, so handle unknown\n`code`, `errorCode` and `status` values gracefully. Breaking changes\nship as a new version (`/api/v3`), and v2 keeps working alongside it.\n\n\nBefore an endpoint, field or version is removed, you get at least\n**3 months' notice**: account owners are emailed, the change is\ndocumented here, and affected responses carry a `Deprecation` header\n(RFC 9745) and a `Sunset` header (RFC 8594) with the removal date,\nplus a `Link` to migration notes. Nothing is deprecated today.\n",
    "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\nduration. Returns a `jobId` that you poll on `GET /info/{jobId}`.\n\n\nSet `includeAudioTracks: true` to also resolve the list of available\naudio tracks (original + dubbed languages, if any). Audio tracks\nrequire a full extraction, which is slower and can occasionally fail\non hard-to-reach videos — only request them when you need to pick an\naudio language ahead of a download.\n\n\nUseful for showing video details in your UI before queueing a\ndownload, or for picking an audio language ahead of time.\n\n\nJob state is kept in Redis with a 1-hour TTL — make sure to\npoll within that window.\n\n\n**Cost:** 1 info request from your plan's info quota, on\nsuccess only. No bandwidth cost. Failures are not charged.\n",
        "tags": [
          "Info"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GetVideoInfoRequest"
              },
              "example": {
                "url": "https://youtu.be/dQw4w9WgXcQ"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job enqueued.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmitVideoInfoResponse"
                },
                "example": {
                  "jobId": "6f9c1234-5e8a-4b2f-9c1e-0a7b69cfc45e",
                  "status": "pending"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LimitReached"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        },
        "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.\n\n\n- `pending` — still fetching metadata\n\n\n- `success` — `result` contains the resolved `VideoInfo`\n\n\n- `failed` — `error` carries a human-readable message and, for recognised\n  video conditions, `errorCode` identifies it (see `DownloadErrorCode`)\n\n\nJob state expires from Redis 1 hour after submission; polling\nafter that returns `404`.\n\n\n**Cost:** Free — polling is never charged.\n",
        "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/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/JobNotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        },
        "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\n`GET /download/{jobId}` until status is `COMPLETED` (or `FAILED`).\n\n\nPrefer push over polling? Pass a `webhookUrl` and we POST the final\nstatus to it instead — the body is byte-for-byte the\n`GET /download/{jobId}` response. See the `download.finished`\nwebhook below for headers, signature verification and retries.\n\n\nThe actually-delivered format is reported as `actualFormat` on the\ncompleted status response.\n\n\n**Cost:** bandwidth at the actual file size delivered (20 MB\nminimum), charged when the background job completes —\nsubmitting the job is free. There is no per-download fee.\nFailures are not charged.\n",
        "tags": [
          "Download"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "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.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "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/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LimitReached"
          },
          "404": {
            "$ref": "#/components/responses/VideoNotFound"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "429": {
            "$ref": "#/components/responses/TooManyPending"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "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`\nis `COMPLETED`, the response carries a `downloadUrl` you can fetch\nthe file from. If `status` is `FAILED`, an `errorCode` identifies\nwhy.\n\n\n**Cost:** Free — polling is never charged.\n",
        "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",
                      "language": null,
                      "createdAt": "2026-09-23T20:10:00Z",
                      "startedAt": "2026-09-23T20:10:04Z",
                      "completedAt": null,
                      "progress": {
                        "stage": "downloading",
                        "percent": 41,
                        "bytesDownloaded": 43500000,
                        "totalBytes": 102300000,
                        "totalBytesIsEstimate": false,
                        "updatedAt": "2026-09-23T20:10:19Z"
                      }
                    }
                  },
                  "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/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/JobNotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/list-channel": {
      "post": {
        "operationId": "list-channel-videos",
        "summary": "List channel videos",
        "description": "List recent videos for a YouTube channel. Returns the raw\n`yt-dlp --flat-playlist -J` output, including channel metadata\nand an `entries` array with one item per video.\n\n\n`maxVideos` controls how many videos are returned (1–1000,\ndefault 5). Higher values take proportionally longer.\n\n\n**Cost:** account add-on — not included in standard plans;\ncontact hello@vidkraken.com to enable it. Counts 1 unit per 10\nitems returned (rounded up). No bandwidth cost; failures are\nnot charged.\n",
        "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\nyt-dlp's output for the given channel — most callers care\nabout `entries[]`, where each entry is a video summary.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListChannelResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LimitReached"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/me": {
      "get": {
        "operationId": "get-account",
        "summary": "Get account info & balances",
        "description": "Returns the account behind the API key — name, email, current\nplan, and the live balance on every metered feature. Use it to\nmirror your remaining quota into your own systems, alert before\nyou run out, or verify which plan a key is on.\n\n\n`balances` is keyed by feature ID. Every account has\n`download_count`, `bandwidth_mb` and `info`; `client_download`\nand `channel_list` appear only on accounts where the add-on is\nenabled.\n\n\n**Cost:** free. Never counts against any quota and has no call\nlimit — poll it as often as you like.\n",
        "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/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        },
        "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\nreaches a terminal state. Fires once per job — either `download.completed`\nor `download.failed` — and the JSON body is exactly what\n`GET /download/{jobId}` returns for the job at that moment.\n\n\n**Acknowledge** with any `2xx` within 15 seconds. Anything else (a\nnon-2xx status, a redirect, a timeout, a connection error) is retried\nwith exponential backoff: 30s, 1m, 2m, 4m, 8m, 16m, 32m, 64m, 128m —\n10 attempts over roughly 4 hours. Reply `410 Gone` to stop retries\nearly. Delivery is at-least-once; treat `jobId` as your idempotency key.\n\n\n**Verify the signature.** Every request carries\n`X-VidKraken-Signature: t=<unix seconds>,v1=<hex>` where `v1` is\n`HMAC-SHA256(key = your API key, message = \"<t>.<raw request body>\")`.\nCompute it over the raw bytes (before JSON parsing), compare in\nconstant time, and reject timestamps older than a few minutes to\ndefeat replays.\n\n\n```js\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nfunction verify(rawBody, signatureHeader, apiKey) {\n  const parts = Object.fromEntries(signatureHeader.split(\",\").map((p) => p.split(\"=\")));\n  const expected = createHmac(\"sha256\", apiKey).update(`${parts.t}.${rawBody}`).digest(\"hex\");\n  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;\n  return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));\n}\n```\n",
        "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": {
          "410": {
            "description": "Endpoint gone — we stop retrying without further attempts."
          },
          "2XX": {
            "description": "Acknowledged — we stop retrying."
          },
          "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\npixels, delivered as MP4.\n\n`audio` delivers the best audio stream YouTube serves, in whatever\ncontainer YouTube gives it — usually M4A (AAC) or WebM (Opus), at\nroughly 130–160 kbps. Nothing is re-encoded, so it is the fastest\noption and the highest quality available. Use this unless your\npipeline needs one specific container. Note that YouTube does not\nserve higher audio bitrates for regular videos, so no option here\nproduces better audio than this one.\n\n`audio-aac` prefers YouTube's AAC stream (M4A, ~128 kbps) over Opus —\nthe most widely compatible audio codec, and useful when a pipeline was\ncalibrated on AAC. Nothing is re-encoded. If YouTube offers no AAC\nstream for a video, the best available audio is delivered instead and\n`actualFormat` reports `audio` rather than `audio-aac`, so the\nsubstitution is visible.\n\n`audio-mp3` always delivers an MP3, encoded at 320 kbps CBR — the\nhighest rate the MP3 format defines — using LAME's best-quality\nanalysis. The source's sample rate and channel layout are preserved\nrather than forced: 48 kHz audio stays 48 kHz with no resampling, and\na mono source stays mono.\n",
        "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\nvalue below describes the video or our policy, not a fault on our\nside — retrying will return the same result, so treat them as final.\n\nOther values may appear for transient infra failures — retry after a\nshort backoff before treating them as fatal.\n\n- `AGE_RESTRICTED` — sign-in required by YouTube for this video\n- `MEMBERS_ONLY` — restricted to channel members\n- `PAYMENT_REQUIRED` — purchase or rental required\n- `PRIVATE_VIDEO` — the video is private\n- `VIDEO_PREMIERE` — a scheduled premiere that has not aired yet; retry after the premiere starts\n- `GEO_RESTRICTED` — not available in the server region\n- `VIDEO_PROCESSING` — YouTube is still processing the video, usually a fresh upload or a live stream that ended recently; retry later\n- `VIDEO_UNAVAILABLE` — unavailable for an unspecified reason\n- `DRM_PROTECTED` — DRM protected\n- `VIDEO_NOT_FOUND` — no such video, or it has been deleted\n- `FORMAT_UNAVAILABLE` — the requested format/quality is not offered\n- `LIVE_STREAM_IN_PROGRESS` — still live; retry once it has ended\n- `COPYRIGHTED_MUSIC` — copyrighted music, not downloadable here\n- `NO_DOWNLOAD_OPT_OUT` — the creator opted out via `#nodownload`\n- `FILE_TOO_LARGE` — the file in this format is larger than we can deliver; try a lower-quality format or a shorter clip\n- `NETWORK_ERROR` — transient network failure on our side; retry after a short backoff\n",
        "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",
          "FILE_TOO_LARGE",
          "NETWORK_ERROR"
        ]
      },
      "SubmitDownloadRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "YouTube video URL. Supported patterns:\n\n- `https://youtube.com/watch?v=ID`\n\n- `https://youtu.be/ID`\n\n- `https://youtube.com/live/ID`\n",
            "example": "https://youtu.be/dQw4w9WgXcQ"
          },
          "format": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MediaFormat"
              }
            ],
            "default": "audio",
            "description": "Target format. Defaults to `audio`."
          },
          "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`,\n`en-US`, `th`). Use `POST /info` with `includeAudioTracks: true`\nto discover what's available for a given video —\n`audio[].language` lists the languages and `audio[].type`\nindicates `original` vs. `dubbed`.\n\n\n**Fallback chain:** if the requested language isn't available\non the video, the download falls back to (1) any track whose\nlanguage tag starts with the requested value (e.g. `en`\nmatches `en-US`), then to (2) the video's original/default\naudio. Omit this field to always use the original audio.\n",
            "example": "th"
          },
          "webhookUrl": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Optional. A public `https://` (or `http://`) URL we POST the job's\nfinal status to once it reaches `COMPLETED` or `FAILED`. The body is\nidentical to the `GET /download/{jobId}` response, so one handler\ncan serve both. Requests are signed with your API key and retried\nwith exponential backoff for up to 10 attempts (~4 hours); see the\n`download.finished` webhook for details.\n\n\nLoopback, private-network and single-label hostnames (e.g.\n`localhost`, `10.x`, `192.168.x`, `intranet`) are rejected with\n`400`. Redirects are not followed — respond `2xx` directly.\n",
            "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`."
          },
          "progress": {
            "$ref": "#/components/schemas/DownloadProgress"
          }
        }
      },
      "DownloadProgress": {
        "type": "object",
        "description": "Live progress of a running job. Present on `IN_QUEUE` and `IN_PROGRESS`\nresponses once there is something to report; absent otherwise. Poll as\noften as you like — polling is never charged.\n",
        "required": [
          "stage",
          "percent",
          "bytesDownloaded",
          "totalBytes",
          "totalBytesIsEstimate",
          "etaSeconds",
          "updatedAt"
        ],
        "properties": {
          "stage": {
            "type": "string",
            "enum": [
              "queued",
              "starting",
              "extracting",
              "downloading",
              "processing",
              "uploading"
            ],
            "description": "Where the job is. The same stages for every job, trimmed or not.\nA stage may repeat (for example `extracting` after `downloading`)\nwhen the job switches route mid-way; `percent` never moves\nbackwards.\n"
          },
          "percent": {
            "type": "integer",
            "minimum": 0,
            "maximum": 99,
            "description": "Overall progress. Only ever increases; reaches 100 when `status` becomes `COMPLETED`."
          },
          "bytesDownloaded": {
            "type": "integer",
            "description": "Bytes received so far in the current transfer."
          },
          "totalBytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Total bytes of the transfer. An estimate from format and duration until the real size is known."
          },
          "totalBytesIsEstimate": {
            "type": "boolean",
            "description": "`true` while `totalBytes` is an estimate."
          },
          "etaSeconds": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Seconds until the job is expected to complete. Deliberately\npessimistic: transfer rates are discounted and the stages still to\ncome are padded, so it usually runs down faster than the clock. It\ncan rise when the job switches route mid-way. `null` when there is\nnothing to base it on.\n"
          },
          "queuePosition": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Only while `stage` is `queued`: place in the queue, `1` = next to\nstart. `null` if it could not be counted at that moment.\n"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the worker last reported (ISO 8601)."
          }
        }
      },
      "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 the video isn't available in that quality."
          },
          "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:\n\n- `https://youtube.com/watch?v=ID`\n\n- `https://youtu.be/ID`\n\n- `https://youtube.com/live/ID`\n",
            "example": "https://youtu.be/dQw4w9WgXcQ"
          },
          "includeAudioTracks": {
            "type": "boolean",
            "default": false,
            "description": "When `true`, the result also lists the video's available audio\ntracks (`audio`) — original + dubbed languages. This requires a\nfull extraction, so it is slower than a plain metadata request.\nWhen omitted or `false`, `audio` is absent from the result.\n"
          }
        }
      },
      "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"
            }
          },
          "regionRestriction": {
            "type": "object",
            "description": "Where YouTube lets the video play, as ISO 3166-1 alpha-2 country codes. Absent when the video plays everywhere. `allowed` means only those countries; `blocked` means everywhere except those. A video limited to countries we cannot download from fails with `GEO_RESTRICTED`, so check this before submitting a download.",
            "properties": {
              "allowed": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "example": [
                  "TR"
                ]
              },
              "blocked": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "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\nmetadata at the top level and an `entries` array with one\nitem per video. Field availability depends on yt-dlp's\noutput for the given channel.\n",
        "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:\n\n- **YouTube** — `youtube.com/watch?v=ID`, `youtu.be/ID`, `youtube.com/live/ID`, `youtube.com/shorts/ID`\n\n- **Instagram** — `instagram.com/p/ID`, `/reel/ID`, `/reels/ID`, `/tv/ID`\n\n- **TikTok** — `tiktok.com/@user/video/ID`, `vm.tiktok.com/ID`\n\n- **Facebook** — `facebook.com/.../{videos,watch,reel}/ID`, `fb.watch/ID`\n",
            "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:\n`download_count`, `bandwidth_mb`, `info`. Present only when\nthe add-on is enabled on the account: `client_download`,\n`channel_list`.\n",
            "additionalProperties": {
              "$ref": "#/components/schemas/FeatureBalance"
            }
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code; branch on this rather than on\n`error`. New codes may be added, so treat an unknown one by\nits HTTP status. See the table under Errors in the API\ndescription for what each one means.\n",
            "enum": [
              "INVALID_REQUEST",
              "UNAUTHORIZED",
              "LIMIT_REACHED",
              "NOT_FOUND",
              "VIDEO_NOT_FOUND",
              "IDEMPOTENCY_CONFLICT",
              "IDEMPOTENCY_KEY_REUSED",
              "TOO_MANY_PENDING",
              "INTERNAL_ERROR",
              "UNAVAILABLE"
            ]
          },
          "details": {
            "type": "string",
            "description": "Optional extra context — e.g. yt-dlp's error message on `500`s."
          }
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "in": "header",
        "name": "Idempotency-Key",
        "required": false,
        "description": "Optional. A unique string (e.g. a UUID), up to 255 visible ASCII\ncharacters. Retrying with the same key and body within 24 hours\nreturns the original response instead of creating a second job.\n",
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "example": "4f1c9a2e-8d3b-4c7a-9f0e-2b6d5a1c3e7f"
      }
    },
    "headers": {
      "Idempotent-Replayed": {
        "description": "`true` when this response is a replay of an earlier request with the same Idempotency-Key.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "RateLimit-Policy": {
        "description": "The pending-downloads cap as an IETF RateLimit policy\n(draft-ietf-httpapi-ratelimit-headers): `q` downloads may be\nqueued or in progress at once.\n",
        "schema": {
          "type": "string"
        },
        "example": "\"pending\";q=100;qu=\"concurrent-requests\""
      },
      "RateLimit": {
        "description": "How many more downloads can be queued right now (`r`).",
        "schema": {
          "type": "string"
        },
        "example": "\"pending\";r=37"
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid input — missing or malformed field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "examples": {
              "missing-url": {
                "value": {
                  "error": "Missing required field: url",
                  "code": "INVALID_REQUEST"
                }
              },
              "bad-format": {
                "value": {
                  "error": "Invalid format. Must be one of: 1080, 720, 480, 360, audio",
                  "code": "INVALID_REQUEST"
                }
              },
              "live-in-progress": {
                "value": {
                  "error": "Live on-going videos are not supported. Try again after the live stream ends.",
                  "code": "INVALID_REQUEST"
                }
              },
              "too-long": {
                "value": {
                  "error": "This video is too long to download in audio-aac (21,812 hours, about 1,265 GB; the limit is 100 GB per file).",
                  "code": "INVALID_REQUEST"
                }
              }
            }
          }
        }
      },
      "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'",
                  "code": "UNAUTHORIZED"
                }
              },
              "invalid": {
                "value": {
                  "error": "Invalid API key",
                  "code": "UNAUTHORIZED"
                }
              }
            }
          }
        }
      },
      "LimitReached": {
        "description": "Usage limit reached for the current plan — the bandwidth\nallowance (plus any overage allowance) or a request quota is\nexhausted. Upgrade or wait for the next billing cycle. The\nerror string is `Credit limit reached` for backwards\ncompatibility.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "Credit limit reached",
              "code": "LIMIT_REACHED"
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "A request with this Idempotency-Key is still being processed. Retry after `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            },
            "example": 2
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "A request with this Idempotency-Key is still being processed. Retry shortly.",
              "code": "IDEMPOTENCY_CONFLICT"
            }
          }
        }
      },
      "IdempotencyKeyReused": {
        "description": "This Idempotency-Key was already used with a different request body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "This Idempotency-Key was already used with a different request body. Use a new key.",
              "code": "IDEMPOTENCY_KEY_REUSED"
            }
          }
        }
      },
      "TooManyPending": {
        "description": "You already have 100 downloads queued or in progress. Wait for\nsome to reach `COMPLETED` / `FAILED` (poll or use a `webhookUrl`),\nthen resubmit. Not charged.\n",
        "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
          },
          "Retry-After": {
            "description": "Seconds to wait before submitting again.",
            "schema": {
              "type": "integer"
            },
            "example": 30
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          }
        },
        "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).",
              "code": "TOO_MANY_PENDING"
            }
          }
        }
      },
      "JobNotFound": {
        "description": "Job not found, expired, or belongs to another user.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "Download job not found",
              "code": "NOT_FOUND"
            }
          }
        }
      },
      "VideoNotFound": {
        "description": "No such video — it is private, deleted, or the id is wrong.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "Video not found. Check the URL — the video may be private, deleted, or the id may be wrong.",
              "code": "VIDEO_NOT_FOUND"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Temporarily unable to accept the job. Not charged; retry after a short backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "The download could not be queued. Please try again in a minute.",
              "code": "UNAVAILABLE"
            }
          }
        }
      },
      "InternalServerError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "Failed to enqueue download job",
              "code": "INTERNAL_ERROR"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "url": "https://vidkraken.com/docs",
    "description": "Hosted documentation"
  }
}