> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tornadoapi.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Job

> Create a new download job

## Overview

Creates a new download job. For single videos, returns a `job_id`. For Spotify shows **and YouTube playlists**, automatically creates a batch and returns a `batch_id` with all episode/video job IDs.

<Warning>
  **If `url` contains a YouTube playlist ID, this endpoint downloads the ENTIRE playlist, not just one video.** This applies even if you only meant to download a single video -- see [Playlist Auto-Detection](#playlist-auto-detection) below before you're surprised by a batch of thousands of jobs from what looked like a single-video request.
</Warning>

## Header Parameters

<ParamField header="x-api-key" type="string" required>
  Your API key for authentication
</ParamField>

## Request

<ParamField body="url" type="string" required>
  The video or show URL to download. **If this URL contains a genuine YouTube playlist ID (`list=PL...`, `UU...`, or `OL...`) or is a Spotify show, the request downloads the WHOLE playlist/show as a batch** -- see [Playlist Auto-Detection](#playlist-auto-detection).
</ParamField>

<ParamField body="format" type="string">
  Output container format. **Video**: `mp4`, `mkv`, `webm`, `mov`. **Audio**: `m4a`, `mp3`, `ogg`, `opus`. Default: `mp4` (or `m4a` when `audio_only` is true)
</ParamField>

<ParamField body="video_codec" type="string">
  Video codec: `copy` (no re-encode), `h264`, `h265`, `vp9`. Default: `copy`
</ParamField>

<ParamField body="audio_codec" type="string">
  Audio codec: `copy` (no re-encode), `aac`, `opus`, `mp3`. Default: `copy` with automatic fallback to `aac` if incompatible
</ParamField>

<ParamField body="audio_bitrate" type="string">
  Audio bitrate when transcoding: `64k`, `128k`, `192k`, `256k`, `320k`. Default: `192k`
</ParamField>

<ParamField body="video_quality" type="integer">
  Video quality CRF (0-51, lower = better quality). Only used when `video_codec` is not `copy`. Default: `23`
</ParamField>

<ParamField body="filename" type="string">
  Custom filename (without extension). Max 255 characters. Cannot contain `..`, `/`, `\`, or null bytes.
</ParamField>

<ParamField body="folder" type="string">
  S3 folder prefix for organizing files. Max 200 characters. Cannot contain `..`, start with `/` or `\`, or be empty/whitespace-only.
</ParamField>

<ParamField body="webhook_url" type="string">
  URL to receive completion notification via POST request.
</ParamField>

<ParamField body="audio_only" type="boolean" default="false">
  Extract audio track only. Outputs `m4a` (native AAC, no re-encoding) by default. Set `format` to `mp3`, `ogg`, or `opus` for other audio formats. Supports `audio_codec` and `audio_bitrate` for transcoding control.
</ParamField>

<ParamField body="download_subtitles" type="boolean" default="false">
  Download subtitles if available. Returns subtitle URL in job response.
</ParamField>

<ParamField body="download_thumbnail" type="boolean" default="false">
  Download video thumbnail. Returns thumbnail URL in job response.

  <Warning>
    **Known gap (2026-07-08): currently only implemented for Spotify shows.** For YouTube -- the vast majority of requests -- setting this to `true` is accepted without error but does not currently produce a thumbnail; no `thumbnail_url`/`thumbnail_key` will appear in the job response or completion webhook. Tracked as a fix; not yet shipped. If you need YouTube thumbnails today, fetch them yourself from `https://i.ytimg.com/vi/<video_id>/maxresdefault.jpg`.
  </Warning>
</ParamField>

<ParamField body="quality_preset" type="string">
  Quality preset that overrides `video_quality`. Options: `highest`, `high`, `medium`, `low`, `lowest`.
</ParamField>

<ParamField body="max_resolution" type="string">
  Maximum video resolution. Options: `best` (default), `2160` (4K), `1440`, `1080`, `720`, `480`, `360`. Works correctly for both horizontal and vertical (Shorts) videos by checking the shorter dimension.
</ParamField>

<ParamField body="clip_start" type="string">
  Start timestamp for video clipping. Format: `HH:MM:SS`, `MM:SS`, or seconds (e.g., `00:01:30` or `90`).
</ParamField>

<ParamField body="clip_end" type="string">
  End timestamp for video clipping. Format: `HH:MM:SS`, `MM:SS`, or seconds (e.g., `00:05:00` or `300`). Must be greater than `clip_start`.
</ParamField>

<ParamField body="live_recording" type="boolean" default="false">
  Enable live stream recording mode. Auto-detected for live URLs.
</ParamField>

<ParamField body="live_from_start" type="boolean" default="false">
  For live streams: record from the beginning (VOD mode) instead of the live point.
</ParamField>

<ParamField body="max_duration" type="integer">
  Maximum recording duration in seconds. Recommended for live streams as a safety cap. Example: `7200` for 2 hours.
</ParamField>

<ParamField body="wait_for_video" type="boolean" default="false">
  Wait for scheduled/upcoming streams to start before downloading.
</ParamField>

<ParamField body="enable_progress_webhook" type="boolean" default="false">
  Enable progress webhooks during processing. Sends updates at each stage: `downloading`, `muxing`, `uploading`.
</ParamField>

<ParamField body="storage" type="object">
  Inline storage credentials. **Required** for marketplace users (RapidAPI, Apify, Zyla). Optional for direct API users (overrides pre-configured storage). Supports 4 providers via the `provider` field. See [Inline Storage examples](#inline-storage) below.
</ParamField>

<ParamField body="paused" type="boolean" default="false">
  For Spotify show batches only. Creates the batch in paused mode: jobs are **not** enqueued for processing immediately. Use `PATCH /batch/{id}/jobs` to rename episodes, then `POST /batch/{id}/start` to launch.
</ParamField>

## Single Job Response

<ResponseField name="job_id" type="string">
  UUID of the created job
</ResponseField>

```json Response theme={null}
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

## Batch Response (Spotify Shows)

When the URL is a Spotify show, a batch is created automatically:

<ResponseField name="batch_id" type="string">
  UUID of the batch job
</ResponseField>

<ResponseField name="total_episodes" type="integer">
  Number of episodes in the show
</ResponseField>

<ResponseField name="paused" type="boolean">
  Whether the batch was created in paused mode
</ResponseField>

<ResponseField name="episodes" type="array">
  List of episode details with job IDs, URLs, titles, descriptions, and release dates
</ResponseField>

<ResponseField name="episode_jobs" type="array">
  List of job IDs for each episode (legacy field)
</ResponseField>

```json Response (default mode) theme={null}
{
  "batch_id": "550e8400-e29b-41d4-a716-446655440001",
  "total_episodes": 142,
  "paused": false,
  "episodes": [
    {
      "job_id": "uuid-1",
      "url": "https://open.spotify.com/episode/abc",
      "title": "Episode 1 - Introduction",
      "description": "In this episode we cover...",
      "release_date": "2024-01-15"
    },
    {
      "job_id": "uuid-2",
      "url": "https://open.spotify.com/episode/def",
      "title": "Episode 2 - Deep Dive",
      "description": "A deep dive into...",
      "release_date": "2024-01-22"
    }
  ],
  "episode_jobs": ["uuid-1", "uuid-2"]
}
```

```json Response (paused mode) theme={null}
{
  "batch_id": "550e8400-e29b-41d4-a716-446655440001",
  "total_episodes": 142,
  "paused": true,
  "episodes": [
    {
      "job_id": "uuid-1",
      "url": "https://open.spotify.com/episode/abc",
      "title": "Episode 1 - Introduction"
    },
    {
      "job_id": "uuid-2",
      "url": "https://open.spotify.com/episode/def",
      "title": "Episode 2 - Deep Dive"
    }
  ],
  "episode_jobs": ["uuid-1", "uuid-2"]
}
```

## Examples

<CodeGroup>
  ```bash Single Video theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "format": "mp4",
      "filename": "my-video"
    }'
  ```

  ```bash With Webhook theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "webhook_url": "https://myapp.com/webhook"
    }'
  ```

  ```bash Custom Encoding theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "format": "mkv",
      "video_codec": "h265",
      "audio_codec": "opus",
      "audio_bitrate": "256k",
      "video_quality": 20
    }'
  ```

  ```bash Spotify Show (Batch) theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://open.spotify.com/show/7iQXmUT7XGuZSzAMjoNWlX",
      "folder": "huberman-lab-2024"
    }'
  ```

  ```bash Audio Only (m4a, default) theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "audio_only": true
    }'
  ```

  ```bash Audio Only (mp3) theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "audio_only": true,
      "format": "mp3",
      "audio_bitrate": "320k"
    }'
  ```

  ```bash With Subtitles & Thumbnail theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "download_subtitles": true,
      "download_thumbnail": true
    }'
  ```

  ```bash Quality Preset theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "quality_preset": "high"
    }'
  ```

  ```bash Resolution Selection (720p) theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "max_resolution": "720"
    }'
  ```

  ```bash Video Clipping theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "clip_start": "00:01:30",
      "clip_end": "00:03:00"
    }'
  ```

  ```bash Live Stream Recording theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/live/abc123",
      "live_recording": true,
      "live_from_start": true,
      "max_duration": 3600
    }'
  ```

  ```bash With Progress Webhook theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "webhook_url": "https://myapp.com/webhook",
      "enable_progress_webhook": true
    }'
  ```

  ```bash Inline Storage (S3) theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "storage": {
        "provider": "s3",
        "endpoint": "https://s3.us-east-1.amazonaws.com",
        "bucket": "my-videos",
        "region": "us-east-1",
        "access_key": "AKIAIOSFODNN7EXAMPLE",
        "secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        "folder_prefix": "downloads/",
        "base_folder": "videos"
      }
    }'
  ```

  ```bash Inline Storage (Azure Blob) theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "storage": {
        "provider": "blob",
        "account_name": "mystorageaccount",
        "container": "tornado-downloads",
        "account_key": "your-storage-account-key-base64==",
        "folder_prefix": "downloads/"
      }
    }'
  ```

  ```bash Inline Storage (GCS) theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "storage": {
        "provider": "gcs",
        "project_id": "my-gcp-project",
        "bucket": "tornado-downloads",
        "service_account_json": "{\"type\":\"service_account\",\"project_id\":\"my-gcp-project\",...}",
        "folder_prefix": "downloads/"
      }
    }'
  ```

  ```bash Inline Storage (Alibaba OSS) theme={null}
  curl -X POST "https://api.tornadoapi.io/jobs" \
    -H "x-api-key: sk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "storage": {
        "provider": "oss",
        "endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
        "bucket": "tornado-downloads",
        "access_key_id": "your-oss-access-key-id",
        "access_key_secret": "your-oss-access-key-secret",
        "folder_prefix": "downloads/"
      }
    }'
  ```
</CodeGroup>

## Inline Storage

The `storage` field lets you provide cloud storage credentials directly in the request. This is **required** for marketplace users and optional for direct API users.

<Info>
  Inline storage credentials take priority over pre-configured storage, are validated before the job is accepted, and are **never stored** or logged.
</Info>

### Provider Fields

<Tabs>
  <Tab title="S3 / S3-Compatible">
    | Field           | Type     | Required | Description                                                                                              |
    | --------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------- |
    | `provider`      | `string` | Yes      | Must be `"s3"`                                                                                           |
    | `endpoint`      | `string` | Yes      | S3 endpoint URL (e.g., `https://s3.us-east-1.amazonaws.com`, `https://ACCOUNT.r2.cloudflarestorage.com`) |
    | `bucket`        | `string` | Yes      | Bucket name                                                                                              |
    | `region`        | `string` | Yes      | AWS region (e.g., `us-east-1`) or `auto` for R2                                                          |
    | `access_key`    | `string` | Yes      | Access key ID                                                                                            |
    | `secret_key`    | `string` | Yes      | Secret access key                                                                                        |
    | `folder_prefix` | `string` | No       | Folder prefix (e.g., `downloads/2024/`)                                                                  |
    | `base_folder`   | `string` | No       | Top-level folder (default: `videos`)                                                                     |
  </Tab>

  <Tab title="Azure Blob">
    | Field           | Type     | Required | Description                          |
    | --------------- | -------- | -------- | ------------------------------------ |
    | `provider`      | `string` | Yes      | Must be `"blob"`                     |
    | `account_name`  | `string` | Yes      | Azure Storage account name           |
    | `container`     | `string` | Yes      | Container name                       |
    | `account_key`   | `string` | No\*     | Account key (Base64)                 |
    | `sas_token`     | `string` | No\*     | SAS token                            |
    | `folder_prefix` | `string` | No       | Folder prefix                        |
    | `base_folder`   | `string` | No       | Top-level folder (default: `videos`) |

    \*Provide either `account_key` or `sas_token`, not both.
  </Tab>

  <Tab title="Google Cloud Storage">
    | Field                  | Type     | Required | Description                          |
    | ---------------------- | -------- | -------- | ------------------------------------ |
    | `provider`             | `string` | Yes      | Must be `"gcs"`                      |
    | `project_id`           | `string` | Yes      | GCP project ID                       |
    | `bucket`               | `string` | Yes      | GCS bucket name                      |
    | `service_account_json` | `string` | Yes      | Full service account JSON as string  |
    | `folder_prefix`        | `string` | No       | Folder prefix                        |
    | `base_folder`          | `string` | No       | Top-level folder (default: `videos`) |
  </Tab>

  <Tab title="Alibaba OSS">
    | Field               | Type     | Required | Description                                                 |
    | ------------------- | -------- | -------- | ----------------------------------------------------------- |
    | `provider`          | `string` | Yes      | Must be `"oss"`                                             |
    | `endpoint`          | `string` | Yes      | OSS endpoint (e.g., `https://oss-cn-hangzhou.aliyuncs.com`) |
    | `bucket`            | `string` | Yes      | Bucket name                                                 |
    | `access_key_id`     | `string` | Yes      | Access key ID                                               |
    | `access_key_secret` | `string` | Yes      | Access key secret                                           |
    | `folder_prefix`     | `string` | No       | Folder prefix                                               |
    | `base_folder`       | `string` | No       | Top-level folder (default: `videos`)                        |
  </Tab>
</Tabs>

## Success Response

<ResponseExample>
  ```json 201 Created (Single Video) theme={null}
  {
    "job_id": "550e8400-e29b-41d4-a716-446655440000"
  }
  ```

  ```json 201 Created (Spotify Show Batch) theme={null}
  {
    "batch_id": "550e8400-e29b-41d4-a716-446655440001",
    "total_episodes": 142,
    "paused": false,
    "episodes": [
      {
        "job_id": "job-uuid-1",
        "url": "https://open.spotify.com/episode/abc",
        "title": "Episode 1 - Introduction"
      }
    ],
    "episode_jobs": ["job-uuid-1", "job-uuid-2", "job-uuid-3"]
  }
  ```

  ```json 201 Created (YouTube Playlist Batch) theme={null}
  {
    "batch_id": "550e8400-e29b-41d4-a716-446655440002",
    "total_videos": 25,
    "video_jobs": ["job-uuid-1", "job-uuid-2", "job-uuid-3"]
  }
  ```
</ResponseExample>

## Playlist Auto-Detection

`POST /jobs` inspects `url` and automatically creates a **batch** (one job per video) instead of a single job whenever it detects a genuine YouTube playlist. There is no separate "batch" endpoint or flag to opt into this -- it happens automatically based on the URL you send.

### Which URLs trigger it

| `url` contains...      | Example                                                                     | Behavior                                                                     |
| ---------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `list=PL...`           | `youtube.com/watch?v=xxx&list=PLxxxx` or `youtube.com/playlist?list=PLxxxx` | ✅ **Downloads the whole playlist** (this is a real, user-created playlist)   |
| `list=UU...`           | `...&list=UUxxxx`                                                           | ✅ **Downloads the whole playlist** (a channel's uploads)                     |
| `list=OL...`           | `...&list=OLxxxx`                                                           | ✅ **Downloads the whole playlist** (an "Online courses"-style auto playlist) |
| `list=RD...`           | `...&list=RDxxxx&start_radio=1`                                             | ❌ Downloads **only** the single video in `v=`                                |
| `list=WL` or `list=LL` | `...&list=WL`                                                               | ❌ Downloads **only** the single video in `v=`                                |
| no `list=` at all      | `youtube.com/watch?v=xxx`                                                   | ❌ Downloads **only** that one video                                          |

<Warning>
  **This is the #1 source of "I asked for one video and got thousands of jobs" confusion.** YouTube's own UI automatically appends `&list=RD<video_id>&start_radio=1` to the URL bar any time you open a video from an autoplay queue, the homepage, related videos, or a "Radio"/Mix button -- **not** because you're viewing a playlist. If you're integrating by copy-pasting URLs from a browser, always double-check for a `list=` parameter before sending it here. As of 2026-07-08, `list=RD...`/`WL`/`LL` URLs are correctly treated as single-video requests (this was previously a real bug that caused two production incidents) -- but if you're on an older integration or unsure, strip any `list=`/`start_radio=` query parameters from the URL yourself before calling this endpoint to be safe.
</Warning>

### Size limit

Even a genuine playlist (`PL`/`UU`/`OL`) is capped at **500 videos** per request (configurable server-side via `MAX_PLAYLIST_BATCH_SIZE`). A playlist larger than that is rejected outright with a `413 Payload Too Large` -- it is never silently truncated. If you need to download a larger playlist, split it into smaller batches or contact support.

### Response

<ResponseField name="batch_id" type="string">
  UUID of the batch job
</ResponseField>

<ResponseField name="total_videos" type="integer">
  Number of videos in the playlist
</ResponseField>

<ResponseField name="video_jobs" type="array">
  List of job IDs for each video
</ResponseField>

```bash Genuine Playlist (downloads all videos) theme={null}
curl -X POST "https://api.tornadoapi.io/jobs" \
  -H "x-api-key: sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf"
  }'
```

```json Response theme={null}
{
  "batch_id": "550e8400-e29b-41d4-a716-446655440002",
  "total_videos": 25,
  "video_jobs": ["job-uuid-1", "job-uuid-2", "job-uuid-3"]
}
```

```bash Radio/Mix URL (downloads only the one video) theme={null}
curl -X POST "https://api.tornadoapi.io/jobs" \
  -H "x-api-key: sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.youtube.com/watch?v=LVCpDa6ymtQ&list=RDLVCpDa6ymtQ&start_radio=1"
  }'
```

```json Response theme={null}
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

```json 413 Payload Too Large (playlist exceeds the cap) theme={null}
{
  "error": "This playlist has 2848 videos, which exceeds the 500-video limit for automatic playlist expansion in a single request.",
  "hint": "Split the playlist into smaller batches (e.g. multiple playlist URLs, or the batch API), or contact support@velys.software if you need a one-off exception.",
  "playlist_video_count": 2848,
  "max_playlist_batch_size": 500
}
```

## Error Responses

<ResponseExample>
  ```json 400 Bad Request theme={null}
  {
    "error": "Folder name too long (max 200 characters)"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "Invalid folder name: path traversal not allowed"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "Folder name cannot be empty or whitespace only"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "Invalid video URL"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "Invalid webhook URL"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "Invalid resolution '4k'. Valid options: [\"best\", \"2160\", \"1440\", \"1080\", \"720\", \"480\", \"360\"]"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "Invalid clip_start format '90x'. Use HH:MM:SS or seconds"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "clip_end must be greater than clip_start"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "No episodes found in this show"
  }
  ```

  ```json 401 Unauthorized theme={null}
  {
    "error": "Missing x-api-key header"
  }
  ```

  ```json 401 Unauthorized theme={null}
  {
    "error": "Invalid API Key"
  }
  ```

  ```json 403 Forbidden theme={null}
  {
    "error": "Storage quota exceeded",
    "limit_gb": "1024.00",
    "used_gb": "1024.00",
    "message": "This API key has a 1024 GB limit and has used 1024.00 GB"
  }
  ```

  ```json 403 Forbidden theme={null}
  {
    "error": "IP address not allowed for this API key"
  }
  ```

  ```json 429 Too Many Requests theme={null}
  {
    "error": "Server is at capacity, please retry later",
    "queue_depth": 5000,
    "retry_after": 30
  }
  ```

  ```json 503 Service Unavailable theme={null}
  {
    "error": "Service is draining, try another node",
    "retry_after": 30
  }
  ```

  ```json 504 Gateway Timeout theme={null}
  {
    "error": "Spotify show extraction timed out. The show may have too many episodes."
  }
  ```
</ResponseExample>

## Notes

<Note>
  The `429 Too Many Requests` and `503 Service Unavailable` responses include a `Retry-After` header indicating how many seconds to wait before retrying.
</Note>

<Note>
  **Codec auto-correction**: Incompatible codec/format combinations are automatically corrected to ensure valid output:

  * `webm` + `h264` → video codec changed to `vp9`
  * `webm` + `aac` → audio codec changed to `opus`
  * `ogg`/`opus` + `aac` → audio codec changed to `opus`
  * `mp3` + `aac` → audio codec changed to `mp3`
  * Setting `audio_bitrate` without `audio_codec` → audio codec defaults to `aac`

  Invalid `format` values are silently replaced with `mp4`.
</Note>
