DocsGet started
Jobs and their states
How a job moves from queued to its end, what each state carries, and how to list, cancel and clean up jobs.
A job is created queued, may become running, and ends succeeded, failed or cancelled. An
ended job never changes state again.
queued ──> running ──> succeeded
│ │ └──> failed
└──────────┴──────> cancelledCopy failed. The text is selected; copy it with your keyboard.
States#
| State | What it means | What the job carries |
|---|---|---|
queued | The job is accepted and waiting to start. Your plan sets how many jobs run at once. | progress, error, usage, started_at and finished_at are null. |
running | Processing has started. | started_at records the start time. progress is null when completion progress is unavailable. |
succeeded | ffmpeg finished and the outputs were stored. | outputs, usage, finished_at. |
failed | The job ended with an error. | error.code and error.message, usage, finished_at. |
cancelled | You cancelled it before it ended. | finished_at. |
A job that waits 24 hours without a free slot fails with service_unavailable (reason
queue_timeout).
The request and the command#
Every job carries both sides of what it runs, whichever way you described it:
kindisoperationsfor a JSON request andcommandfor an ffmpeg command body;operationnames the per-operation endpoint that made it (transcode,thumbnails, ...), or isnull.specis a JSON request as the API accepted it, with presets and defaults filled in, ornullfor a command job.command,input_filesandoutput_filesare the command body the job runs. For a JSON job this is the command it compiled to; send it toPOST /v1/jobsand you get the same outputs.ffmpeg_commandis one line that runs the same thing with a local ffmpeg, the aliases replaced by the input URLs and the output file names.stepslists what the job runs, in order: each step'stool, theinputsandoutputsit reads and writes by name, andcommand, the shell line of anffmpegstep ornullfor another tool. A command job has one step,ffmpeg; ametadataoutput adds anffprobestep before it.
A job's command is fixed when it is created; a later change to presets or defaults never changes a job that exists.
queued#
{
"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,
"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": null,
"error": null,
"outputs": [],
"usage": null,
"created_at": "2026-11-04T12:00:00.000Z",
"started_at": null,
"finished_at": null
}Copy failed. The text is selected; copy it with your keyboard.
running#
{
"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,
"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": 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
}Copy failed. The text is selected; copy it with your keyboard.
succeeded#
{
"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"
}Copy failed. The text is selected; copy it with your keyboard.
failed#
{
"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,
"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": 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"
}Copy failed. The text is selected; copy it with your keyboard.
Why a job failed#
error.code is one of these, with a reason for the finer case (Errors). A code
keeps its meaning once it is documented; new codes and reasons may be added.
| Code | Meaning | Reasons |
|---|---|---|
limit_exceeded | A plan or request limit was passed. | request_body_size, upload_size, input_size, output_size, job_duration, low_balance, member_limit, organization_limit, token_uploads, subtitles_size |
usage_cap_reached | The plan's overage cap for this period is reached. | none |
out_of_credits | The prepaid credit balance is at zero. | none |
service_unavailable | We could not complete this in time, after retrying on our side. Quote the request id to support if it repeats. | temporary, cancel_unconfirmed, queue_timeout |
internal_error | Something failed on our side. Quote the request id to support if it repeats. | none |
input_unreachable | An input could not be read. | url_refused, http_status, network, redirects, too_slow, upload_expired |
processing_failed | The media could not be processed with these settings. | ffmpeg_error, output_empty, time_past_end, subtitles_invalid, media_unreadable |
Before a job fails we try again what can pass on its own: an input server that answered 429 or
a 5xx, or dropped the connection (3 tries per input), and failures on our side, up to 3 runs of
the job in all. Retries only take time: you pay for the final run, and a job that fails with
internal_error or service_unavailable uses no credits. A job succeeds only when every output
has content: an output that comes out empty fails the job with processing_failed (reason
output_empty). For a JSON request, a frame time or a trim start past the end of an input fails
the job before ffmpeg runs (reason time_past_end); only the bytes fetched count. So does a
subtitles file that is not UTF-8 SRT or WebVTT (processing_failed, reason subtitles_invalid) or
is larger than 1 MB (limit_exceeded, reason subtitles_size).
An input ffprobe cannot read for a metadata output fails the job before
ffmpeg runs too (reason media_unreadable).
Outputs and retention#
Each output lists its alias, file name, size in bytes, SHA-256, content type, expires_at,
download_url and download_url_expires_at. See Files for downloads and private
input uploads. Your plan's output retention sets the expiry (Limits per plan).
Files are unavailable after expires_at, so download the files you want to keep before then.
To remove a job's files earlier, call DELETE /v1/jobs/{id}/files; it sets deleted_at and
deletes the files. For a job that is still queued or running it returns 409 conflict (reason
job_active).
Usage and credits#
A job that ended reports usage: the seconds of ffmpeg and ffprobe, the input and output bytes, and
the credits it used. Credits are the minutes of each output by its quality, plus
10 per GB of inputs and outputs, plus processing past the time included in the
output. Limits per plan has the rates and a worked example.
Listing jobs#
GET /v1/jobs returns your jobs newest first, 20 a page by default and up to
100 with limit. Filter by one state, and by creation time: created_after is inclusive,
created_before exclusive, both ISO 8601 UTC (2026-11-04T00:00:00Z); any other value answers
400 invalid_request. When next_cursor is not null, pass it as cursor for the next page, with
the same filters.
curl "$VIDEXO_API_URL/v1/jobs?limit=20&state=failed" \
-H "Authorization: Bearer $VIDEXO_API_KEY"Copy failed. The text is selected; copy it with your keyboard.
const response = await fetch(`${process.env.VIDEXO_API_URL}/v1/jobs?limit=20&state=failed`, {
headers: { Authorization: `Bearer ${process.env.VIDEXO_API_KEY}` },
});
const page = await response.json();Copy failed. The text is selected; copy it with your keyboard.
Cancelling a job#
POST /v1/jobs/{id}/cancel cancels a queued or running job. For a job that already ended it
returns the job as it is, so cancelling twice is safe. If cancellation cannot be confirmed, the API
returns 503 service_unavailable (reason cancel_unconfirmed): read the latest job state before
retrying. The job may already have finished or been cancelled.
Metadata#
metadata holds your own string pairs, such as an order number, and comes back with the job: at
most 20 keys of up to 40 characters, values up to
500 characters.