yt-dlp API: Use yt-dlp from Python, Node.js or an HTTP Service (2026)
Does yt-dlp have an API? Yes, a Python one. How to use YoutubeDL for metadata and downloads, call yt-dlp from Node.js, wrap it in a REST API, and what it takes to run that in production.
Vid Kraken · Updated October 28, 2026
yt-dlp is a command-line tool, but you're writing code, and you want to call it like an API: pass a URL, get metadata or a file back.
Short version: yt-dlp has a Python API, the YoutubeDL class, which is what the command line runs on. From any other language, you run the command and read its JSON output. There is no official HTTP API. If you want "POST a URL, get a file", you either wrap yt-dlp in a service yourself or use a managed API that does the running for you.
| You want to | What exists |
|---|---|
| Call yt-dlp from Python | import yt_dlp and the YoutubeDL class (official) |
| Call it from Node.js, Go, PHP or anything else | Run the yt-dlp command, read JSON from -J or --print |
| Call it over HTTP | Nothing official. Write a small service, or use a managed API |
The Python API: YoutubeDL
Install it with the [default] extra, which includes the scripts yt-dlp needs to solve YouTube's JavaScript challenges. You also need Deno installed, as you do for the command line.
python3 -m pip install -U "yt-dlp[default]"
Get metadata without downloading
import yt_dlp
URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
with yt_dlp.YoutubeDL({"quiet": True}) as ydl:
info = ydl.sanitize_info(ydl.extract_info(URL, download=False))
print(info["title"], info["duration"], len(info["formats"]))
extract_info(..., download=False) returns the same dictionary yt-dlp -J prints: title, duration, uploader, thumbnails, and every available format. sanitize_info turns it into plain data you can pass to json.dumps.
Download a video or an MP3
import yt_dlp
def on_progress(d):
if d["status"] == "finished":
print("downloaded", d["filename"])
opts = {
"format": "bv*[height<=1080]+ba/b[height<=1080]",
"merge_output_format": "mp4",
"outtmpl": "%(id)s.%(ext)s",
"progress_hooks": [on_progress],
"quiet": True,
"noprogress": True,
}
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
print(info["requested_downloads"][0]["filepath"])import yt_dlp
opts = {
"format": "ba/b",
"outtmpl": "%(id)s.%(ext)s",
"postprocessors": [{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "320",
}],
"quiet": True,
"noprogress": True,
}
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
print(info["requested_downloads"][0]["filepath"])extract_info downloads by default. The final path, after merging or converting, is in info["requested_downloads"][0]["filepath"]. Merging and MP3 conversion need ffmpeg installed.
A few things that save time:
- Option names aren't the flag names.
--merge-output-formatbecomesmerge_output_format, but many don't map that neatly. yt-dlp'sdevscripts/cli_to_api.pyprints the options dictionary for any command line. - Errors raise. In the Python API, a failed download raises
yt_dlp.utils.DownloadError. The CLI would print the error and carry on. The error is a message string, not a code, so to tell "private video" from "blocked IP" you match on the text. yt-dlp not working lists the common messages. - Pick a JavaScript runtime in code. If you use Node instead of Deno, pass
"js_runtimes": {"node": {}}. - Use one
YoutubeDLper job. Don't share an instance across threads.
From Node.js (or any other language)
There's no official yt-dlp package for other languages. Run the command, ask it for JSON, and parse that. Never parse its normal log output, which is meant for people and changes between versions.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const run = promisify(execFile);
const url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
// Metadata: the same JSON as the Python API
const { stdout } = await run("yt-dlp", ["-J", "--no-warnings", "--", url], {
maxBuffer: 64 * 1024 * 1024,
});
const info = JSON.parse(stdout);
console.log(info.title, info.duration);
// Download, then print only the final file path
const { stdout: path } = await run("yt-dlp", [
"-f", "bv*[height<=1080]+ba/b[height<=1080]",
"--merge-output-format", "mp4",
"-o", "%(id)s.%(ext)s",
"--print", "after_move:filepath",
"--", url,
]);
console.log(path.trim());
Pass arguments as an array, as execFile does here, and never build a shell string from a URL a user gave you. The -- stops a URL that starts with - from being read as an option. --print after_move:filepath prints only the final path and still downloads; a plain --print would only simulate.
Wrapping yt-dlp in a REST API
The code for a basic HTTP service is short. Downloads take seconds to minutes, so the service starts a job and returns its ID straight away rather than holding the request open:
import uuid
import yt_dlp
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
jobs = {}
def run(job_id, url):
opts = {
"format": "bv*[height<=1080]+ba/b[height<=1080]",
"merge_output_format": "mp4",
"outtmpl": f"/data/{job_id}.%(ext)s",
"quiet": True,
"noprogress": True,
}
try:
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info(url)
jobs[job_id] = {"status": "COMPLETED", "file": info["requested_downloads"][0]["filepath"]}
except yt_dlp.utils.DownloadError as e:
jobs[job_id] = {"status": "FAILED", "error": str(e)}
@app.post("/download")
def download(url: str, tasks: BackgroundTasks):
job_id = uuid.uuid4().hex
jobs[job_id] = {"status": "IN_PROGRESS"}
tasks.add_task(run, job_id, url)
return {"jobId": job_id}
@app.get("/download/{job_id}")
def status(job_id: str):
return jobs.get(job_id, {"status": "NOT_FOUND"})
That works on your laptop. Here's what it's missing before real users can rely on it:
- A real job queue. The
jobsdictionary disappears on restart and lives in one process. You need a persistent queue, retries, and a limit on how many downloads run at once. - Storage and delivery. Files fill the disk. You need object storage, links your users can download from, and cleanup.
- An IP YouTube doesn't block. On cloud servers, YouTube answers with "Sign in to confirm you're not a bot" and 403 errors. Most teams end up paying for residential proxies.
- PO tokens and a JavaScript runtime on every worker, plus a PO Token provider when YouTube asks for one.
- Updates without outages. YouTube changes often, and fixes arrive in new yt-dlp releases. Update too slowly and downloads fail; update too eagerly and a bad release can break them.
- Error codes. Your users need "this video is private" or "not available in your country", not a yt-dlp traceback. You have to map messages to codes yourself.
- Input checks. yt-dlp downloads from thousands of sites, including arbitrary web pages. Accept only the URLs you mean to support.
None of these is hard alone. Together they make a service you have to keep running, and it needs attention every time YouTube changes something.
Or use a managed API
If downloading YouTube is a feature of your product rather than the product itself, a managed API gives you the HTTP endpoint without the list above. With Vid Kraken, you send a URL and a format and get back a link to the file on our CDN:
curl -X POST https://vidkraken.com/api/v2/download \
-H "Authorization: Bearer $VIDKRAKEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://youtu.be/dQw4w9WgXcQ", "format": "1080"}'npx vidkraken download "https://youtu.be/dQw4w9WgXcQ" -f 1080 -o ./ --jsonFormats are 1080, 720, 480, 360, audio-mp3 (320 kbps) and audio (the original stream). Bot detection, proxies and YouTube's changes are handled on our side. When a video can't be downloaded, you get a typed error code such as AGE_RESTRICTED or GEO_RESTRICTED, and failed downloads aren't charged. You pay for bandwidth only. The YouTube downloader API guide has the full walkthrough.
What you give up compared with yt-dlp:
- YouTube only. yt-dlp supports thousands of sites.
- Less metadata. yt-dlp's info dictionary has views, description, chapters and every format. Vid Kraken's metadata endpoint returns the title, duration, region restrictions and audio languages.
- A cost per GB instead of free software plus your own servers and proxies.
Try it without writing code
Paste a YouTube URL and get a direct download link.
Paste a link. See it work.
Paste a YouTube link, pick a format, and hit download.
Which should you use?
| Situation | Use |
|---|---|
| A script for yourself, a few videos | The yt-dlp command or the Python API. See how to use yt-dlp |
| A Python project running from a home connection | YoutubeDL directly |
| An internal tool where occasional failures are fine | Your own wrapper service, on a proxy |
| A product feature where users paste YouTube links | A managed API |
| Sites other than YouTube | yt-dlp |
More options in yt-dlp alternatives.
Skip building the service.
Send a YouTube URL, get a direct MP4 or MP3 link. No proxies, cookies or yt-dlp updates to maintain, and failed downloads are free.
Frequently asked questions
Does yt-dlp have an API?+
Yes, a Python one. Import yt_dlp and use the YoutubeDL class: extract_info(url, download=False) returns metadata, and extract_info(url) downloads. There is no official HTTP or REST API, and no official library for other languages.
Is there a REST API for yt-dlp?+
Not an official one. You can wrap yt-dlp in a small web service (FastAPI or Flask in Python), but in production that service also needs a job queue, storage, proxies for YouTube's bot checks, and regular yt-dlp updates. A managed API like Vid Kraken gives you the REST endpoint without running any of that.
How do I use yt-dlp from Node.js?+
Run the yt-dlp command with child_process.execFile, passing arguments as an array. Use -J for metadata as JSON, and --print after_move:filepath to get the downloaded file's path. Don't parse yt-dlp's normal log output.
How do I get the downloaded file path from the yt-dlp Python API?+
After info = ydl.extract_info(url), the final path (after merging or converting) is info['requested_downloads'][0]['filepath'].
Why does yt-dlp work on my laptop but fail on my server?+
YouTube treats cloud and datacenter IPs as likely bots, so the same command gets 'Sign in to confirm you're not a bot' or 403 errors on a server. The usual fixes are residential proxies and a PO Token provider, or a managed API that handles it for you.