Endpoints
FFmpeg API
POST /v1/ffmpeg runs ffmpeg 8.1 with your own arguments in the cloud. Send up to 50 input files, get back up to 20 outputs as download URLs, and let a single command run for up to 600 seconds.
Any filter graph, codec or format ffmpeg supports works, inside a job directory that keeps each job to its own files.
- Typical run
- 4 to 46 s
- for the recipes on this page
- Price
- $0.01
- per compute minute
- Inputs
- 500 MB each
- 1,000 MB per job
Example#
Turn a landscape video into a 1080x1920 vertical one, with a blurred copy filling the space above and below. Point in.mp4 at a public link to your video or an upload.
curl -sS "https://api.reelsdk.com/v1/ffmpeg" \
-H "Authorization: Bearer $REELSDK_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"in.mp4": "https://example.com/landscape.mp4"
},
"args": [
"-i", "in.mp4", "-filter_complex",
"[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=24:2[bg];[0:v]scale=1080:-2[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2",
"-c:a", "copy", "vertical.mp4"
]
}'Parameters#
Send a JSON body with args. Fields we don't recognize are ignored.
| Parameter | Description |
|---|---|
inputsobject | Files to download before ffmpeg starts, as Up to 50 files. Names are 1 to 64 letters, digits, dots, dashes or underscores, starting with a letter or digit |
argsarray or stringRequired | The ffmpeg arguments, without 1 to 1,000 arguments of up to 8,192 characters each, or one string up to 65,536 characters |
outputsarray | The files ffmpeg writes that you want back. Leave it out and we use the last argument when it's a plain file name. 1 to 20 unique file names, same naming rules as inputs |
timeout_sinteger | Stop ffmpeg after this many seconds. Default |
webhook_urlstring | We POST the finished job to this URL. See Webhooks. Public http(s) URL |
How a job runs#
- We download every file in
inputsinto an empty job directory, under the name you gave it. - We run ffmpeg 8.1 in that directory with your
args, straight from the array, with no shell in between. Existing files are overwritten unless you pass-n. - When ffmpeg exits with code 0, every file in
outputsmust exist and not be empty. Each one comes back as a URL underjob.outputs, keyed by its file name.
Inputs can be up to 500 MB each and 1,000 MB together, and a job may write up to 8 GB while it runs. The outputs you get back can add up to 2 GB, and the whole job has 10 minutes.
Sandbox rules#
The job directory is all ffmpeg can see. We check your arguments before the job starts, and a blocked option returns 400 naming the argument, such as args[3]: the movie filter is not supported, add the file as another -i instead.
- No URLs or protocols like
http:,tcp:,concat:orpipe:in inputs, outputs or file options. List remote files ininputsinstead. - No absolute paths,
~or... The one exception isfontfile=andfontsdir=pointing into/usr/share/fonts/reelapi/. - No
movieoramoviefilters. Add the file as another-i. - No
-f tee,-pattern_type globor-safe 0. Use a%dimage sequence, and list concat files by name. - No
-filter_script,-filter_complex_script,-prepreset files, hardware devices, or protocol and format whitelists. -progress pipe:1andpipe:2are allowed.
Fonts#
drawtext, subtitles and ass can use the fonts we bundle in /usr/share/fonts/reelapi/, the same ones the captions use. Point fontfile= at a file, or fontsdir= at the folder and pick a font by name.
| Family | Files |
|---|---|
| Montserrat | Montserrat-Black.ttf, Montserrat-ExtraBold.ttf |
| Poppins | Poppins-Black.ttf, Poppins-ExtraBold.ttf, Poppins-Bold.ttf |
| Anton | Anton-Regular.ttf |
| Bebas Neue | BebasNeue-Regular.ttf |
| Noto Sans CJK | NotoSansJP-Bold.otf, NotoSansSC-Bold.otf, NotoSansKR-Bold.otf |
| Noto Sans | NotoSansArabic-Bold.ttf, NotoSansHebrew-Bold.ttf, NotoSansThai-Bold.ttf, NotoSansDevanagari-Bold.ttf, NotoSansLao-Bold.ttf, NotoSansMyanmar-Bold.ttf |
Recipes#
Each of these ran on the live API. Swap in your own URLs and send the body to POST /v1/ffmpeg.
Grab a thumbnail
One frame at 5 seconds, as a high quality JPEG. Took 6 s.
{
"inputs": {
"in.mp4": "https://example.com/clip.mp4"
},
"args": [
"-ss", "5", "-i", "in.mp4", "-frames:v", "1", "-q:v", "2",
"poster.jpg"
]
}Make a GIF preview
The first 4 seconds at 12 fps and 480 px wide, with a palette built from the clip so colors stay clean. Took 16 s.
{
"inputs": {
"in.mp4": "https://example.com/clip.mp4"
},
"args": [
"-t", "4", "-i", "in.mp4", "-vf",
"fps=12,scale=480:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse",
"-loop", "0", "preview.gif"
]
}Extract the audio as MP3
Drops the video and encodes variable bitrate MP3 at about 190 kbps. Took 4 s.
{
"inputs": {
"in.mp4": "https://example.com/clip.mp4"
},
"args": ["-i", "in.mp4", "-vn", "-c:a", "libmp3lame", "-q:a", "2", "audio.mp3"]
}Burn in an SRT file
Renders your subtitles with a bundled font. The audio is copied without re-encoding. Took 29 s for 42 s.
{
"inputs": {
"in.mp4": "https://example.com/clip.mp4",
"subs.srt": "https://example.com/subs.srt"
},
"args": [
"-i", "in.mp4", "-vf",
"subtitles=subs.srt:fontsdir=/usr/share/fonts/reelapi:force_style='FontName=Poppins Black,FontSize=16'",
"-c:a", "copy", "subtitled.mp4"
]
}Draw a title on the video
Centered text near the top in Anton with a black outline. Took 41 s for 30 s.
{
"inputs": {
"in.mp4": "https://example.com/clip.mp4"
},
"args": [
"-i", "in.mp4", "-vf",
"drawtext=fontfile=/usr/share/fonts/reelapi/Anton-Regular.ttf:text='Day 1':fontsize=120:fontcolor=white:borderw=6:bordercolor=black:x=(w-text_w)/2:y=h*0.1",
"-c:a", "copy", "titled.mp4"
]
}Join two clips
Plays one clip after the other. Both need an audio track and the same frame size. Took 46 s for 37 s.
{
"inputs": {
"a.mp4": "https://example.com/a.mp4",
"b.mp4": "https://example.com/b.mp4"
},
"args": [
"-i", "a.mp4", "-i", "b.mp4", "-filter_complex",
"[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[v][a]", "-map", "[v]",
"-map", "[a]", "joined.mp4"
]
}Send args as one string
Paste a command as you'd type it in a terminal. We split it like a shell and drop the leading ffmpeg. Took 5 s.
{
"inputs": {
"in.mp4": "https://example.com/clip.mp4"
},
"args": "ffmpeg -i in.mp4 -vf \"scale=640:-2\" -an small.mp4"
}Response#
outputs is keyed by the file names you asked for, so read job.outputs["vertical.mp4"]. usage.output_seconds adds up the length of every audio and video output, and images count as 0. duration_s is the length of the first one.
{
"job": {
"id": "job_tc4682j24vighvp5f5op",
"op": "ffmpeg",
"status": "succeeded",
"stage": null,
"created_at": "2026-10-26T01:21:16.435Z",
"finished_at": "2026-10-26T01:21:39.971Z",
"outputs": {
"vertical.mp4": "https://api.reelsdk.com/f/out/job_tc4682j24vighvp5f5op/vertical.mp4"
},
"result": null,
"duration_s": 30,
"error": null,
"usage": {
"output_seconds": 30,
"audio_seconds": 0,
"tts_characters": 0,
"images": 0
},
"poll_url": "https://api.reelsdk.com/v1/jobs/job_tc4682j24vighvp5f5op"
}
}Errors#
When ffmpeg fails, the job fails with 422 and the message is the last useful line ffmpeg printed. The body carries the job next to the error.
{
"error": {
"code": "FFMPEG_FAILED",
"message": "Error opening output files: Filter not found"
},
"job": {
"id": "job_o1d0oim2n3bxs867jivv",
"op": "ffmpeg",
"status": "failed",
"stage": null,
"created_at": "2026-10-26T02:12:35.197Z",
"finished_at": "2026-10-26T02:12:38.421Z",
"outputs": {},
"result": null,
"duration_s": null,
"error": {
"code": "FFMPEG_FAILED",
"message": "Error opening output files: Filter not found"
},
"usage": {
"output_seconds": 0,
"audio_seconds": 0,
"tts_characters": 0,
"images": 0
},
"poll_url": "https://api.reelsdk.com/v1/jobs/job_o1d0oim2n3bxs867jivv"
}
}| Code | HTTP | When it happens |
|---|---|---|
| INVALID_REQUEST | 400 | A sandbox rule was broken, or outputs is missing and the last argument isn't a file name |
| INPUT_FETCH_FAILED | 422 | We couldn't download an input, for example Could not fetch inputs.in.mp4: HTTP 404 |
| INPUT_TOO_LARGE | 422 | An input is over 500 MB, or all inputs together are over 1,000 MB |
| FFMPEG_FAILED | 422 | ffmpeg exited with an error. The message is its last useful line. |
| OUTPUT_MISSING | 422 | A file in outputs wasn't written or is empty, for example ffmpeg did not produce other.png |
| OUTPUT_TOO_LARGE | 422 | The job wrote more than 8 GB, or the outputs add up to more than 2 GB |
| TIMEOUT | 422 | ffmpeg ran past timeout_s, or the job past 10 minutes |
Last updated 26 October 2026