API reference

Download YouTube videos
from your own code.

Two endpoints. Resolve a URL, stream the bytes, mux on your side. Plus subscription $1/month — no per-call billing, no per-GB billing, no surprises.

Overview#

The VidPickr API resolves a YouTube URL into downloadable video and audio tracks and proxies the raw bytes through our infrastructure. Two endpoints, no SDK required, no audio or video state stored on our side. You bring an API key, we hand back the streams.

Use cases that fit cleanly:

  • Batch-download YouTube content for offline editing pipelines
  • Build a download button into your own creator tool
  • Archive a channel or playlist programmatically
  • Pull audio tracks to feed local Whisper / transcription pipelines
  • Power a hobby script that doesn't need a CLI dependency on yt-dlp

Calls live under https://api.vidpickr.com/v1. Authentication is by X-API-Key header — keys are minted in your account dashboard and tied to a Plus subscription.

Hello, VidPickr

curl -H "X-API-Key: $VIDPICKR_API_KEY" \
  "https://api.vidpickr.com/v1/info?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"

Quickstart#

Three steps to your first download. Total time, including signing up, is under two minutes.

  1. 1

    Upgrade to Plus

    API access is a Plus-tier feature ($1/mo). Subscribe here if you haven't already.

  2. 2

    Mint an API key

    From your account dashboard, click Create key. Copy the secret — it's shown once.

  3. 3

    Make your first call

    Set VIDPICKR_API_KEY in your environment and run the snippet on the right. You'll get a fully muxed MP4 in your working directory.

Resolve + stream + mux

# 1. Set your key
export VIDPICKR_API_KEY="vpk_live_..."

# 2. Resolve the URL — capture the tokens we'll need next
INFO=$(curl -s -H "X-API-Key: $VIDPICKR_API_KEY" \
  "https://api.vidpickr.com/v1/info?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ")
V_TOKEN=$(echo "$INFO" | jq -r '.resolutions[] | select(.height==1080) | .video_only[0].download_token')
A_TOKEN=$(echo "$INFO" | jq -r '.audio_only | sort_by(-.bitrate) | .[0].download_token')

# 3. Stream both tracks in parallel, then mux with ffmpeg
curl -s -H "X-API-Key: $VIDPICKR_API_KEY" -o v.mp4 \
  "https://api.vidpickr.com/v1/stream?token=$V_TOKEN" &
curl -s -H "X-API-Key: $VIDPICKR_API_KEY" -o a.m4a \
  "https://api.vidpickr.com/v1/stream?token=$A_TOKEN" &
wait
ffmpeg -y -i v.mp4 -i a.m4a -c copy out.mp4 && rm v.mp4 a.m4a
echo "wrote out.mp4"

Authentication#

Every /v1/* request carries an X-API-Key header. The value is the secret shown at key creation time, prefixed with vpk_live_.

We store only the sha256 hash of the secret — the plaintext key is unrecoverable after creation. If you lose a key, revoke it and mint a new one; lost keys cannot be retrieved.

Treat keys like passwords. Never commit them to git, never send them in client-side JavaScript, never log them. A leaked key gives anyone full access to your quota.

A request with a missing, malformed, revoked, or wrong-tier key returns 401 with a JSON error body. See the Errors section for the shape.

Authenticated request

curl -H "X-API-Key: vpk_live_..." \
  "https://api.vidpickr.com/v1/info?url=..."

Rate limits#

Limits are enforced per user (not per key) so rotating keys doesn't bypass them. The Plus tier ceilings are abuse-protection levels — normal use never touches them.

EndpointPer minutePer hour
GET /v1/info1002 000
GET /v1/stream601 500
GET /v1/split_token1002 000
GET /v1/subtitle1002 000

When a limit is hit, the API returns 429 rate_limited with two response headers:

  • Retry-After — integer seconds until the next slot frees.
  • X-RateLimit-Remaining — slots left in the current minute window (always present, even on 200 responses).

Robust clients respect Retry-After and back off with jitter. Hammering at the limit boundary just keeps you 429'd.

Backoff on 429

async function call(url) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(url, { headers: { "X-API-Key": KEY } });
    if (res.status !== 429) return res;
    const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
    const jitter = Math.random() * 500;
    await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter));
  }
  throw new Error("rate-limited too many times");
}

Errors#

Every error response is JSON with a consistent shape so SDKs can parse uniformly. HTTP status reflects the nature; the code string is a stable identifier for branching logic.

CodeStatusMeaning
missing_api_key401No X-API-Key header on the request.
invalid_api_key401Key not recognized or has been revoked.
plus_required402Owner of this key does not have an active Plus subscription.
rate_limited429You hit the per-minute or per-hour ceiling.
bad_request400Required parameter missing or malformed.
upstream_failed502YouTube refused the resolution. Try again or check the URL.
lookup_failed500Our database or resolver had an internal error.

For client logic, key off code, not the human-readable message. The message text is for humans and may change between versions.

Branch on error.code

# An error response (HTTP 429)
{
  "error": {
    "code": "rate_limited",
    "message": "too many requests, retry in 14s"
  }
}

Versioning#

The version is in the URL path (/v1/). Breaking changes ship as a new version; we'll keep the previous version online for at least 12 months and announce sunset dates in the changelog with email notification.

Non-breaking additions — new fields on responses, new optional query parameters, additional format options — ship inside v1 without notice. Code defensively: tolerate unknown fields, don't assume a fixed set of resolutions.

Version is in the path

# v1 (current)
curl "https://api.vidpickr.com/v1/info?url=..."

# Future versions will live alongside:
# curl "https://api.vidpickr.com/api/v2/info?url=..."

Resolve URL#

GET/v1/info

Resolves a YouTube URL into the full list of available video and audio tracks. Response includes signed download_token values you pass to /v1/stream; tokens expire after roughly 10 minutes.

Request parameters

ParameterTypeRequiredDescription
urlstringyesA valid YouTube watch URL or shortlink (youtu.be).

Response fields

FieldTypeDescription
titlestringVideo title from the YouTube metadata.
duration_secnumberDuration in seconds.
platformstringAlways "youtube" for now.
thumbnailstringURL of the highest-res thumbnail.
resolutionsarrayOne entry per available height, with codec variants under video_only.
audio_onlyarrayStandalone audio tracks for MP3/M4A pipelines.
subtitlesarrayAvailable subtitle tracks (auto-generated and uploader-provided).

Resolutions are not exhaustive. A 4K or 8K video may not have every codec at every height — match on height and pick the codec you want from video_only.

GET /v1/info — response

$ curl -H "X-API-Key: vpk_live_..." \
    "https://api.vidpickr.com/v1/info?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"
{
  "title": "Never Gonna Give You Up",
  "duration_sec": 213,
  "platform": "youtube",
  "thumbnail": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/maxresdefault.webp",
  "resolutions": [
    {
      "height": 1080,
      "video_only": [
        {
          "ext": "mp4",
          "vcodec": "av01.0.08M.08",
          "size_mb": 28.4,
          "download_token": "v1.eyJWIjoidjEiLCJUIjoiYXYwMSIsIkUiOjE3..."
        },
        {
          "ext": "webm",
          "vcodec": "vp09.00.50.08",
          "size_mb": 35.1,
          "download_token": "v1.eyJWIjoidjEiLCJUIjoidnA5IiwiRSI6MTc..."
        }
      ]
    },
    { "height": 720, "video_only": [ /* ... */ ] },
    { "height": 480, "video_only": [ /* ... */ ] }
  ],
  "audio_only": [
    {
      "ext": "m4a",
      "acodec": "mp4a.40.2",
      "bitrate": 128,
      "size_mb": 3.2,
      "download_token": "a1.eyJWIjoiYTEiLCJUIjoiYWFjIiwiRSI6MTc..."
    },
    {
      "ext": "webm",
      "acodec": "opus",
      "bitrate": 160,
      "size_mb": 4.1,
      "download_token": "a1.eyJWIjoiYTEiLCJUIjoib3B1cyIsIkUiOjE3..."
    }
  ],
  "subtitles": [
    { "language": "en", "kind": "manual", "download_token": "s1..." },
    { "language": "tr", "kind": "auto",   "download_token": "s1..." }
  ]
}

Stream track#

GET/v1/stream

Streams a single video or audio track. The response body is the raw bytes — set a streaming reader on your side and pipe to disk. Content-Length is set when YouTube reports it; otherwise the connection closes when the stream is complete.

Request parameters

ParameterTypeRequiredDescription
tokenstringyesA download_token from a recent /v1/info response. HMAC-signed and time-limited.

Response headers

HeaderDescription
Content-Typevideo/mp4, audio/mp4, etc. — matches the source codec.
Content-LengthTotal bytes, when YouTube reports it (most cases).
Content-Dispositionattachment; filename="..." — safe default for browser saves.
X-RateLimit-RemainingStream-quota slots left in the current minute.

Tokens are single-use-window, not single-use. You can retry a failed stream with the same token within its lifetime, but after ~10 minutes you'll need to re-resolve.

GET /v1/stream — pipe to disk

curl -H "X-API-Key: vpk_live_..." \
  -o video.mp4 \
  "https://api.vidpickr.com/v1/stream?token=v1.eyJWIjoidjEi..."

Split merge token#

GET/v1/split_token

Video formats in /v1/info return tokens that bundle both the video and audio source URLs into a single signed string (the response field marks them with endpoint: "merge"). To stream them, exchange that merge token here for a separate video-only token and audio-only token, then call /v1/stream twice in parallel.

The Node SDK does this transparently. You only need /v1/split_token when you're calling the raw API by hand (curl, untyped HTTP client, custom pipeline).

Request parameters

ParameterTypeRequiredDescription
tokenstringyesA merge download_token from /v1/info — i.e. a video resolution token where endpoint is "merge".

Response fields

FieldTypeDescription
video_tokenstringPass to /v1/stream to fetch the video-only track.
audio_tokenstringPass to /v1/stream to fetch the audio-only track. Mux the two with ffmpeg -c copy, or with the SDK.

GET /v1/split_token — exchange + stream both halves

# Resolve the URL and grab the 1080p merge token
INFO=$(curl -s -H "X-API-Key: $VIDPICKR_API_KEY" \
  "https://api.vidpickr.com/v1/info?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ")
MERGE=$(echo "$INFO" | jq -r '.resolutions[] | select(.height==1080) | .download_token')

# Split into video-only + audio-only tokens
SPLIT=$(curl -s -H "X-API-Key: $VIDPICKR_API_KEY" \
  "https://api.vidpickr.com/v1/split_token?token=$MERGE")
V_TOK=$(echo "$SPLIT" | jq -r '.video_token')
A_TOK=$(echo "$SPLIT" | jq -r '.audio_token')

# Stream both, then mux with ffmpeg
curl -s -H "X-API-Key: $VIDPICKR_API_KEY" -o v.mp4 \
  "https://api.vidpickr.com/v1/stream?token=$V_TOK" &
curl -s -H "X-API-Key: $VIDPICKR_API_KEY" -o a.m4a \
  "https://api.vidpickr.com/v1/stream?token=$A_TOK" &
wait
ffmpeg -y -i v.mp4 -i a.m4a -c copy out.mp4

Download subtitles#

GET/v1/subtitle

Each subtitle track in /v1/info's subtitles[] array carries its own download_token. This endpoint serves the requested track in the requested format. Default is SRT; pass ?format=vtt or ?format=txt for the other formats.

Subtitle tokens are not stream tokens. Calling /v1/stream with a subtitle token returns token has no source because the token carries only a timedtext URL, not a media source.

Request parameters

ParameterTypeRequiredDescription
tokenstringyesA subtitle download_token from /v1/info subtitles[].
formatstringnosrt (default), vtt, or txt.

Response

The track in the chosen format, with Content-Disposition: attachment so browser-based callers save it directly. Body is plain text.

GET /v1/subtitle — captions in SRT / VTT / TXT

# Resolve, find the language track, download as SRT
INFO=$(curl -s -H "X-API-Key: $VIDPICKR_API_KEY" \
  "https://api.vidpickr.com/v1/info?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ")
TOKEN=$(echo "$INFO" | jq -r '.subtitles[] | select(.code=="en" and .is_auto==false) | .download_token')

# Default format is SRT
curl -s -H "X-API-Key: $VIDPICKR_API_KEY" \
  -o captions.srt \
  "https://api.vidpickr.com/v1/subtitle?token=$TOKEN"

# Or VTT / TXT
curl -s -H "X-API-Key: $VIDPICKR_API_KEY" \
  -o captions.vtt \
  "https://api.vidpickr.com/v1/subtitle?token=$TOKEN&format=vtt"

Muxing video + audio#

For resolutions above 720p, YouTube ships video and audio as separate tracks. After fetching both, merge them into a single MP4 with a stream-copy operation — no re-encoding, takes about a second per minute of video.

Three muxer choices, in order of preference

  1. Official SDK — bundles a native MP4 muxer transparently. One function call handles resolve + parallel stream + mux + write. npm install vidpickr is available now; Python and Go SDKs are in development.
  2. Language-native MP4 muxer — zero-binary libraries that handle the stream-copy yourself: mp4-muxer + mp4box in JS, mp4ff in Go, or a Rust extension shim for Python. Same pipeline the official SDK uses internally.
  3. ffmpeg — the universal fallback. One CLI invocation, works everywhere, but pulls an ~80 MB binary dependency you need to install separately.

Stream-copy mux

# Both files already downloaded via /v1/stream
ffmpeg -y -i video.mp4 -i audio.m4a -c copy out.mp4

# That's it. No re-encoding (the -c copy flag).
# Takes about a second per minute of source video.

Parallel downloads#

Fetching the video and audio tracks in parallel cuts wall-clock time roughly in half. Both tracks come from the same upstream, but our proxy multiplexes connections cleanly — the rate limit applies to fetch starts per minute, not concurrent connections.

On Python use concurrent.futures.ThreadPoolExecutor with two workers. On Node use Promise.all([fetchVideo(), fetchAudio()]). On Go fan two goroutines with a sync.WaitGroup or errgroup.Group.

Concurrency cap. A single client should keep concurrent stream fetches under 10. There's no hard limit yet but heavy concurrency from one user gets flagged for review.

Concurrent video + audio fetch

await Promise.all([
  pull(video.download_token, "v.mp4"),
  pull(audio.download_token, "a.m4a"),
]);

Best practices#

Six things that separate a robust client from one that breaks on the first edge case:

  1. Stream, don't buffer. A 4K video can be a gigabyte. Loading the whole response into memory will OOM on small machines. Pipe directly to disk.
  2. Resolve, then immediately stream. Tokens are time-limited. Don't batch resolutions days in advance — that list expires.
  3. Pick by height + codec, not order. Format order is not stable across requests. Iterate resolutions[] and match on the height + codec you want.
  4. Honor Retry-After. Back off with jitter on 429. Don't hot-loop the API.
  5. Treat 502 as retryable. Upstream YouTube hiccups happen — one immediate retry usually fixes it. Two consecutive 502s on the same URL is a real failure; surface it.
  6. Never log full keys. If you must log, mask: keep the vpk_live_ prefix + last 4 chars.

Robust download — full pattern

import os, time, random, requests
from contextlib import contextmanager

API_KEY = os.environ["VIDPICKR_API_KEY"]
BASE = "https://api.vidpickr.com/v1"

def masked_key(k):
    return k[:9] + "..." + k[-4:]   # vpk_live_...abcd

def call_with_backoff(method, url, **kw):
    """Retries on 429 with Retry-After + jitter; retries once on 502."""
    for attempt in range(5):
        r = requests.request(method, url, headers={"X-API-Key": API_KEY}, **kw)
        if r.status_code == 429:
            wait = int(r.headers.get("Retry-After", "5"))
            time.sleep(wait + random.random() * 0.5)
            continue
        if r.status_code == 502 and attempt == 0:
            time.sleep(2)
            continue
        return r
    r.raise_for_status()
    return r

def resolve(youtube_url):
    return call_with_backoff("GET", f"{BASE}/info",
                              params={"url": youtube_url}).json()

@contextmanager
def stream_to(path, token):
    r = call_with_backoff("GET", f"{BASE}/stream",
                          params={"token": token}, stream=True)
    r.raise_for_status()
    with open(path, "wb") as f:
        for chunk in r.iter_content(chunk_size=1 << 20):
            f.write(chunk)
    yield path

Official SDKs#

Each official SDK wraps the raw API with idiomatic client objects, handles parallel streaming, and bundles a native MP4 muxer — so you don't need ffmpeg installed.

Node.js

@vidpickr/sdkavailable

npm install @vidpickr/sdk. Pure JS muxer via mp4-muxer (no ffmpeg dependency), ESM + CJS exports, Node 18+ and Bun. Source: github.com/vidpickr/sdk-node.

Python

vidpickravailable

pip install vidpickr. Subprocess-ffmpeg by default (small wheel); install vidpickr[bundled-ffmpeg] to pull imageio-ffmpeg and skip the system dependency. Python 3.9+. Source: github.com/vidpickr/sdk-python.

Go

github.com/vidpickr/sdk-goavailable

go get github.com/vidpickr/sdk-go. Pure-Go MP4 muxer via mp4ff — zero runtime dependencies, single-binary install. CLI at cmd/vidpickr. Source: github.com/vidpickr/sdk-go.

Watch the changelog or follow @vidpickr on X for release announcements.

SDK quickstart

# Three SDKs published — npm @vidpickr/sdk, pip vidpickr,
# go get github.com/vidpickr/sdk-go. The curl + ffmpeg pattern in
# Quickstart still works if you'd rather skip the SDK.

Changelog#

Notable additions and bug fixes to the public API. We publish a new entry every time something user-visible changes; subscribe to the full changelog for the broader product history.

Initial release

  • GET /v1/info — resolve URLs into format options
  • GET /v1/stream — stream proxy for individual tracks
  • X-API-Key header authentication
  • Per-user rate limits (Plus tier)
  • Dashboard for key creation, listing, and revocation