Skip to content

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.

text
queued ──> running ──> succeeded
   │          │   └──> failed
   └──────────┴──────> cancelled

States#

StateWhat it meansWhat the job carries
queuedThe 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.
runningProcessing has started.started_at records the start time. progress is null when completion progress is unavailable.
succeededffmpeg finished and the outputs were stored.outputs, usage, finished_at.
failedThe job ended with an error.error.code and error.message, usage, finished_at.
cancelledYou 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:

  • kind is operations for a JSON request and command for an ffmpeg command body; operation names the per-operation endpoint that made it (transcode, thumbnails, ...), or is null.
  • spec is a JSON request as the API accepted it, with presets and defaults filled in, or null for a command job.
  • command, input_files and output_files are the command body the job runs. For a JSON job this is the command it compiled to; send it to POST /v1/jobs and you get the same outputs.
  • ffmpeg_command is one line that runs the same thing with a local ffmpeg, the aliases replaced by the input URLs and the output file names.
  • steps lists what the job runs, in order: each step's tool, the inputs and outputs it reads and writes by name, and command, the shell line of an ffmpeg step or null for another tool. A command job has one step, ffmpeg; a metadata output adds an ffprobe step 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#

JSON
{
  "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
}

running#

JSON
{
  "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
}

succeeded#

JSON
{
  "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"
}

failed#

JSON
{
  "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"
}

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.

CodeMeaningReasons
limit_exceededA 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_reachedThe plan's overage cap for this period is reached.none
out_of_creditsThe prepaid credit balance is at zero.none
service_unavailableWe 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_errorSomething failed on our side. Quote the request id to support if it repeats.none
input_unreachableAn input could not be read.url_refused, http_status, network, redirects, too_slow, upload_expired
processing_failedThe 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.

bash
curl "$VIDEXO_API_URL/v1/jobs?limit=20&state=failed" \
  -H "Authorization: Bearer $VIDEXO_API_KEY"

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.