Skip to content

DocsReference

API changelog

Changes to the API during the preview, newest first, with what to change in your code for a breaking one.

The API is in preview: until launch a change may break a client, and every such change is listed here with what to do. Changes to the options of an existing preset are labelled Changed. After launch /v1 only grows: new endpoints, optional request fields, response fields, error codes and enum values, so read responses in a way that ignores fields you do not know (Versioning).

2026-11-07#

Added: reason low_balance on limit_exceeded#

  • A job whose time limit was shortened to the credits left, and that reaches it, now fails with limit_exceeded and reason low_balance. Its message says how many seconds the credits allowed and how to get more. These jobs used to say job_duration, which now means the plan's own limit. What the job pays is unchanged. If you branch on job_duration to shorten a job, branch on low_balance to add credits.

Corrected: a cursor on GET /v1/client/jobs#

  • This list has one page and writes no cursor, and it used to ignore a cursor you sent. It now answers 400 invalid_request, with errors pointing at the cursor parameter. Remove the parameter from the request.

Added: 503 on every endpoint that creates a job#

  • A new account whose free credits are still being granted gets 503 service_unavailable (reason temporary) with Retry-After, and no job. It used to get 402 out_of_credits in that moment. Send the request again after the wait; 402 out_of_credits still means no credits are coming.

Added: the Swift SDK#

  • A Swift package for iOS and macOS, videxo-swift 0.1.0, for uploads and job status through a client token or a one-time upload URL. See Swift SDK.

Changed: credits are billed per minute of output (breaking)#

  • A job's credits no longer come from ffmpeg's seconds. It pays for its stored outputs by the minute at their quality, for its data, and for processing past 3 times its output. See Pricing.
  • usage.credits can now hold a decimal (13.5). Read it as a number; do not assume it is whole.
  • usage.seconds stays ffmpeg's time, and no longer sets the price by itself. If you estimate a job's cost from it, use usage.bill.
  • Added: usage.bill, what the credits are made of: lines (per stored output, and per video stream of an output with several), data and processing. It is null on a job from before this change and on a job that used nothing.
  • What a job holds while it runs is different, and it can grow. At the start it holds its data limits, its longest run as processing, and a minute of each output at the quality it asks for, or at the lowest video rate for what it does not name. Once its inputs are fetched it holds its estimate from them. It can end out_of_credits or usage_cap_reached at that point, paying only the data it fetched, or later, while it runs or before its outputs are stored, when more cannot be held: that pays as a failure after ffmpeg ran, at most what it held, and stores nothing.

Added: created_after and created_before on GET /v1/jobs#

  • The list filters by creation time, as the MCP list_jobs tool does: created_after is inclusive, created_before exclusive, both ISO 8601 UTC. See Listing jobs.
  • Changed: these two parameters used to be ignored, so a request with them got every job. If you send them, you now get only the jobs in that window, and any value that is not an ISO 8601 UTC time answers 400 invalid_request.

Corrected: what ?wait=false answers on POST /v1/probe#

  • The docs said it always answers 202. It answers 202 while the job is queued or running, and 200 if the job has already ended. Nothing changed in the API.

Added: steps on a job#

  • Every job and POST /v1/jobs/compile have steps: what the job runs, in order, with each step's tool, its inputs and outputs by name, and command for an ffmpeg step (The request and the command). command, input_files, output_files and ffmpeg_command keep their meaning.
  • A body whose outputs would not fit the disk of one run is refused: 400 invalid_request for a JSON body, command_invalid for a command. Only many scene lists beside a large output can reach this on the largest plan, and splitting the job avoids it.
  • A step that leaves a file it did not declare ends with processing_failed.

Added: title cards#

  • A concat part on POST /v1/jobs may be a title card: { "color", "duration", "text", "transition" } (Join files).
  • A command may use a solid colour as an input: -f lavfi -i color=c=0x101828:s=1920x1080:r=30:d=3 (Commands).

Added: transitions#

  • A concat part may cross with the part before: "transition": { "type", "duration" }, picture and sound, with 17 types (Join files). POST /v1/slideshow takes transition for every image, and each image may set its own. Sound-only outputs refuse it.
  • A command may use the filters xfade, with a fixed transition, and acrossfade (Commands).

Added: Connected apps#

  • Settings, Connected apps lists the apps that signed in to the MCP server with OAuth: the app, what it may do, who connected it, and when it was authorized and last used. Each member sees and revokes their own; Owners and Admins see and revoke every member's. A revoked app's next request answers 401 (MCP server).
  • The audit log records app.authorized and app.revoked.

2026-11-06#

Added: still images and slideshows#

  • A concat part may be a still image: { "image", "duration", "motion", "text" }, held for 0.1 to 300 seconds, with an optional zoom or pan. On POST /v1/jobs, concat now takes 1 to 16 parts, so one image part is an image to video (Slideshows).
  • POST /v1/slideshow, the SDK's slideshow() and the playground's slideshow form.
  • audio.input works with concat on a video output: it replaces the parts' sound and ends with the picture. It used to be refused.
  • A command may use silence as an input: -f lavfi -i anullsrc=r=48000:cl=stereo:d=4, the one lavfi source allowed (Commands).

Fixed: JPEG and BMP inputs given by URL#

  • An input whose URL ended in .jpg, .jpeg or .bmp failed with processing_failed, whatever it held. It now runs. A job's input_files and commands do not change.

Breaking: /mcp needs a credential; OAuth sign-in on the MCP server#

  • /mcp without an Authorization header now answers 401, with a WWW-Authenticate challenge, instead of the public tools. The tools that need no account moved to /mcp/public: change the URL of a client that connects without a key. Clients with an API key see no change.
  • An MCP client can now sign in with OAuth, for one organization and with what the member's role holds (MCP server). OAuth tokens are not accepted on /v1.
  • The MCP tool probe_media needs the scope jobs:run when called with an OAuth token.
  • New reasons: authentication_failed missing_credentials, invalid_token, token_expired and token_revoked; permission_denied insufficient_scope, and role_insufficient now also on MCP (Errors). A malformed credential on /mcp now reads invalid_token, not invalid_api_key.

Added: media metadata#

  • POST /v1/probe reads the container, duration and streams of a file with ffprobe, and the metadata output type does the same inside any job (Read media metadata).
  • The SDK's probe() and the MCP tool probe_media.
  • processing_failed has a new reason, media_unreadable.

Breaking: command and the fields beside it may be null#

  • command, input_files, output_files and ffmpeg_command on a job and on POST /v1/jobs/compile are null when every output is metadata, since no ffmpeg runs. In a job with other outputs they describe the ffmpeg part, without the metadata outputs. Check for null if you read these fields on jobs that can have metadata outputs.

Added: client tokens for apps and browsers#

  • POST /v1/client-tokens mints a short-lived token with a narrow grant; /v1/client/* is what an app or a web page calls with it: create uploads, read the jobs you name. Jobs are still created only with your API key (Client tokens).
  • New reasons: missing_client_token, invalid_client_token, client_token_expired, client_token_superseded, client_token_revoked, token_scope, client_token_required, token_uploads and client_token (Errors).
  • Signed download links answer CORS for any origin, so a web app can fetch an output.

Added: scene lists and the speed of sound#

  • A scenes output and POST /v1/scenes: a JSON file with every scene of a video, its start, end and the score of the change that starts it, on the input's clock (Scene detection). Commands make one with a .json output and scdet (Command rules).
  • speed now changes the sound too, keeping its pitch, on video and audio outputs. A video output with speed no longer has to set "audio": false; a request that does keeps working (Speed).
  • Commands may use atempo (a literal factor from 0.5 to 2).
  • The Output type and output_extensions gain scenes and .json; ignore values you do not know.

2026-11-05#

Added: burned-in subtitles#

  • subtitles on video outputs burns the captions of an SRT or WebVTT input into the picture, with a font, size, colour, outline, position and margin (Operations). Cues follow the output's input through trims and speed, and a concat's joined parts.
  • Commands may use the subtitles filter with the file as an input alias (Command rules).
  • A job's error may carry the new reasons subtitles_invalid (processing_failed) and subtitles_size (limit_exceeded).

Breaking: Pay as you go replaces Free, credits with one decimal, and GET /v1/credits#

  • The plan free is gone: an account without a subscription is on Pay as you go (payg in GET /v1/me and MCP). It has no monthly credits; your personal organization gets 500 free credits once, valid 30 days, after your email is verified. Read plan as one of payg, starter, pro and scale.
  • Credits are counted to one decimal: a job's usage.credits and the amounts of GET /v1/credits may carry one (13.5), so read them as numbers, not integers.
  • GET /v1/credits (new) answers the account's credits: the balance, what running jobs hold, the grants with credits left and when they expire, and a subscriber's cycle and overage. MCP's get_usage answers the same.
  • When the account's credits leave no room for a job, POST /v1/jobs answers 402 out_of_credits; usage_cap_reached stays for a subscriber at the cycle's overage cap.

Added: text layers and fonts#

  • text on video, gif and image outputs, and on concat parts: up to 8 text layers (4 on a part) with a font, size, colour, position, box, outline, shadow, times and fades (Operations). The text is drawn exactly as written.
  • GET /v1/fonts and the MCP tool list_fonts list the fonts, without an API key (Fonts).
  • Commands may use drawtext with literal text and a curated font file (Command rules); its quoted text is the one place a command may hold characters beyond ASCII.

Breaking: x264 and x265 presets up to slow, and JSON jobs encode with veryfast#

  • -preset (and -preset:v) takes ultrafast to slow. slower, veryslow and placebo are refused with command_invalid (option_value), and the message lists the presets you can use. Use slow or a faster preset, or lower -crf for a smaller file at the same speed.
  • video.encoder_preset takes ultrafast to slow. An H.264 or H.265 output without encoder_preset now encodes with veryfast instead of medium, and so do the presets web-720p, web-1080p, social-vertical and square. The jobs finish sooner; at the same CRF the file size and the picture differ slightly from medium. Set "encoder_preset": "medium" to keep the earlier encoding.

Changed: retries before a job fails, empty outputs fail, and ffmpeg's log tail#

  • Failures that can pass on their own are tried again before a job fails: an input server that answered 429 or a 5xx or dropped the connection, and failures on our side. You pay for the final run only, and a job that fails with internal_error or service_unavailable uses no credits.
  • An output that comes out empty, or not at all, fails the job with processing_failed (reason output_empty); it used to succeed with a 0-byte file.
  • A JSON job whose frame time or trim start is past the end of an input fails before ffmpeg runs with processing_failed (reason time_past_end), and its message names the time and the input.
  • A job that ffmpeg failed carries error.log_tail: ffmpeg's last lines about your media, without URLs, keys or our own paths.

Breaking: fifteen error codes with reasons, and request ids#

  • The error codes are now a short list, the same for request answers and failed jobs, each with a documented reason for the finer case: Errors. Statuses did not change. Branch on the new code, and on reason where you need the finer case; the Errors page lists every earlier code and what answers now (job_not_found is not_found with reason job, idempotency_conflict is conflict with reason idempotency_key_reused, and so on).
  • A failed job's error gains reason, retryable and log_tail; its codes change the same way (ffmpeg_failed is processing_failed with reason ffmpeg_error, time_limit is limit_exceeded with reason job_duration). A cancelled job has error: null.
  • A body over the size limit is 413 limit_exceeded (reason request_body_size), no longer invalid_request.
  • A cancel that could not be confirmed is 503 service_unavailable with reason cancel_unconfirmed: read the job before cancelling again.
  • Every answer carries Request-Id, and problem details carry request_id and retryable.
  • GET /health answers { "ok": true } only.

Fixed: bitrate caps on VP9 and AV1, and outputs that ran past their picture#

  • A VP9 or AV1 output with video.max_bitrate failed when it ran (ffmpeg_failed): it now compiles to constrained quality, the cap as the target bitrate.
  • An output with a video overlay longer than its main input, or with sound from another, longer input (audio.input), ran to that input's length. It now ends with its main picture; a shorter sound is padded with silence.
  • audio.mix or audio.codec: "copy" together with audio.input naming another input is now refused with 400 invalid_request (at audio/mix or audio/codec), as the output could not end with its picture.

Changed: web presets no longer enlarge small inputs#

resize has a new field, enlarge (default true). With false, an input smaller than the size keeps its own size instead of being scaled up. The presets web-720p, web-1080p, poster and gif-preview now set enlarge: false, so a 640x360 input with web-720p stays 640x360 instead of becoming 1280x720. A GIF without a preset is at most 480 pixels wide and is not enlarged either. social-vertical and square still make their exact sizes. A job made before this change keeps the command it was created with.

Breaking: errors are problem details#

Every error answer is now RFC 9457 problem details with Content-Type: application/problem+json (Error codes). The shape { "error": { "code", "message", "issues" | "problems" } } is gone, in the REST API and in MCP tool errors. To update a client:

BeforeNow
error.codecode
error.messagetitle (the code's sentence); detail says what happened this time
error.issues[].path, such as body.input_files.in_videoerrors[].pointer, such as #/input_files/in_video; errors[].parameter for a query or path parameter (cursor) and errors[].header for a header (Idempotency-Key)
error.issues[].messageerrors[].detail
error.problemsproblems, unchanged

What stays: every code and its HTTP status, Retry-After on 429 and WWW-Authenticate on 401. A failed job's error (code and message inside the job) and webhook payloads are not error answers and do not change.

An unknown field in a body is now one errors entry each, pointing at the field.

New: jobs described in JSON#

  • POST /v1/jobs also takes an operations body: named inputs and a list of outputs, each an output type with options or a preset. A body with the fields of both kinds, or of neither, is 400 invalid_request.
  • Per-operation endpoints create the same job from one input: POST /v1/transcode, /v1/thumbnails, /v1/storyboard, /v1/gif, /v1/audio and /v1/concat.
  • POST /v1/jobs/compile answers the command a body compiles to, without making a job or using credits; GET /v1/presets lists the presets.
  • Every job has the new fields kind, operation, spec and ffmpeg_command, and each output a name. The fields a job had keep their meaning.
  • An Idempotency-Key now remembers the endpoint as well as the body: the same key on another endpoint is 409 idempotency_conflict (Idempotency). Keys sent to POST /v1/jobs before this change still answer their retries.
  • MCP: create_job takes either body; new tools compile_job and list_presets; new prompt build_video_job.

The API reference lists every field of these requests.