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://video-generator-ai.sfo3.cdn.digitaloceanspaces.com/assistant-test-files/video.mp4",
"options": [
"-t",
"30"
]
},
{
"input_source": "inputs/42/brand/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,
"duration_seconds": 36.482,
"billed_seconds": 68.91,
"outputs": [
{
"filename": "output.mp4",
"size": 5242880,
"url": "https://s3.example.com/outputs/42/output.mp4?X-Amz-Signature=..."
}
],
"ffmpeg_version": "7.1"
}
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,
"duration_seconds": 36.482,
"billed_seconds": 68.91,
"outputs": [
{
"filename": "output.mp4",
"size": 5242880,
"url": "https://s3.example.com/outputs/42/output.mp4?X-Amz-Signature=..."
}
],
"ffmpeg_version": "7.1"
}
/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://video-generator-ai.sfo3.cdn.digitaloceanspaces.com/assistant-test-files/video.mp4",
"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",
"duration_seconds": 1.874,
"billed_seconds": 0.041,
"format": {},
"streams": [
{
"width": 1280,
"height": 720,
"r_frame_rate": "30/1"
}
],
"ffmpeg_version": "7.1"
}
Code example
{
"input_source": "https://video-generator-ai.sfo3.cdn.digitaloceanspaces.com/assistant-test-files/video.mp4",
"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",
"duration_seconds": 1.874,
"billed_seconds": 0.041,
"format": {},
"streams": [
{
"width": 1280,
"height": 720,
"r_frame_rate": "30/1"
}
],
"ffmpeg_version": "7.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",
"duration_seconds": 86.231,
"billed_seconds": 85.004,
"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"
}
],
"ffmpeg_version": "7.1"
}
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",
"duration_seconds": 86.231,
"billed_seconds": 85.004,
"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"
}
],
"ffmpeg_version": "7.1"
}
/api/files/upload/sign/
Uploading takes three calls, and the file itself never passes through this API: you ask for a signed URL here, PUT the bytes straight to the bucket, and then confirm. That is what lets you upload a file of several GB — nothing has to fit inside one request to us. Only key is required; if you also know the exact size, declaring it gets it signed into the URL so the storage refuses anything else before a byte is kept. Either way the confirm step measures the file and rejects one over your plan's limit. Try it below runs the three calls for real, with a fixed sample file: the bytes leave your browser straight for the bucket, exactly as they would from your own client.
{
"key": "demo/quickffmpeg-sample.mp4",
"size": 12345678
}
{
"key": "inputs/42/demo/quickffmpeg-sample.mp4",
"method": "PUT",
"url": "https://s3.us-southeast-1.idrivee2.com/quickffmpeg-files/inputs/42/demo/quickffmpeg-sample.mp4?X-Amz-Signature=...",
"headers": {
"Content-Length": "12345678"
},
"expires_in": 900,
"max_bytes": 157286400
}
Code example
{
"key": "demo/quickffmpeg-sample.mp4",
"size": 12345678
}
Runs the three calls for real, always with the same sample file and the same key.
/api/files/upload/confirm/
Registers a file you already uploaded to the bucket, which is what makes its key usable as an input_source. It reads the real size from storage — a file that never arrived gets a 404, and one uploaded but never confirmed is reaped by a daily cleanup, so confirm it in the same run instead of coming back for it tomorrow. Confirming the same key twice is safe: the second time answers 200 instead of 201.
{
"key": "inputs/42/demo/quickffmpeg-sample.mp4"
}
{
"key": "inputs/42/demo/quickffmpeg-sample.mp4",
"size": 12345678,
"url": "https://s3.example.com/inputs/42/demo/quickffmpeg-sample.mp4?X-Amz-Signature=..."
}
Code example
{
"key": "inputs/42/demo/quickffmpeg-sample.mp4"
}
{
"key": "inputs/42/demo/quickffmpeg-sample.mp4",
"size": 12345678,
"url": "https://s3.example.com/inputs/42/demo/quickffmpeg-sample.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/confirm/ 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/demo/quickffmpeg-sample.mp4",
"url": "https://s3.example.com/inputs/42/demo/quickffmpeg-sample.mp4?X-Amz-Signature=..."
}
Code example
(no request body — GET request)
{
"key": "inputs/42/demo/quickffmpeg-sample.mp4",
"url": "https://s3.example.com/inputs/42/demo/quickffmpeg-sample.mp4?X-Amz-Signature=..."
}