Download and Transcode Videos in One API Call

May 22, 2026 · RenderIO

Why two jobs when one will do

The straightforward approach to downloading and processing a video is: download it first, then run FFmpeg on the file. Two API calls, two command IDs to track, a temporary file sitting somewhere between the two steps.

/api/v1/run-ytdlp-command does both in one request. You send a URL and an FFmpeg command; the API downloads the video, pipes it through FFmpeg, and gives you the processed output. One command_id to poll.

The request format

curl -X POST https://renderio.dev/api/v1/run-ytdlp-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_key" \
  -d '{
    "input_urls": {
      "in_video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    },
    "ffmpeg_command": "-i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}",
    "output_files": {
      "out_audio": "audio.mp3"
    }
  }'

A few things about the format:

  • input_urls keys must start with in_

  • ffmpeg_command uses {{double_braces}} for placeholders

  • output_files is required when ffmpeg_command is set — it maps each output key to a filename

If you leave out ffmpeg_command, it behaves like the plain /api/v1/ytdlp-download endpoint and returns the raw downloaded file.

Recipes

Extract audio as MP3

The most common use case — pull the audio track from a YouTube video:

curl -X POST https://renderio.dev/api/v1/run-ytdlp-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_key" \
  -d '{
    "input_urls": {
      "in_video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    },
    "ffmpeg_command": "-i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}",
    "output_files": {
      "out_audio": "audio.mp3"
    }
  }'

Crop to 9:16 for vertical video

Download a landscape YouTube video and crop it to center 9:16 at 1080×1920:

curl -X POST https://renderio.dev/api/v1/run-ytdlp-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_key" \
  -d '{
    "input_urls": {
      "in_video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    },
    "ffmpeg_command": "-i {{in_video}} -vf crop=ih*9/16:ih,scale=1080:1920 -c:v libx264 -crf 23 -c:a aac {{out_video}}",
    "output_files": {
      "out_video": "vertical.mp4"
    }
  }'

Trim to a specific segment

Extract 45 seconds starting at 1:30:

curl -X POST https://renderio.dev/api/v1/run-ytdlp-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_key" \
  -d '{
    "input_urls": {
      "in_video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    },
    "ffmpeg_command": "-i {{in_video}} -ss 00:01:30 -t 00:00:45 -c copy {{out_clip}}",
    "output_files": {
      "out_clip": "clip.mp4"
    }
  }'

-c copy skips re-encoding (fast, but cuts on keyframe boundaries). Drop it if you need frame-accurate cuts.

Resize and compress

Download a TikTok video and re-encode at 720p:

curl -X POST https://renderio.dev/api/v1/run-ytdlp-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_key" \
  -d '{
    "input_urls": {
      "in_video": "https://www.tiktok.com/@user/video/1234567890"
    },
    "ffmpeg_command": "-i {{in_video}} -vf scale=720:-2 -c:v libx264 -crf 26 -c:a aac -b:a 128k {{out_video}}",
    "output_files": {
      "out_video": "compressed_720p.mp4"
    }
  }'

scale=720:-2 sets width to 720 and picks a height divisible by 2 to keep the aspect ratio.

Thumbnail at a timestamp

Grab a frame at 30 seconds:

curl -X POST https://renderio.dev/api/v1/run-ytdlp-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_key" \
  -d '{
    "input_urls": {
      "in_video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    },
    "ffmpeg_command": "-i {{in_video}} -ss 00:00:30 -vframes 1 {{out_thumb}}",
    "output_files": {
      "out_thumb": "thumbnail.jpg"
    }
  }'

Polling

Same as any other RenderIO command:

curl https://renderio.dev/api/v1/commands/cmd_abc123 \
  -H "X-API-KEY: ffsk_your_key"

On success:

{
  "command_id": "cmd_abc123",
  "status": "SUCCESS",
  "output_files": {
    "out_audio": {
      "storage_url": "https://media.renderio.dev/downloads/cmd_abc123/out_audio.mp3",
      "size_mbytes": 8.3
    }
  }
}

Status values: QUEUED, PROCESSING, SUCCESS, FAILED.

Node.js

const API_KEY = process.env.RENDERIO_API_KEY;
const BASE_URL = "https://renderio.dev/api/v1";

async function downloadAndProcess(url, ffmpegCommand, outputFiles) {
  const res = await fetch(`${BASE_URL}/run-ytdlp-command`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-API-KEY": API_KEY },
    body: JSON.stringify({
      input_urls: { in_video: url },
      ffmpeg_command: ffmpegCommand,
      output_files: outputFiles,
    }),
  });

  const { command_id } = await res.json();

  while (true) {
    await new Promise((r) => setTimeout(r, 3000));

    const poll = await fetch(`${BASE_URL}/commands/${command_id}`, {
      headers: { "X-API-KEY": API_KEY },
    });

    const result = await poll.json();

    if (result.status === "SUCCESS") return result;
    if (result.status === "FAILED") throw new Error(result.error);
  }
}

// Extract audio from a YouTube video
const result = await downloadAndProcess(
  "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "-i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}",
  { out_audio: "audio.mp3" }
);

console.log(result.output_files.out_audio.storage_url);

Python

import os
import time
import requests

API_KEY = os.environ["RENDERIO_API_KEY"]
BASE_URL = "https://renderio.dev/api/v1"


def download_and_process(url: str, ffmpeg_command: str, output_files: dict) -> dict:
    res = requests.post(
        f"{BASE_URL}/run-ytdlp-command",
        headers={"Content-Type": "application/json", "X-API-KEY": API_KEY},
        json={
            "input_urls": {"in_video": url},
            "ffmpeg_command": ffmpeg_command,
            "output_files": output_files,
        },
    )
    res.raise_for_status()
    command_id = res.json()["command_id"]

    while True:
        time.sleep(3)
        poll = requests.get(
            f"{BASE_URL}/commands/{command_id}",
            headers={"X-API-KEY": API_KEY},
        )
        result = poll.json()
        if result["status"] == "SUCCESS":
            return result
        if result["status"] == "FAILED":
            raise RuntimeError(result.get("error", "unknown error"))


result = download_and_process(
    url="https://www.tiktok.com/@user/video/1234567890",
    ffmpeg_command="-i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}",
    output_files={"out_audio": "audio.mp3"},
)
print(result["output_files"]["out_audio"]["storage_url"])

When to use this vs. two separate calls

Use the combined endpoint when the processing step is fixed — you know before submitting that you'll always extract audio, or always crop to 9:16. It keeps everything in one job.

Use separate calls when the processing step depends on what you learn from the download — inspecting duration, resolution, or format before deciding how to transcode. The plain ytdlp-download gives you the file first; you can then submit an FFmpeg command as a second step with the result.