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.
How FFmpeg mode works
Section titled “How FFmpeg mode works”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}}"] }}Use input_files + output_files with {{in_*}} / {{out_*}} placeholders:
{ "mode": "ffmpeg", "input_files": { "in_1": "https://example.com/video.mp4" }, "output_files": { "out_1": "result.mp4" }, "ffmpeg": { "args": ["-i", "{{in_1}}", "-c:v", "libx264", "-crf", "23", "{{out_1}}"] }}Single-file mode
Section titled “Single-file mode”The simplest way — one input, one output. Use {{input}} and {{output}} as placeholders in your args:
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}}" ] } }'const response = await fetch('https://api.xora.sh/v1/jobs', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ 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}}' ] } })});const data = await response.json();console.log(data);import requests
response = requests.post( "https://api.xora.sh/v1/jobs", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "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}}" ] } })data = response.json()print(data)Placeholders
Section titled “Placeholders”| Placeholder | Replaced with |
|---|---|
{{input}} | Path to the downloaded input file |
{{output}} | Path for the output file (extension from output.format) |
Multi-file mode
Section titled “Multi-file mode”For commands with multiple inputs or outputs, use named input_files and output_files:
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}}" ] } }'const response = await fetch('https://api.xora.sh/v1/jobs', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ 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}}' ] } })});const data = await response.json();console.log(data);import requests
response = requests.post( "https://api.xora.sh/v1/jobs", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "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}}" ] } })data = response.json()print(data)Naming rules
Section titled “Naming rules”| Field | Key pattern | Example |
|---|---|---|
input_files | in_ + alphanumeric/underscore | in_1, in_video, in_audio_track |
output_files | out_ + alphanumeric/underscore | out_1, out_video, out_thumbnail |
Output file values must be a filename with an extension (e.g., result.mp4, audio.mp3).
Examples
Section titled “Examples”Generate animated GIF
Section titled “Generate animated GIF”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}}" ] }}Add a watermark overlay
Section titled “Add a watermark overlay”{ "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}}" ] }}Extract a 5-second clip as GIF
Section titled “Extract a 5-second clip as GIF”{ "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}}" ] }}Convert to VP9 WebM with custom bitrate
Section titled “Convert to VP9 WebM with custom bitrate”{ "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}}" ] }}Remux without re-encoding (fast)
Section titled “Remux without re-encoding (fast)”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}}"] }}Security restrictions
Section titled “Security restrictions”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.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Max args | 128 elements |
| Job timeout | 15 minutes |
| Max working disk | 10 GB |
Supported output formats
Section titled “Supported output formats”All 9 formats are available in FFmpeg mode: mp4, webm, mov, gif, mp3, aac, wav, jpg, png.