Download + Process Video (yt-dlp)
API reference for downloading a public video via yt-dlp and optionally post-processing it with an FFmpeg command in the same async job.
Download + Process Video (yt-dlp)
POST /api/v1/run-ytdlp-commandDownload one or more publicly accessible videos via yt-dlp, then optionally pipe the output through an FFmpeg command. Use this when you need to transcode, trim, extract audio, or otherwise process the video immediately after downloading.
If you only need the raw download with no post-processing, use the simpler POST /api/v1/ytdlp-download endpoint instead.
Only public content is supported — no DRM bypass, no private accounts, no paywalled content.
Authentication
Requires API key via X-API-KEY header.
Video downloads are available during trials and on the Growth plan or higher.
Request
Headers
| Header | Type | Required | Description |
|---|---|---|---|
Content-Type | string | Yes | Must be application/json |
X-API-KEY | string | Yes | Your API key with ffsk_ prefix |
Body
interface RunYtDlpCommandRequest {
input_urls: Record<string, string>; // in_* aliases mapped to public video URLs
ffmpeg_command?: string; // FFmpeg command with {{alias}} placeholders
output_files?: Record<string, string>; // out_* aliases mapped to output filenames (required if ffmpeg_command set)
metadata?: Record<string, string | number | boolean>; // Max 10 keys
webhook_url?: string; // Per-command webhook override
storage_destination_id?: string; // Business: send outputs to your S3-compatible bucket
}| Field | Type | Required | Description |
|---|---|---|---|
input_urls | Record<string, string> | Yes | Map of alias names (must start with in_) to public video URLs. |
ffmpeg_command | string | No | FFmpeg command to run after download. Use {{alias}} placeholders to reference input_urls keys and output_files keys. If omitted, behaves like a simple download. |
output_files | Record<string, string> | Conditional | Map of alias names (must start with out_) to output filenames. Required when ffmpeg_command is provided. |
metadata | Record<string, string | number | boolean> | No | Arbitrary key-value metadata. Maximum 10 keys. |
webhook_url | string | No | URL to receive a POST when this command completes. Overrides any account-level webhook configuration for this command. Requires the Business plan. |
storage_destination_id | string | No | Send outputs to this S3-compatible storage destination. Requires the Business plan. |
Placeholder syntax
Use {{double_braces}} in ffmpeg_command to reference files:
{
"input_urls": { "in_video": "https://www.tiktok.com/@user/video/123" },
"ffmpeg_command": "-i {{in_video}} -vn -acodec libmp3lame -ab 192k {{out_audio}}",
"output_files": { "out_audio": "audio.mp3" }
}{
"input_urls": { "in_video": "https://www.tiktok.com/@user/video/123" },
"ffmpeg_command": "-i <<in_video>> -vn -acodec libmp3lame -ab 192k <<out_audio>>",
"output_files": { "out_audio": "audio.mp3" }
}Response
200 OK
{
command_id: string;
}| Field | Type | Description |
|---|---|---|
command_id | string | Unique identifier for the command. Use this to poll for status. |
Getting the output file
Poll GET /api/v1/commands/:commandId until status is SUCCESS.
- With
ffmpeg_command: file metadata is atresult.output_files.<your_out_key>. - Without
ffmpeg_command: file metadata is atresult.output_files.out_<suffix>, where<suffix>matches yourin_<suffix>input key.
For managed storage or a public BYOB destination, use storage_url. For a private BYOB destination, storage_url is null; use external_uri or external_object_key with your own S3 client. The persisted command result can be retrieved later without rerunning the job. See Bring Your Own Bucket.
Partial failures (download-only batches)
When you submit multiple input_urls without ffmpeg_command, each URL downloads independently:
- The command finishes with
status: "SUCCESS"when at least one URL stored successfully. - URLs that could not be downloaded are reported in a
failed_filesmap, keyed by theirin_*alias. Each entry carries the sourceurl, anerror_statuscode, and a human-readableerror_message. - The command only becomes
FAILEDwhen every URL fails.
{
"command_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "SUCCESS",
"output_files": {
"out_tiktok_1": { "storage_url": "https://media.renderio.dev/output/abc.mp4", "status": "STORED" }
},
"failed_files": {
"in_tiktok_2": {
"url": "https://www.tiktok.com/@user/video/7123456790",
"error_status": "SOURCE_LOGIN_REQUIRED",
"error_message": "This post is only visible to logged-in users"
}
}
}Always check for failed_files on SUCCESS responses when you submit batches. See Error Handling for the full list of error_status codes.
Commands with an ffmpeg_command remain all-or-nothing: every download must succeed for the FFmpeg step to run, so any failed URL fails the whole command.
Submit-time URL rejection
Obviously unavailable YouTube and TikTok URLs (removed videos, malformed IDs) are rejected at submit time with a 422 response that lists every bad URL, so you never burn a command on a dead link. These submit-time checks are being extended to cover TikTok login-gated and removed posts.
Error responses
| Status | Error | Description |
|---|---|---|
401 | UNAUTHORIZED | Missing or invalid API key. |
403 | FORBIDDEN | Video downloads are unavailable on the current plan, or a Business-only webhook or storage destination was requested by an ineligible account. |
404 | NOT_FOUND | The supplied storage destination does not exist or belongs to another account. |
409 | CONFLICT | The supplied storage destination is disabled. |
422 | VALIDATION_ERROR | Missing input_urls, invalid key prefixes or URLs, a malformed storage_destination_id, missing output definitions, placeholder mismatch, or excessive metadata. |
429 | RATE_LIMITED | Too many requests. Retry after the period indicated in the Retry-After header. |
501 | NOT_IMPLEMENTED | yt-dlp backend unavailable. Retry after a short delay. |
Examples
# Download a YouTube video and extract audio as MP3
curl -X POST https://renderio.dev/api/v1/run-ytdlp-command \
-H "Content-Type: application/json" \
-H "X-API-KEY: ffsk_your_api_key_here" \
-d '{
"input_urls": {
"in_video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
},
"ffmpeg_command": "-i {{in_video}} -vn -acodec libmp3lame -ab 192k {{out_audio}}",
"output_files": {
"out_audio": "audio.mp3"
}
}'# Download a YouTube video and extract audio as MP3
curl -X POST https://renderio.dev/api/v1/run-ytdlp-command \
-H "Content-Type: application/json" \
-H "X-API-KEY: ffsk_your_api_key_here" \
-d '{
"input_urls": {
"in_video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
},
"ffmpeg_command": "-i <<in_video>> -vn -acodec libmp3lame -ab 192k <<out_audio>>",
"output_files": {
"out_audio": "audio.mp3"
}
}'import os, time, requests
API_KEY = os.environ["RENDERIO_API_KEY"]
BASE = "https://renderio.dev"
# Download TikTok and resize to 9:16 portrait
res = requests.post(
f"{BASE}/api/v1/run-ytdlp-command",
headers={"X-API-KEY": API_KEY, "Content-Type": "application/json"},
json={
"input_urls": {"in_video": "https://www.tiktok.com/@user/video/7123456789"},
"ffmpeg_command": (
"-i {{in_video}} "
"-vf scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:-1:-1 "
"{{out_reel}}"
),
"output_files": {"out_reel": "reel-9x16.mp4"},
},
)
res.raise_for_status()
command_id = res.json()["command_id"]
# Poll until done
while True:
time.sleep(2)
result = requests.get(
f"{BASE}/api/v1/commands/{command_id}",
headers={"X-API-KEY": API_KEY},
).json()
if result["status"] == "SUCCESS":
output = result["output_files"]["out_reel"]
location = (
output.get("storage_url")
or output.get("external_uri")
or output.get("external_object_key")
)
print(location)
break
if result["status"] == "FAILED":
raise RuntimeError(result.get("error_message"))import os, time, requests
API_KEY = os.environ["RENDERIO_API_KEY"]
BASE = "https://renderio.dev"
# Download TikTok and resize to 9:16 portrait
res = requests.post(
f"{BASE}/api/v1/run-ytdlp-command",
headers={"X-API-KEY": API_KEY, "Content-Type": "application/json"},
json={
"input_urls": {"in_video": "https://www.tiktok.com/@user/video/7123456789"},
"ffmpeg_command": (
"-i {{in_video}} "
"-vf scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:-1:-1 "
"{{out_reel}}"
),
"output_files": {"out_reel": "reel-9x16.mp4"},
},
)
res.raise_for_status()
command_id = res.json()["command_id"]
# Poll until done
while True:
time.sleep(2)
result = requests.get(
f"{BASE}/api/v1/commands/{command_id}",
headers={"X-API-KEY": API_KEY},
).json()
if result["status"] == "SUCCESS":
output = result["output_files"]["out_reel"]
location = (
output.get("storage_url")
or output.get("external_uri")
or output.get("external_object_key")
)
print(location)
break
if result["status"] == "FAILED":
raise RuntimeError(result.get("error_message"))const API_KEY = process.env.RENDERIO_API_KEY!;
const BASE = "https://renderio.dev";
interface CommandResult {
status: "QUEUED" | "PROCESSING" | "SUCCESS" | "FAILED";
output_files: Record<string, {
storage_url: string | null;
storage_location?: "INTERNAL" | "EXTERNAL";
external_uri?: string;
external_object_key?: string;
}>;
error_message?: string;
}
async function downloadAndProcess(videoUrl: string): Promise<string> {
const submitRes = await fetch(`${BASE}/api/v1/run-ytdlp-command`, {
method: "POST",
headers: { "X-API-KEY": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
input_urls: { in_video: videoUrl },
ffmpeg_command: "-i {{in_video}} -vn -acodec libmp3lame -ab 192k {{out_audio}}",
output_files: { out_audio: "audio.mp3" },
}),
});
if (!submitRes.ok) throw new Error(`Submit failed: ${await submitRes.text()}`);
const { command_id } = await submitRes.json();
while (true) {
await new Promise((r) => setTimeout(r, 2000));
const pollRes = await fetch(`${BASE}/api/v1/commands/${command_id}`, {
headers: { "X-API-KEY": API_KEY },
});
const result: CommandResult = await pollRes.json();
if (result.status === "SUCCESS") {
const output = result.output_files.out_audio;
const location = output.storage_url ?? output.external_uri ?? output.external_object_key;
if (!location) throw new Error("Completed output has no storage location");
return location;
}
if (result.status === "FAILED") throw new Error(result.error_message);
}
}
const location = await downloadAndProcess("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
console.log("MP3 output:", location);const API_KEY = process.env.RENDERIO_API_KEY!;
const BASE = "https://renderio.dev";
interface CommandResult {
status: "QUEUED" | "PROCESSING" | "SUCCESS" | "FAILED";
output_files: Record<string, {
storage_url: string | null;
storage_location?: "INTERNAL" | "EXTERNAL";
external_uri?: string;
external_object_key?: string;
}>;
error_message?: string;
}
async function downloadAndProcess(videoUrl: string): Promise<string> {
const submitRes = await fetch(`${BASE}/api/v1/run-ytdlp-command`, {
method: "POST",
headers: { "X-API-KEY": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
input_urls: { in_video: videoUrl },
ffmpeg_command: "-i <<in_video>> -vn -acodec libmp3lame -ab 192k <<out_audio>>",
output_files: { out_audio: "audio.mp3" },
}),
});
if (!submitRes.ok) throw new Error(`Submit failed: ${await submitRes.text()}`);
const { command_id } = await submitRes.json();
while (true) {
await new Promise((r) => setTimeout(r, 2000));
const pollRes = await fetch(`${BASE}/api/v1/commands/${command_id}`, {
headers: { "X-API-KEY": API_KEY },
});
const result: CommandResult = await pollRes.json();
if (result.status === "SUCCESS") {
const output = result.output_files.out_audio;
const location = output.storage_url ?? output.external_uri ?? output.external_object_key;
if (!location) throw new Error("Completed output has no storage location");
return location;
}
if (result.status === "FAILED") throw new Error(result.error_message);
}
}
const location = await downloadAndProcess("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
console.log("MP3 output:", location);const API_KEY = process.env.RENDERIO_API_KEY;
const BASE = "https://renderio.dev";
async function downloadAndProcess(videoUrl) {
const submitRes = await fetch(`${BASE}/api/v1/run-ytdlp-command`, {
method: "POST",
headers: { "X-API-KEY": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
input_urls: { in_video: videoUrl },
ffmpeg_command: "-i {{in_video}} -vn -acodec libmp3lame -ab 192k {{out_audio}}",
output_files: { out_audio: "audio.mp3" },
}),
});
const { command_id } = await submitRes.json();
while (true) {
await new Promise((r) => setTimeout(r, 2000));
const result = await fetch(`${BASE}/api/v1/commands/${command_id}`, {
headers: { "X-API-KEY": API_KEY },
}).then((r) => r.json());
if (result.status === "SUCCESS") {
const output = result.output_files.out_audio;
const location = output.storage_url ?? output.external_uri ?? output.external_object_key;
if (!location) throw new Error("Completed output has no storage location");
return location;
}
if (result.status === "FAILED") throw new Error(result.error_message);
}
}
const location = await downloadAndProcess("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
console.log("MP3 output:", location);const API_KEY = process.env.RENDERIO_API_KEY;
const BASE = "https://renderio.dev";
async function downloadAndProcess(videoUrl) {
const submitRes = await fetch(`${BASE}/api/v1/run-ytdlp-command`, {
method: "POST",
headers: { "X-API-KEY": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
input_urls: { in_video: videoUrl },
ffmpeg_command: "-i <<in_video>> -vn -acodec libmp3lame -ab 192k <<out_audio>>",
output_files: { out_audio: "audio.mp3" },
}),
});
const { command_id } = await submitRes.json();
while (true) {
await new Promise((r) => setTimeout(r, 2000));
const result = await fetch(`${BASE}/api/v1/commands/${command_id}`, {
headers: { "X-API-KEY": API_KEY },
}).then((r) => r.json());
if (result.status === "SUCCESS") {
const output = result.output_files.out_audio;
const location = output.storage_url ?? output.external_uri ?? output.external_object_key;
if (!location) throw new Error("Completed output has no storage location");
return location;
}
if (result.status === "FAILED") throw new Error(result.error_message);
}
}
const location = await downloadAndProcess("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
console.log("MP3 output:", location);Common recipes
Download only (no post-processing)
Omit ffmpeg_command and output_files — the endpoint downloads and stores the video as-is:
{
"input_urls": { "in_video": "https://www.instagram.com/reel/..." }
}Output metadata: result.output_files.out_video. Use storage_url for managed/public storage or external_uri and external_object_key for private BYOB storage.
Extract audio as MP3
{
"input_urls": { "in_video": "https://www.youtube.com/watch?v=..." },
"ffmpeg_command": "-i {{in_video}} -vn -acodec libmp3lame -ab 192k {{out_audio}}",
"output_files": { "out_audio": "audio.mp3" }
}{
"input_urls": { "in_video": "https://www.youtube.com/watch?v=..." },
"ffmpeg_command": "-i <<in_video>> -vn -acodec libmp3lame -ab 192k <<out_audio>>",
"output_files": { "out_audio": "audio.mp3" }
}Resize to vertical 9:16
{
"input_urls": { "in_video": "https://www.tiktok.com/@user/video/..." },
"ffmpeg_command": "-i {{in_video}} -vf scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:-1:-1 {{out_reel}}",
"output_files": { "out_reel": "reel-9x16.mp4" }
}{
"input_urls": { "in_video": "https://www.tiktok.com/@user/video/..." },
"ffmpeg_command": "-i <<in_video>> -vf scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:-1:-1 <<out_reel>>",
"output_files": { "out_reel": "reel-9x16.mp4" }
}Trim to first 30 seconds
{
"input_urls": { "in_video": "https://www.youtube.com/watch?v=..." },
"ffmpeg_command": "-i {{in_video}} -t 30 -c copy {{out_clip}}",
"output_files": { "out_clip": "clip.mp4" }
}{
"input_urls": { "in_video": "https://www.youtube.com/watch?v=..." },
"ffmpeg_command": "-i <<in_video>> -t 30 -c copy <<out_clip>>",
"output_files": { "out_clip": "clip.mp4" }
}Related docs
Download Video (yt-dlp)
API reference for downloading a publicly accessible video from YouTube, TikTok, Instagram, X/Twitter, Reddit, Vimeo, Twitch, Facebook, and other yt-dlp-supported platforms.
Run Chained Commands
Submit up to 10 FFmpeg commands that execute sequentially, with outputs from earlier steps available as inputs to later steps.