Skip to content

Custom FFmpeg Jobs

When the built-in recipes don’t cover your use case, you can pass your own FFmpeg argument array directly. This gives you full control over filters, codecs, and encoding parameters.

Set mode to "ffmpeg" and provide an ffmpeg.args array. Xora runs your command with a full FFmpeg build (all codecs and filters available).

There are two input/output patterns:

Use input + output with {{input}} / {{output}} placeholders:

{
"mode": "ffmpeg",
"input": { "url": "https://example.com/video.mp4" },
"output": { "format": "mp4" },
"ffmpeg": {
"args": ["-i", "{{input}}", "-vf", "scale=640:-1", "{{output}}"]
}
}

The simplest way — one input, one output. Use {{input}} and {{output}} as placeholders in your args:

Terminal window
curl -X POST https://api.xora.sh/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "ffmpeg",
"input": { "url": "https://example.com/video.mp4" },
"output": { "format": "mp4" },
"ffmpeg": {
"args": [
"-i", "{{input}}",
"-vf", "scale=1280:-1",
"-c:v", "libx264",
"-crf", "23",
"-c:a", "aac",
"{{output}}"
]
}
}'
PlaceholderReplaced with
{{input}}Path to the downloaded input file
{{output}}Path for the output file (extension from output.format)

For commands with multiple inputs or outputs, use named input_files and output_files:

Terminal window
curl -X POST https://api.xora.sh/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "ffmpeg",
"input_files": {
"in_1": "https://example.com/video.mp4",
"in_2": "https://example.com/watermark.png"
},
"output_files": {
"out_1": "watermarked.mp4"
},
"ffmpeg": {
"args": [
"-i", "{{in_1}}",
"-i", "{{in_2}}",
"-filter_complex", "overlay=10:10",
"-c:v", "libx264",
"{{out_1}}"
]
}
}'
FieldKey patternExample
input_filesin_ + alphanumeric/underscorein_1, in_video, in_audio_track
output_filesout_ + alphanumeric/underscoreout_1, out_video, out_thumbnail

Output file values must be a filename with an extension (e.g., result.mp4, audio.mp3).

Create an 8fps, 320px-wide GIF preview:

{
"mode": "ffmpeg",
"input_files": { "in_1": "https://example.com/video.mp4" },
"output_files": { "out_1": "preview.gif" },
"ffmpeg": {
"args": [
"-i", "{{in_1}}",
"-vf", "fps=8,scale=320:-1:flags=lanczos",
"-an", "-loop", "0",
"{{out_1}}"
]
}
}
{
"mode": "ffmpeg",
"input_files": {
"in_1": "https://example.com/video.mp4",
"in_2": "https://example.com/logo.png"
},
"output_files": { "out_1": "branded.mp4" },
"ffmpeg": {
"args": [
"-i", "{{in_1}}",
"-i", "{{in_2}}",
"-filter_complex", "overlay=W-w-10:H-h-10",
"-c:v", "libx264", "-crf", "23",
"-c:a", "copy",
"{{out_1}}"
]
}
}
{
"mode": "ffmpeg",
"input": { "url": "https://example.com/video.mp4" },
"output": { "format": "gif" },
"ffmpeg": {
"args": [
"-ss", "10", "-t", "5",
"-i", "{{input}}",
"-vf", "fps=10,scale=480:-1:flags=lanczos",
"-loop", "0",
"{{output}}"
]
}
}
{
"mode": "ffmpeg",
"input": { "url": "https://example.com/video.mp4" },
"output": { "format": "webm" },
"ffmpeg": {
"args": [
"-i", "{{input}}",
"-c:v", "libvpx-vp9",
"-b:v", "1M",
"-c:a", "libopus",
"{{output}}"
]
}
}

Change the container without re-encoding — near instant:

{
"mode": "ffmpeg",
"input": { "url": "https://example.com/video.mkv" },
"output": { "format": "mp4" },
"ffmpeg": {
"args": ["-i", "{{input}}", "-c", "copy", "{{output}}"]
}
}

FFmpeg jobs run inside a sandboxed environment. The following are blocked:

  • Shell metacharacters — no pipes (|), backticks, $(...), etc.
  • Unsafe protocols — only https:// and local managed paths are allowed
  • Arbitrary output paths — output can only go to the managed working directory
  • Unbounded streams — commands that could generate infinite output are rejected

If your command fails validation, you’ll get a VALIDATION_ERROR or UNSUPPORTED_COMMAND error with a description of what was rejected.

LimitValue
Max args128 elements
Job timeout15 minutes
Max working disk10 GB

All 9 formats are available in FFmpeg mode: mp4, webm, mov, gif, mp3, aac, wav, jpg, png.