Assistant
Ask what you want to do — "how do I downscale a video from 1080p to 480p" — and you get the endpoint, the full request, and an explanation of every option used.
Composing an answer
Every job takes inputs — an uploaded key or an external http(s) URL — and returns a job id you poll until it lands. Your ffmpeg options pass through verbatim.
/api/ffmpeg/job/
Queues an ffmpeg operation over one or more inputs (already-uploaded files or external URLs) and produces one or more outputs. Processing runs async on a separate worker — this response only confirms the job was queued, not the final result (see GET /api/ffmpeg/job/{job_id}/ to check on it).
{
"global_options": [
"-y"
],
"inputs": [
{
"input_source": "https://example.com/input.mp4",
"options": [
"-t",
"30"
]
},
{
"input_source": "uploads/8f3a2b1c-logo.png",
"options": [
"-loop",
"1"
]
}
],
"outputs": [
{
"filename": "output.mp4",
"options": [
"-filter_complex",
"[0:v][1:v]overlay=10:10",
"-c:v",
"libx264",
"-crf",
"23"
]
},
{
"filename": "thumbnail.jpg",
"options": [
"-vf",
"scale=320:-1",
"-frames:v",
"1"
]
}
]
}
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "pending"
}
Ask what you want to do — "how do I downscale a video from 1080p to 480p" — and you get the endpoint, the full request, and an explanation of every option used.
Composing an answer
Code example
Original inputs
Result
/api/ffmpeg/job/{job_id}/
Checks the status of a job created by POST /api/ffmpeg/job/ — poll this until status reaches a terminal value ("success", "failed" or "timeout"); before that it's "pending", "download", "running" or "upload" depending on which stage of the pipeline it's in. A job id that doesn't exist, or belongs to someone else, gets the same 404 either way — never leaks which one it was.
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "success",
"created_at": "2026-08-22T10:00:00Z",
"started_at": "2026-08-22T10:00:05Z",
"finished_at": "2026-08-22T10:00:42Z",
"output_bytes": 5242880,
"outputs": [
{
"filename": "output.mp4",
"size": 5242880,
"url": "https://s3.example.com/outputs/42/output.mp4?X-Amz-Signature=..."
}
]
}
Code example
(no request body — GET request)
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "success",
"created_at": "2026-08-22T10:00:00Z",
"started_at": "2026-08-22T10:00:05Z",
"finished_at": "2026-08-22T10:00:42Z",
"output_bytes": 5242880,
"outputs": [
{
"filename": "output.mp4",
"size": 5242880,
"url": "https://s3.example.com/outputs/42/output.mp4?X-Amz-Signature=..."
}
]
}
/api/ffprobe/job/
Runs ffprobe over an already-uploaded file or an external URL and returns its metadata (container format, duration, codecs, one entry per stream) — unlike /api/ffmpeg/job/, it doesn't produce any output file, it only inspects the one it's given. Answers in one of two ways, and your client has to handle both: 200 with the result in the body (an ordinary metadata probe, which takes tens of milliseconds), or 202 with a job_id when it's taking longer — then poll GET /api/ffprobe/job/{job_id}/, the same contract as POST /api/ffmpeg/job/. You hit the 202 path when the probe has real work to do: -count_frames over a long video, or an external URL that's slow to download.
{
"input_source": "https://lorem.video/720p",
"options": [
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height,r_frame_rate"
]
}
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "success",
"created_at": "2026-08-21T12:00:00Z",
"started_at": "2026-08-21T12:00:01Z",
"finished_at": "2026-08-21T12:00:03Z",
"format": {},
"streams": [
{
"width": 1280,
"height": 720,
"r_frame_rate": "30/1"
}
]
}
Code example
{
"input_source": "https://lorem.video/720p",
"options": [
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height,r_frame_rate"
]
}
Original input
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "success",
"created_at": "2026-08-21T12:00:00Z",
"started_at": "2026-08-21T12:00:01Z",
"finished_at": "2026-08-21T12:00:03Z",
"format": {},
"streams": [
{
"width": 1280,
"height": 720,
"r_frame_rate": "30/1"
}
]
}
/api/ffprobe/job/{job_id}/
Checks the status of a probe that came back as 202 — poll this until status reaches a terminal value ("success", "failed" or "timeout"). A probe that answered 200 is already done and needs no polling. A job id that doesn't exist, or belongs to someone else, gets the same 404 either way — never leaks which one it was.
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "success",
"created_at": "2026-08-21T12:00:00Z",
"started_at": "2026-08-21T12:00:01Z",
"finished_at": "2026-08-21T12:01:27Z",
"format": {
"filename": "pipe:0",
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"duration": "600.000000"
},
"streams": [
{
"index": 0,
"codec_type": "video",
"nb_read_frames": "36000"
}
]
}
Code example
(no request body — GET request)
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "success",
"created_at": "2026-08-21T12:00:00Z",
"started_at": "2026-08-21T12:00:01Z",
"finished_at": "2026-08-21T12:01:27Z",
"format": {
"filename": "pipe:0",
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"duration": "600.000000"
},
"streams": [
{
"index": 0,
"codec_type": "video",
"nb_read_frames": "36000"
}
]
}
/api/files/upload/
Uploads a file (typically a video/audio you'll want to process later) and stores it under your account. Returns a storage key you can pass as the input_source of /api/ffmpeg/job/ or /api/ffprobe/job/ instead of an external URL, plus a signed URL to download the same file directly.
{
"file": "video.mp4"
}
{
"key": "inputs/42/3fa85f64b8e94b0b9c1e2a7d6f5c4b3a_video.mp4",
"url": "https://s3.example.com/inputs/42/3fa85f64b8e94b0b9c1e2a7d6f5c4b3a_video.mp4?X-Amz-Signature=..."
}
Code example
{
"file": "video.mp4"
}
{
"key": "inputs/42/3fa85f64b8e94b0b9c1e2a7d6f5c4b3a_video.mp4",
"url": "https://s3.example.com/inputs/42/3fa85f64b8e94b0b9c1e2a7d6f5c4b3a_video.mp4?X-Amz-Signature=..."
}
/api/files/read/{key}/
Returns a fresh signed, temporary URL to download a file you already own. The URL that POST /api/files/upload/ hands back expires after 15 minutes — this is how you get another one without re-uploading. A key that doesn't exist, or belongs to someone else, gets the same 404 either way — never leaks which one it was.
{
"key": "inputs/42/3fa85f64b8e94b0b9c1e2a7d6f5c4b3a_video.mp4",
"url": "https://s3.example.com/inputs/42/3fa85f64b8e94b0b9c1e2a7d6f5c4b3a_video.mp4?X-Amz-Signature=..."
}
Code example
(no request body — GET request)
{
"key": "inputs/42/3fa85f64b8e94b0b9c1e2a7d6f5c4b3a_video.mp4",
"url": "https://s3.example.com/inputs/42/3fa85f64b8e94b0b9c1e2a7d6f5c4b3a_video.mp4?X-Amz-Signature=..."
}