The ffmpeg API. Your command over HTTPS.
Hosted ffmpeg as a REST API: send the command you already run with your input URLs, and get the files and a signed webhook back, or send the same job as JSON.
curl "$VIDEXO_API_URL/v1/jobs" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input_files": {
"in_video": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_main": "talk-720p.mp4"
},
"command": "-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {{out_main}}",
"metadata": {
"order": "1234"
}
}' import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY, baseUrl: process.env.VIDEXO_API_URL });
const job = await videxo.jobs.create({
"input_files": {
"in_video": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_main": "talk-720p.mp4"
},
"command": "-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {{out_main}}",
"metadata": {
"order": "1234"
}
});
const done = await videxo.jobs.wait(job.id); Install with npm i @videxo/sdk, or send the same request with curl or JavaScript. About the SDK
ffmpeg \
-i talk.mp4 \
-vf scale=-2:720 \
-c:v libx264 -crf 23 \
-c:a copy \
talk-720p.mp4 curl "$VIDEXO_API_URL/v1/transcode" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"preset": "web-720p",
"filename": "talk-720p.mp4",
"metadata": {
"order": "1234"
}
}' The request
Five fields. Two of them optional.
input_files- Aliases such as in_video, each to a public http or https URL, or the input_url of a completed private upload.
output_files- Aliases such as out_main, each to the file name you want back. The extension picks the format.
command- ffmpeg’s arguments without “ffmpeg”, with each file named by its {{alias}}.
webhook_url- Optional. An https URL that gets the job, signed, when it ends.
metadata- Optional. Your own strings, returned with the job and its webhook.
curl "$VIDEXO_API_URL/v1/jobs" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input_files": {
"in_video": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_main": "talk-720p.mp4"
},
"command": "-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {{out_main}}",
"metadata": {
"order": "1234"
}
}' import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY, baseUrl: process.env.VIDEXO_API_URL });
const job = await videxo.jobs.create({
"input_files": {
"in_video": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_main": "talk-720p.mp4"
},
"command": "-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {{out_main}}",
"metadata": {
"order": "1234"
}
});
const done = await videxo.jobs.wait(job.id); Install with npm i @videxo/sdk, or send the same request with curl or JavaScript. About the SDK
const response = await fetch(`${process.env.VIDEXO_API_URL}/v1/jobs`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIDEXO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"input_files": {
"in_video": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_main": "talk-720p.mp4"
},
"command": "-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {{out_main}}",
"metadata": {
"order": "1234"
}
}),
});
const job = await response.json(); {
"id": "job_3k9x0c2mfq8w1n7r5t4b6v2z",
"state": "queued",
"kind": "command",
"operation": null,
"spec": null,
"command": "-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {{out_main}}",
"input_files": {
"in_video": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_main": "talk-720p.mp4"
},
"ffmpeg_command": "ffmpeg -i https://videxo.io/samples/talk.mp4 -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy talk-720p.mp4",
"steps": [
{
"tool": "ffmpeg",
"inputs": [
"video"
],
"outputs": [
"main"
],
"command": "ffmpeg -i https://videxo.io/samples/talk.mp4 -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy talk-720p.mp4"
}
],
"webhook_url": null,
"metadata": {
"order": "1234"
},
"progress": null,
"error": null,
"outputs": [],
"usage": null,
"created_at": "2026-11-04T12:00:00.000Z",
"started_at": null,
"finished_at": null
} The answer is the job, queued, with its id. Ask for it with GET /v1/jobs/{id} or wait for the webhook.
Or JSON
Or skip the command. Say what you want in JSON.
Name an input and a preset or a few options, with curl or any HTTP client. The API writes the ffmpeg command and returns it with the job, so you can see and keep exactly what ran.
Operations referencecurl "$VIDEXO_API_URL/v1/transcode" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"preset": "web-720p",
"filename": "talk-720p.mp4",
"metadata": {
"order": "1234"
}
}' import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY, baseUrl: process.env.VIDEXO_API_URL });
const job = await videxo.transcode({
"input": "https://videxo.io/samples/talk.mp4",
"preset": "web-720p",
"filename": "talk-720p.mp4",
"metadata": {
"order": "1234"
}
});
const done = await videxo.jobs.wait(job.id); Install with npm i @videxo/sdk, or send the same request with curl or JavaScript. About the SDK
const response = await fetch(`${process.env.VIDEXO_API_URL}/v1/transcode`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIDEXO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"input": "https://videxo.io/samples/talk.mp4",
"preset": "web-720p",
"filename": "talk-720p.mp4",
"metadata": {
"order": "1234"
}
}),
});
const job = await response.json(); ffmpeg \
-i https://videxo.io/samples/talk.mp4 \
-map 0:v:0 \
-vf "scale='min(iw,1280)':'min(ih,720)':force_original_aspect_ratio=decrease:force_divisible_by=2" \
-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p \
-map "0:a:0?" \
-c:a aac -b:a 128k -movflags +faststart \
talk-720p.mp4 The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
Compatibility
What your commands can use. ffmpeg 6.1.1.
Options go where ffmpeg reads them: before an input, before an output or globally, with stream suffixes such as -c:v and -b:a. Values,
filter graphs and expressions are written as you write them today.
- Output formats the file name's extension picks the muxer
- .mp4 mp4
- .mov mov
- .m4a ipod
- .mkv matroska
- .webm webm
- .ts mpegts
- .mp3 mp3
- .aac adts
- .wav wav
- .flac flac
- .ogg ogg
- .opus opus
- .gif gif
- .png image2pipe
- .jpg image2pipe
- .jpeg image2pipe
- Input formats ffmpeg demuxers that open inputs
- mov
- matroska
- webm
- avi
- mpegts
- mp3
- aac
- wav
- flac
- ogg
- gif
- png_pipe
- jpeg_pipe
- webp_pipe
- bmp_pipe
- rawvideo
- s16le
- Codecs for -c, -codec and their stream forms, and copy
- libx264
- libx265
- libvpx
- libvpx-vp9
- libaom-av1
- mpeg4
- mjpeg
- png
- gif
- ffv1
- aac
- libmp3lame
- libopus
- libvorbis
- flac
- pcm_s16le
- pcm_s24le
- pcm_f32le
- alac
- Encoder presets and pixel formats -preset, -pix_fmt and the format filter
- ultrafast
- superfast
- veryfast
- faster
- fast
- medium
- slow
- yuv420p
- yuv422p
- yuv444p
- yuv420p10le
- yuv422p10le
- yuv444p10le
- yuva420p
- rgb24
- rgba
- bgra
- gray
- pal8
- nv12
- 53 options each where ffmpeg reads it
- -loglevel
- -v
- -filter_complex
- -filter_complex_threads
- -filter_threads
- -f
- -ss
- -t
- -to
- -itsoffset
- -skip_frame
- -stream_loop
- -r
- -s
- -pix_fmt
- -ar
- -ac
- -threads
- -c
- -codec
- -b
- -maxrate
- -minrate
- -bufsize
- -crf
- -q
- -qscale
- -preset
- -tune
- -profile
- -level
- -g
- -keyint_min
- -bf
- -tag
- -cpu-used
- -frames
- -vframes
- -aframes
- -map
- -map_metadata
- -map_chapters
- -metadata
- -movflags
- -fps_mode
- -vn
- -an
- -sn
- -dn
- -shortest
- -vf
- -af
- -filter
- 48 filters in -vf, -af, -filter and -filter_complex
- acrossfade
- afade
- aformat
- amix
- apad
- aresample
- asetpts
- asplit
- atempo
- atrim
- boxblur
- colorchannelmixer
- concat
- crop
- drawtext
- eq
- fade
- format
- fps
- hflip
- hue
- loudnorm
- overlay
- pad
- palettegen
- paletteuse
- rotate
- scale
- scale2ref
- scdet
- select
- setdar
- setpts
- setsar
- showspectrum
- showwaves
- split
- subtitles
- thumbnail
- tile
- tpad
- transpose
- trim
- unsharp
- vflip
- volume
- xfade
- zoompan
A command holds up to 1,024 arguments and 65,536 characters, names up to 16 input and 16 output files, and uses up to 128 filters in its graphs.
Migrate
From your server. To one POST.
- 1 Drop
ffmpegitself and the options for a terminal, such as-yand-hide_banner. - 2 Give each input an alias in
input_files, mapped to its URL or upload, and write{{in_name}}where its path was. - 3 Give each output an alias in
output_files, mapped to its file name, and write{{out_name}}where its path was.
ffmpeg \
-i talk.mp4 \
-vf scale=-2:720 \
-c:v libx264 -crf 23 \
-c:a copy \
talk-720p.mp4 curl "$VIDEXO_API_URL/v1/jobs" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input_files": {
"in_video": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_main": "talk-720p.mp4"
},
"command": "-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {{out_main}}",
"metadata": {
"order": "1234"
}
}' import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY, baseUrl: process.env.VIDEXO_API_URL });
const job = await videxo.jobs.create({
"input_files": {
"in_video": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_main": "talk-720p.mp4"
},
"command": "-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {{out_main}}",
"metadata": {
"order": "1234"
}
});
const done = await videxo.jobs.wait(job.id); Install with npm i @videxo/sdk, or send the same request with curl or JavaScript. About the SDK
ffmpeg \
-i https://videxo.io/samples/talk.mp4 \
-vf scale=-2:720 \
-c:v libx264 -crf 23 \
-c:a copy \
talk-720p.mp4 The ffmpeg tab is the job's ffmpeg_command: your command with the URLs and file names put back.
Webhooks
Signed when it ends. Retried for about a day.
When a job succeeds, fails or is cancelled, the API posts the whole job to your webhook in the Standard Webhooks format, signed with your account’s secret. Verify it with the standardwebhooks library or with a few lines of node:crypto.
Webhooks and signaturesimport crypto from 'node:crypto';
// The event when the headers prove the body came from us, null otherwise.
function verifyWebhook(secret, headers, body, now = Date.now()) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signatures = headers['webhook-signature'];
if (!id || !timestamp || !signatures) return null;
// Refuse a timestamp more than five minutes away: an old delivery replayed.
if (Math.abs(now / 1000 - Number(timestamp)) > 300) return null;
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${body}`).digest();
// After a rotation the header carries two signatures, space separated: either may match.
for (const signature of signatures.split(' ')) {
const [version, value = ''] = signature.split(',');
const given = Buffer.from(value, 'base64');
if (version === 'v1' && given.length === expected.length && crypto.timingSafeEqual(given, expected)) {
return JSON.parse(body);
}
}
return null;
} {
"type": "job.succeeded",
"timestamp": "2026-11-04T12:01:09.000Z",
"data": {
"id": "job_3k9x0c2mfq8w1n7r5t4b6v2z",
"state": "succeeded",
"kind": "operations",
"operation": "transcode",
"spec": {
"inputs": {
"source": {
"url": "https://videxo.io/samples/talk.mp4"
}
},
"outputs": [
{
"name": "video",
"type": "video",
"preset": "web-720p",
"input": "source",
"filename": "talk-720p.mp4",
"format": "mp4",
"resize": {
"width": 1280,
"height": 720,
"fit": "contain",
"enlarge": false
},
"video": {
"codec": "h264",
"crf": 23,
"encoder_preset": "veryfast",
"pixel_format": "yuv420p",
"faststart": true
},
"audio": {
"codec": "aac",
"bitrate": "128k"
}
}
]
},
"command": "-i {{in_source}} -map 0:v:0 -vf \"scale='min(iw,1280)':'min(ih,720)':force_original_aspect_ratio=decrease:force_divisible_by=2\" -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map 0:a:0? -c:a aac -b:a 128k -movflags +faststart {{out_video}}",
"input_files": {
"in_source": "https://videxo.io/samples/talk.mp4"
},
"output_files": {
"out_video": "talk-720p.mp4"
},
"ffmpeg_command": "ffmpeg -i https://videxo.io/samples/talk.mp4 -map 0:v:0 -vf 'scale='\\''min(iw,1280)'\\'':'\\''min(ih,720)'\\'':force_original_aspect_ratio=decrease:force_divisible_by=2' -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart talk-720p.mp4",
"steps": [
{
"tool": "ffmpeg",
"inputs": [
"source"
],
"outputs": [
"video"
],
"command": "ffmpeg -i https://videxo.io/samples/talk.mp4 -map 0:v:0 -vf 'scale='\\''min(iw,1280)'\\'':'\\''min(ih,720)'\\'':force_original_aspect_ratio=decrease:force_divisible_by=2' -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart talk-720p.mp4"
}
],
"webhook_url": null,
"metadata": {
"order": "1234"
},
"progress": 1,
"error": null,
"outputs": [
{
"alias": "out_video",
"name": "video",
"filename": "talk-720p.mp4",
"size_bytes": 300000000,
"sha256": "9f2c4e1a7b3d5f60c8e2a4b6d8f0e1c3a5b7d9f1e3c5a7b9d1f3e5c7a9b1d3f5",
"content_type": "video/mp4",
"expires_at": "2026-11-05T12:01:09.000Z",
"download_url": "https://api.videxo.io/v1/files/job_3k9x0c2mfq8w1n7r5t4b6v2z/out_video?account=acc_example&expires=1793797269&signature=example",
"download_url_expires_at": "2026-11-04T13:01:09.000Z",
"deleted_at": null
}
],
"usage": {
"seconds": 61,
"input_bytes": 1200000000,
"output_bytes": 300000000,
"credits": 45,
"bill": {
"lines": [
{
"kind": "video",
"seconds": 300,
"per_minute": 6,
"multiplier": 1,
"credits": 30
}
],
"data": {
"input_bytes": 1200000000,
"output_bytes": 300000000,
"credits": 15
},
"processing": {
"seconds": 61,
"included_seconds": 900,
"credits": 0
}
}
},
"created_at": "2026-11-04T12:00:00.000Z",
"started_at": "2026-11-04T12:00:04.000Z",
"finished_at": "2026-11-04T12:01:09.000Z"
}
} Retries
Retry without a second job.
A timeout can hide whether a job was created. Send an Idempotency-Key and retry with the same key: for 24 hours you get the same job back, never a second one.
Idempotencycurl "$VIDEXO_API_URL/v1/transcode" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-transcode" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"preset": "web-720p",
"filename": "talk-720p.mp4",
"metadata": {
"order": "1234"
}
}' Examples
Start from a command that runs. Then change it.
- Compress and transcode
-i {{in_video}} -c:v libx264 -crf 28 -preset veryfast -c:a aac -b:a 128k -movflags +faststart {{out_main}} - Trim and clip
-ss 00:01:30 -i {{in_video}} -t 30 -c:v libx264 -crf 23 -c:a aac {{out_main}} - Vertical 9:16
-i {{in_video}} -vf "crop=ih*9/16:ih, scale=1080: 1920" -c:v libx264 -crf 23 -c:a copy {{out_main}} - Watermark or logo
-i {{in_video}} -i {{in_logo}} -filter_complex "[1:v]scale=160:-1[logo]; [0:v][logo]overlay=x=W-w-24: y=H-h-24[v]" -map "[v]" -map 0:a? -c:v libx264 -crf 23 -c:a copy {{out_main}} - GIF previews
-ss 5 -t 4 -i {{in_video}} -filter_complex "[0:v]fps=12,scale=480: -1: flags=lanczos, split[a][b]; [a]palettegen[p]; [b][p]paletteuse[v]" -map "[v]" {{out_main}} - Loudness normalisation
-i {{in_video}} -c:v copy -af "loudnorm=I=-16:LRA=11: TP=-1.5" -c:a aac -b:a 192k {{out_main}}
Questions, answered.
Is this ffmpeg as a service?
Yes. Hosted ffmpeg behind a REST API: POST /v1/jobs takes your ffmpeg arguments, input URLs or uploads and output names, runs the job and stores the files, and GET /v1/jobs/{id} or a signed webhook returns them. There is no ffmpeg server, queue or storage for you to run.
Which ffmpeg features can my commands use?
ffmpeg 6.1.1 with 48 filters, 53 options, the codecs libx264, libx265, libvpx, libvpx-vp9, libaom-av1, mpeg4, mjpeg, png, gif, ffv1, aac, libmp3lame, libopus, libvorbis, flac, pcm_s16le, pcm_s24le, pcm_f32le, alac and the output formats .mp4 .mov .m4a .mkv .webm .ts .mp3 .aac .wav .flac .ogg .opus .gif .png .jpg .jpeg. The list above and the supported commands page name every one.
Can I describe a job in JSON instead of a command?
Yes. POST /v1/transcode, /v1/thumbnails, /v1/gif and the other operations take an input and a preset or a few options, and POST /v1/jobs takes a list of outputs. The job returns the ffmpeg command it compiled to, so you can move one job at a time and run the same command yourself.
Which languages can call it?
Any language that can send an HTTPS request. The docs show curl and JavaScript and have complete example projects for common frameworks, and the OpenAPI 3.1 document at /docs/openapi.json can generate a client for the rest.
How do I get the output files?
Each output includes a signed download_url valid for up to one hour, bounded by its retention. Read the job again for a fresh link, or use GET /v1/jobs/{id}/outputs/{alias} with your API key.
What does a job cost?
6 credits a minute of 720p output and 12 at 1080p, plus 10 credits for each GB fetched and stored. Pay as you go needs no card, and your personal organization gets 500 free credits once.
What happens when a command needs a change?
The API answers 400 before a job exists, with each argument to change and a sentence that says how; nothing is billed. If ffmpeg stops while running, the job ends failed with an error code, and its webhook says so.