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.

POST/v1/ffmpeg
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.

ParameterDescription
inputsobject

Files to download before ffmpeg starts, as { "name.ext": "https://..." }. Each name is the file name your args use.

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 ffmpeg itself. An array is used as is. A string is split like a shell would, quotes included, and a leading ffmpeg is dropped.

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 6001 to 600

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 inputs into 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 outputs must exist and not be empty. Each one comes back as a URL under job.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: or pipe: in inputs, outputs or file options. List remote files in inputs instead.
  • No absolute paths, ~ or ... The one exception is fontfile= and fontsdir= pointing into /usr/share/fonts/reelapi/.
  • No movie or amovie filters. Add the file as another -i.
  • No -f tee, -pattern_type glob or -safe 0. Use a %d image sequence, and list concat files by name.
  • No -filter_script, -filter_complex_script, -pre preset files, hardware devices, or protocol and format whitelists.
  • -progress pipe:1 and pipe:2 are 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.

FamilyFiles
MontserratMontserrat-Black.ttf, Montserrat-ExtraBold.ttf
PoppinsPoppins-Black.ttf, Poppins-ExtraBold.ttf, Poppins-Bold.ttf
AntonAnton-Regular.ttf
Bebas NeueBebasNeue-Regular.ttf
Noto Sans CJKNotoSansJP-Bold.otf, NotoSansSC-Bold.otf, NotoSansKR-Bold.otf
Noto SansNotoSansArabic-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.

Request body
{
  "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.

Request body
{
  "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.

Request body
{
  "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.

Request body
{
  "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.

Request body
{
  "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.

Request body
{
  "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.

Request body
{
  "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.

200 OK
{
  "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.

422 Unprocessable Content
{
  "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"
  }
}
CodeHTTPWhen it happens
INVALID_REQUEST400A sandbox rule was broken, or outputs is missing and the last argument isn't a file name
INPUT_FETCH_FAILED422We couldn't download an input, for example Could not fetch inputs.in.mp4: HTTP 404
INPUT_TOO_LARGE422An input is over 500 MB, or all inputs together are over 1,000 MB
FFMPEG_FAILED422ffmpeg exited with an error. The message is its last useful line.
OUTPUT_MISSING422A file in outputs wasn't written or is empty, for example ffmpeg did not produce other.png
OUTPUT_TOO_LARGE422The job wrote more than 8 GB, or the outputs add up to more than 2 GB
TIMEOUT422ffmpeg ran past timeout_s, or the job past 10 minutes

Last updated 26 October 2026