The video processing API. ffmpeg inside. No servers.
Compress, resize, trim, watermark, make thumbnails and GIFs, clean up audio. Send one JSON request with a preset or a few options, from curl or any HTTP client; Videxo runs it, stores the files and calls your webhook. Already use ffmpeg? Your commands work too.
500 free credits to start, no card.
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"
}
}'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 -crf 23 -pix_fmt yuv420p \
-map "0:a:0?" \
-c:a aac -b:a 128k -movflags +faststart -preset:v veryfast \
talk-720p.mp4The run
- Compiled to one ffmpeg command, 21 arguments
-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 -crf 23 -pix_fmt yuv420p -map 0:a:0? -c:a aac -b:a 128k -movflags +faststart {{out_video}}
- Queued waited 4 s for a slot 12:00:00
- Running 61 s of ffmpeg 12:00:04
- Succeeded after 65 s 12:01:09
talk-720p.mp4 300 MB video/mp4 sha256 9f2c4e1a7b3d expires in 24 h
- Webhook job.succeeded, signed
- 52.5 credits 37.5 for 5 min of 720p, 15 for 1.5 GB in and out
The example job from the docs, replayed. Its times, sizes and credits are from the job's record.
Endpoints
One endpoint per job. JSON in, files out.
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"
}' 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"
});
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"
}),
});
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 -crf 23 -pix_fmt yuv420p \
-map "0:a:0?" \
-c:a aac -b:a 128k -movflags +faststart -preset:v veryfast \
video.mp4 The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
curl "$VIDEXO_API_URL/v1/thumbnails" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"times": [
2,
7,
12
],
"resize": {
"width": 640
}
}' 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.thumbnails({
"input": "https://videxo.io/samples/talk.mp4",
"times": [
2,
7,
12
],
"resize": {
"width": 640
}
});
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/thumbnails`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIDEXO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"input": "https://videxo.io/samples/talk.mp4",
"times": [
2,
7,
12
],
"resize": {
"width": 640
}
}),
});
const job = await response.json(); ffmpeg \
-ss 2 -i https://videxo.io/samples/talk.mp4 \
-ss 7 -i https://videxo.io/samples/talk.mp4 \
-ss 12 -i https://videxo.io/samples/talk.mp4 \
-map 0:v:0 \
-vf scale=640:7680:force_original_aspect_ratio=decrease:force_divisible_by=2 \
-frames:v 1 -q:v 2 \
thumbnail_1.jpg \
-map 1:v:0 \
-vf scale=640:7680:force_original_aspect_ratio=decrease:force_divisible_by=2 \
-frames:v 1 -q:v 2 \
thumbnail_2.jpg \
-map 2:v:0 \
-vf scale=640:7680:force_original_aspect_ratio=decrease:force_divisible_by=2 \
-frames:v 1 -q:v 2 \
thumbnail_3.jpg The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
curl "$VIDEXO_API_URL/v1/storyboard" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"interval": 1,
"columns": 5,
"rows": 3
}' 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.storyboard({
"input": "https://videxo.io/samples/talk.mp4",
"interval": 1,
"columns": 5,
"rows": 3
});
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/storyboard`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIDEXO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"input": "https://videxo.io/samples/talk.mp4",
"interval": 1,
"columns": 5,
"rows": 3
}),
});
const job = await response.json(); ffmpeg \
-skip_frame nokey -t 15 -i https://videxo.io/samples/talk.mp4 \
-map 0:v:0 \
-vf fps=1/1,scale=320:320:force_original_aspect_ratio=decrease:force_divisible_by=2,tile=5x3 \
-frames:v 1 -q:v 5 \
storyboard.jpg The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
curl "$VIDEXO_API_URL/v1/gif" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"preset": "gif-preview"
}' 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.gif({
"input": "https://videxo.io/samples/talk.mp4",
"preset": "gif-preview"
});
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/gif`, {
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": "gif-preview"
}),
});
const job = await response.json(); ffmpeg \
-t 5 -i https://videxo.io/samples/talk.mp4 \
-filter_complex "[0:v:0]scale='min(iw,480)':'min(ih,7680)':force_original_aspect_ratio=decrease:force_divisible_by=2:flags=lanczos,fps=12[o0s0];[o0s0]split[o0g0][o0g1];[o0g0]palettegen=max_colors=256[o0p];[o0g1][o0p]paletteuse[o0v]" \
-map "[o0v]" \
gif.gif The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
curl "$VIDEXO_API_URL/v1/audio" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"preset": "podcast-audio"
}' 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.audio({
"input": "https://videxo.io/samples/talk.mp4",
"preset": "podcast-audio"
});
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/audio`, {
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": "podcast-audio"
}),
});
const job = await response.json(); ffmpeg \
-i https://videxo.io/samples/talk.mp4 \
-vn \
-map 0:a:0 \
-af loudnorm=I=-16:LRA=11:TP=-1.5 \
-c:a libmp3lame -b:a 128k -ar 44100 -ac 2 \
audio.mp3 The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
curl "$VIDEXO_API_URL/v1/concat" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": [
{
"source": "https://videxo.io/samples/talk.mp4",
"start": 0,
"end": 5
},
{
"source": "https://videxo.io/samples/talk.mp4",
"start": 10,
"end": 15
}
],
"resize": {
"width": 1920,
"height": 1080,
"fit": "pad"
}
}' 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.concat({
"inputs": [
{
"source": "https://videxo.io/samples/talk.mp4",
"start": 0,
"end": 5
},
{
"source": "https://videxo.io/samples/talk.mp4",
"start": 10,
"end": 15
}
],
"resize": {
"width": 1920,
"height": 1080,
"fit": "pad"
}
});
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/concat`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIDEXO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"inputs": [
{
"source": "https://videxo.io/samples/talk.mp4",
"start": 0,
"end": 5
},
{
"source": "https://videxo.io/samples/talk.mp4",
"start": 10,
"end": 15
}
],
"resize": {
"width": 1920,
"height": 1080,
"fit": "pad"
}
}),
});
const job = await response.json(); ffmpeg \
-t 5 -i https://videxo.io/samples/talk.mp4 \
-ss 10 -t 5 -i https://videxo.io/samples/talk.mp4 \
-filter_complex "[0:v]setpts=PTS-STARTPTS,scale=1920:1080:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=0x000000,setsar=1,fps=30[o0c0v];[0:a]asetpts=PTS-STARTPTS[o0c0a];[1:v]setpts=PTS-STARTPTS,scale=1920:1080:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=0x000000,setsar=1,fps=30[o0c1v];[1:a]asetpts=PTS-STARTPTS[o0c1a];[o0c0v][o0c0a][o0c1v][o0c1a]concat=n=2:v=1:a=1[o0cv][o0ca]" \
-map "[o0cv]" \
-c:v libx264 -crf 23 -pix_fmt yuv420p \
-map "[o0ca]" \
-c:a aac -b:a 128k -movflags +faststart -preset:v veryfast \
video.mp4 The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
curl "$VIDEXO_API_URL/v1/slideshow" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"images": [
"https://example.com/photo-1.jpg",
{
"source": "https://example.com/photo-2.jpg",
"motion": "pan-right"
}
],
"duration": 4,
"motion": "zoom-in",
"resize": {
"width": 1280,
"height": 720
}
}' 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.slideshow({
"images": [
"https://example.com/photo-1.jpg",
{
"source": "https://example.com/photo-2.jpg",
"motion": "pan-right"
}
],
"duration": 4,
"motion": "zoom-in",
"resize": {
"width": 1280,
"height": 720
}
});
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/slideshow`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIDEXO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"images": [
"https://example.com/photo-1.jpg",
{
"source": "https://example.com/photo-2.jpg",
"motion": "pan-right"
}
],
"duration": 4,
"motion": "zoom-in",
"resize": {
"width": 1280,
"height": 720
}
}),
});
const job = await response.json(); ffmpeg \
-i https://example.com/photo-1.jpg \
-i https://example.com/photo-2.jpg \
-filter_complex "[0:v]fps=30:start_time=0:round=up,trim=end_frame=1,scale=2560:1440:force_original_aspect_ratio=increase,crop=2560:1440,zoompan=z=1+0.2*on/119:x=(iw-iw/zoom)/2:y=(ih-ih/zoom)/2:d=120:s=1280x720:fps=30,setsar=1[o0c0v];[1:v]fps=30:start_time=0:round=up,trim=end_frame=1,scale=2560:1440:force_original_aspect_ratio=increase,crop=2560:1440,zoompan=z=1.2:x=(iw-iw/zoom)*on/119:y=(ih-ih/zoom)/2:d=120:s=1280x720:fps=30,setsar=1[o0c1v];[o0c0v][o0c1v]concat=n=2:v=1:a=0[o0cv]" \
-map "[o0cv]" \
-c:v libx264 -crf 23 -pix_fmt yuv420p -an \
-movflags +faststart -preset:v veryfast \
video.mp4 The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
curl "$VIDEXO_API_URL/v1/scenes" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"threshold": 12
}' 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.scenes({
"input": "https://videxo.io/samples/talk.mp4",
"threshold": 12
});
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/scenes`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIDEXO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"input": "https://videxo.io/samples/talk.mp4",
"threshold": 12
}),
});
const job = await response.json(); ffmpeg \
-i https://videxo.io/samples/talk.mp4 \
-map 0:v:0 \
-vf scdet=threshold=12,settb=1/1000000,metadata=mode=print:key=lavfi.scd.score:file=scenes.scores.txt \
-f null \
- The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
curl "$VIDEXO_API_URL/v1/transcribe" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://videxo.io/samples/talk.mp4",
"language": "en"
}' 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.transcribe({
"input": "https://videxo.io/samples/talk.mp4",
"language": "en"
});
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/transcribe`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIDEXO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"input": "https://videxo.io/samples/talk.mp4",
"language": "en"
}),
});
const job = await response.json(); The answer is the job, queued, with its id.
curl "$VIDEXO_API_URL/v1/jobs" \
-H "Authorization: Bearer $VIDEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"talk": "https://videxo.io/samples/talk.mp4"
},
"outputs": [
{
"name": "web",
"preset": "web-720p"
},
{
"name": "poster",
"preset": "poster"
}
],
"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({
"inputs": {
"talk": "https://videxo.io/samples/talk.mp4"
},
"outputs": [
{
"name": "web",
"preset": "web-720p"
},
{
"name": "poster",
"preset": "poster"
}
],
"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({
"inputs": {
"talk": "https://videxo.io/samples/talk.mp4"
},
"outputs": [
{
"name": "web",
"preset": "web-720p"
},
{
"name": "poster",
"preset": "poster"
}
],
"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 -crf 23 -pix_fmt yuv420p \
-map "0:a:0?" \
-c:a aac -b:a 128k -movflags +faststart -preset:v veryfast \
web.mp4 \
-map 0:v:0 \
-vf "thumbnail=300,scale='min(iw,1280)':'min(ih,720)':force_original_aspect_ratio=decrease:force_divisible_by=2" \
-frames:v 1 -q:v 2 \
poster.jpg The answer is the job, queued, with its id. The ffmpeg tab is the command this request runs, as the job returns it in ffmpeg_command.
What it does
Every video job, one API. Each with a request that runs.
Transcode and compress
Compress video, convert between H.264, H.265, VP9 and AV1, resize and set the frame rate.
- Compress and transcode
- WebM for browsers
- Renditions: several sizes in one job
- Change the frame rate
- Adaptive streaming (HLS) and a player Soon
- AI upscale Soon
Apps that store what users upload, course and media platforms, and anyone paying for bandwidth.
Social formats
Crop to 9:16 and 1:1, letterbox into a new frame, rotate and flip.
- Vertical 9:16
- Square 1:1
- Pad and letterbox
- Rotate and flip
Social tools, creator apps, marketing teams and anyone republishing long video as short clips.
Clips and trims
Trim clips by time, join files, change the speed, fade in and out, and find the scenes of a video.
- Trim and clip
- Join files
- Slideshows and image to video
- Transitions between parts
- Title cards
- Speed up or slow down
- Scene detection
- Fade in and out
- Scene thumbnails and chapters Soon
Editors in the browser, highlight and clipping tools, course platforms, meeting recorders.
Watermarks and overlays
Overlay logos, watermarks, titles and captions, and place one video over another as picture in picture.
- Watermark or logo
- Picture in picture
- Text and titles
- Burned-in subtitles
Products that export video for their customers, stock and preview libraries, course and tutorial makers.
Colour and effects
Correct colour with eq and hue, blur a region for privacy, sharpen after scaling.
- Colour correction
- Blur a region
- Sharpen
- AI edit Soon
- Background removal for people Soon
User-generated video platforms, support and research teams sharing recordings, media teams.
Thumbnails and GIFs
Pick thumbnails, lay out storyboards and contact sheets, and make GIF previews with a fitted palette.
- Thumbnails
- WebP, AVIF and smaller images
- Storyboards and contact sheets
- GIF previews
- Storyboards with WebVTT Soon
Video libraries, players, feeds, chat apps and anyone showing video in a list.
Audio
Extract and convert audio, normalise loudness, mix background music and draw waveforms.
- Transcripts and caption files
- Extract and convert audio
- Loudness normalisation
- Background music
- Waveform and spectrum video
- Waveform image
- Silence removal Soon
Podcast and course platforms, meeting recorders, social tools and transcription pipelines.
Your own command
Already know ffmpeg? Send the command you use, with 49 filters, the common codecs and formats and the options that go with them.
-i {{in_video}} -filter_complex "…" {{out_main}}
Coming to the platform
Planned and not available yet. What works today is everything above.
- Signed playback Soon
- Viewer analytics Soon
Who it is for
Built for teams that ship video. Not for teams that run servers.
Mobile and web apps
Compress what users upload, make posters and previews, cut vertical versions. Your backend sends the job; the phone does not encode.
SaaS products
Exports, branded clips, thumbnails and GIFs inside your product, from one API call per job, without a media team.
Media teams
The batch work that used to run on a workstation: loudness to a standard, formats for each channel, contact sheets for review.
AI agents
An assistant connects to the MCP server with your API key, then builds and validates requests, runs jobs, waits for them and reads usage as tools.
How it works
One request in. Files and a webhook out.
- 01
Send
POST /v1/transcodeJSON with the input URL and a preset or a few options, from curl or any HTTP client. The answer is the job, queued, with its id.
- 02
Run
GET /v1/jobs/{id}The request becomes one ffmpeg command and runs. Ask for the job when you like, or wait for the webhook.
- 03
Collect
job.succeededOutputs include signed download links, size, SHA-256 and expiry. The webhook carries the whole job, signed.
The job is accepted and waits for a slot; your plan sets how many run at once. ffmpeg_command is the command it will run.
{
"id": "job_3k9x0c2mfq8w1n7r5t4b6v2z",
"state": "queued",
"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,
"speed": "standard",
"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 -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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast 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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast talk-720p.mp4"
}
],
"notices": [],
"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
}Processing has started; started_at says since when.
{
"id": "job_3k9x0c2mfq8w1n7r5t4b6v2z",
"state": "running",
"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,
"speed": "standard",
"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 -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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast 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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast talk-720p.mp4"
}
],
"notices": [],
"webhook_url": null,
"metadata": {
"order": "1234"
},
"progress": null,
"error": null,
"outputs": [],
"usage": null,
"created_at": "2026-11-04T12:00:00.000Z",
"started_at": "2026-11-04T12:00:04.000Z",
"finished_at": null
}The outputs are stored with their size, SHA-256 and expiry; usage says what the job cost in credits.
{
"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,
"speed": "standard",
"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 -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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast 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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast talk-720p.mp4"
}
],
"notices": [],
"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": 52.5,
"bill": {
"lines": [
{
"kind": "video",
"seconds": 300,
"per_minute": 6,
"multiplier": 1.25,
"credits": 37.5
}
],
"data": {
"input_bytes": 1200000000,
"output_bytes": 300000000,
"credits": 15
},
"processing": {
"seconds": 61,
"included_seconds": 900,
"per_minute": 10,
"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"
}error carries a documented code and a sentence that says what to change.
{
"id": "job_3k9x0c2mfq8w1n7r5t4b6v2z",
"state": "failed",
"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,
"speed": "standard",
"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 -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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast 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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast talk-720p.mp4"
}
],
"notices": [],
"webhook_url": null,
"metadata": {
"order": "1234"
},
"progress": null,
"error": {
"code": "input_unreachable",
"reason": "http_status",
"message": "The server of the URL answered with an error.",
"retryable": false,
"log_tail": null
},
"outputs": [],
"usage": {
"seconds": 0,
"input_bytes": 0,
"output_bytes": 0,
"credits": 0,
"bill": null
},
"created_at": "2026-11-04T12:00:00.000Z",
"started_at": "2026-11-04T12:00:04.000Z",
"finished_at": "2026-11-04T12:00:05.000Z"
}Webhook body the job when it ends, signed
{
"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,
"speed": "standard",
"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 -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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast 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 -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart -preset:v veryfast talk-720p.mp4"
}
],
"notices": [],
"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": 52.5,
"bill": {
"lines": [
{
"kind": "video",
"seconds": 300,
"per_minute": 6,
"multiplier": 1.25,
"credits": 37.5
}
],
"data": {
"input_bytes": 1200000000,
"output_bytes": 300000000,
"credits": 15
},
"processing": {
"seconds": 61,
"included_seconds": 900,
"per_minute": 10,
"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"
}
}ffmpeg when you want it
Already have commands? Send them as they are.
Name the files with aliases such as {{in_video}} and keep every other argument.
Every JSON job returns the command it runs, so you can move one job at a time in either direction.
- Outputs
- .mp4 .mov .m4a .mkv .webm .ts .mp3 .aac .wav .flac .ogg .opus .gif .png .jpg .jpeg
- 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 copy
- 49 filters, among them
- scale crop trim overlay concat fps loudnorm fade
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
The same command as a job: the input and output files named by alias, the rest unchanged.
Pricing
Pay for minutes of output. Not for idle servers.
15 credits a minute of 1080p H.264 + 10 credits a GB
Minutes of output by quality. GB of inputs as fetched plus outputs as stored. The example job reads a 1.2 GB input and stores 5 minutes of 720p, 0.3 GB: 37.5 + 15 = 52.5 credits.
Pay as you go
$0a month
0 credits
3 jobs at once
Starter
$21a month
30,000 credits
3 jobs at once
Pro
$65a month
100,000 credits
10 jobs at once
Scale
$300a month
500,000 credits
30 jobs at once
Questions, answered.
Do I need to know ffmpeg?
No. Describe each output in JSON with a preset such as web-720p or options such as resize and trim, with curl or any HTTP client. The job returns the ffmpeg command it runs as ffmpeg_command, and commands you already have can be sent as they are.
Is there an SDK?
Yes, for TypeScript and JavaScript: install @videxo/sdk from npm. It runs on Node 20 or newer, Bun, Deno and Cloudflare Workers, retries what is safe to retry and verifies webhooks. From any other language, call the HTTP API with any HTTP client: the quickstart, the API reference and the integration examples show every request.
Which ffmpeg runs my jobs?
ffmpeg 6.1.1, with 19 codecs, 49 filters and the output formats .mp4 .mov .m4a .mkv .webm .ts .mp3 .aac .wav .flac .ogg .opus .gif .png .jpg .jpeg. The supported commands page lists every option, codec, format and filter.
Where do inputs come from, and when do outputs expire?
Inputs are public http or https URLs, or private uploads up to your plan’s input limit: up to 100 MB in one request, larger files in resumable parts. Each output is stored with an expires_at set by your plan’s retention: 7 days on Pay as you go, 7 days on Starter, 30 days on Pro, 30 days on Scale.
Download links last up to one hour; read the job again for a fresh one. Files are unavailable after expires_at, so download what you want to keep before then. DELETE /v1/jobs/{id}/files removes stored outputs earlier.
What happens when a job fails?
It ends failed with a documented error code and a sentence that says what to change, and your webhook gets job.failed. The error codes page lists every code with what to do.
Can I try it for free?
Yes. Pay as you go needs no card, and your personal organization gets 500 free credits once, valid 30 days. When they are used, new jobs are refused with out_of_credits.