RenderIO
API referenceCommands

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-command

Download 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

HeaderTypeRequiredDescription
Content-TypestringYesMust be application/json
X-API-KEYstringYesYour 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
}
FieldTypeRequiredDescription
input_urlsRecord<string, string>YesMap of alias names (must start with in_) to public video URLs.
ffmpeg_commandstringNoFFmpeg 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_filesRecord<string, string>ConditionalMap of alias names (must start with out_) to output filenames. Required when ffmpeg_command is provided.
metadataRecord<string, string | number | boolean>NoArbitrary key-value metadata. Maximum 10 keys.
webhook_urlstringNoURL to receive a POST when this command completes. Overrides any account-level webhook configuration for this command. Requires the Business plan.
storage_destination_idstringNoSend 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;
}
FieldTypeDescription
command_idstringUnique 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 at result.output_files.<your_out_key>.
  • Without ffmpeg_command: file metadata is at result.output_files.out_<suffix>, where <suffix> matches your in_<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_files map, keyed by their in_* alias. Each entry carries the source url, an error_status code, and a human-readable error_message.
  • The command only becomes FAILED when 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

StatusErrorDescription
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENVideo downloads are unavailable on the current plan, or a Business-only webhook or storage destination was requested by an ineligible account.
404NOT_FOUNDThe supplied storage destination does not exist or belongs to another account.
409CONFLICTThe supplied storage destination is disabled.
422VALIDATION_ERRORMissing input_urls, invalid key prefixes or URLs, a malformed storage_destination_id, missing output definitions, placeholder mismatch, or excessive metadata.
429RATE_LIMITEDToo many requests. Retry after the period indicated in the Retry-After header.
501NOT_IMPLEMENTEDyt-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" }
}

On this page