On this page
- 2026-11-07
- Added: reason low_balance on limit_exceeded
- Corrected: a cursor on GET /v1/client/jobs
- Added: 503 on every endpoint that creates a job
- Added: the Swift SDK
- Changed: credits are billed per minute of output (breaking)
- Added: created_after and created_before on GET /v1/jobs
- Corrected: what ?wait=false answers on POST /v1/probe
- Added: steps on a job
- Added: title cards
- Added: transitions
- Added: Connected apps
- 2026-11-06
- Added: still images and slideshows
- Fixed: JPEG and BMP inputs given by URL
- Breaking: /mcp needs a credential; OAuth sign-in on the MCP server
- Added: media metadata
- Breaking: command and the fields beside it may be null
- Added: client tokens for apps and browsers
- Added: scene lists and the speed of sound
- 2026-11-05
- Added: burned-in subtitles
- Breaking: Pay as you go replaces Free, credits with one decimal, and GET /v1/credits
- Added: text layers and fonts
- Breaking: x264 and x265 presets up to slow, and JSON jobs encode with veryfast
- Changed: retries before a job fails, empty outputs fail, and ffmpeg's log tail
- Breaking: fifteen error codes with reasons, and request ids
- Fixed: bitrate caps on VP9 and AV1, and outputs that ran past their picture
- Changed: web presets no longer enlarge small inputs
- Breaking: errors are problem details
- New: jobs described in JSON
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_exceededand reasonlow_balance. Itsmessagesays how many seconds the credits allowed and how to get more. These jobs used to sayjob_duration, which now means the plan's own limit. What the job pays is unchanged. If you branch onjob_durationto shorten a job, branch onlow_balanceto 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
cursoryou sent. It now answers400 invalid_request, witherrorspointing at thecursorparameter. 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(reasontemporary) withRetry-After, and no job. It used to get402 out_of_creditsin that moment. Send the request again after the wait;402 out_of_creditsstill means no credits are coming.
Added: the Swift SDK#
- A Swift package for iOS and macOS,
videxo-swift0.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.creditscan now hold a decimal (13.5). Read it as a number; do not assume it is whole.usage.secondsstays ffmpeg's time, and no longer sets the price by itself. If you estimate a job's cost from it, useusage.bill.- Added:
usage.bill, what the credits are made of:lines(per stored output, and per video stream of an output with several),dataandprocessing. It isnullon 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_creditsorusage_cap_reachedat 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_jobstool does:created_afteris inclusive,created_beforeexclusive, 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 answers202while the job is queued or running, and200if the job has already ended. Nothing changed in the API.
Added: steps on a job#
- Every job and
POST /v1/jobs/compilehavesteps: what the job runs, in order, with each step'stool, itsinputsandoutputsby name, andcommandfor anffmpegstep (The request and the command).command,input_files,output_filesandffmpeg_commandkeep their meaning. - A body whose outputs would not fit the disk of one run is refused:
400 invalid_requestfor a JSON body,command_invalidfor 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
concatpart onPOST /v1/jobsmay 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
concatpart may cross with the part before:"transition": { "type", "duration" }, picture and sound, with 17 types (Join files).POST /v1/slideshowtakestransitionfor every image, and each image may set its own. Sound-only outputs refuse it. - A command may use the filters
xfade, with a fixedtransition, andacrossfade(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.authorizedandapp.revoked.
2026-11-06#
Added: still images and slideshows#
- A
concatpart may be a still image:{ "image", "duration", "motion", "text" }, held for 0.1 to 300 seconds, with an optional zoom or pan. OnPOST /v1/jobs,concatnow takes 1 to 16 parts, so one image part is an image to video (Slideshows). POST /v1/slideshow, the SDK'sslideshow()and the playground's slideshow form.audio.inputworks withconcaton 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 onelavfisource allowed (Commands).
Fixed: JPEG and BMP inputs given by URL#
- An input whose URL ended in
.jpg,.jpegor.bmpfailed withprocessing_failed, whatever it held. It now runs. A job'sinput_filesand commands do not change.
Breaking: /mcp needs a credential; OAuth sign-in on the MCP server#
/mcpwithout anAuthorizationheader now answers401, with aWWW-Authenticatechallenge, 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_medianeeds the scopejobs:runwhen called with an OAuth token. - New reasons:
authentication_failedmissing_credentials,invalid_token,token_expiredandtoken_revoked;permission_deniedinsufficient_scope, androle_insufficientnow also on MCP (Errors). A malformed credential on/mcpnow readsinvalid_token, notinvalid_api_key.
Added: media metadata#
POST /v1/probereads the container, duration and streams of a file with ffprobe, and themetadataoutput type does the same inside any job (Read media metadata).- The SDK's
probe()and the MCP toolprobe_media. processing_failedhas a new reason,media_unreadable.
Breaking: command and the fields beside it may be null#
command,input_files,output_filesandffmpeg_commandon a job and onPOST /v1/jobs/compilearenullwhen every output ismetadata, since no ffmpeg runs. In a job with other outputs they describe the ffmpeg part, without themetadataoutputs. Check fornullif you read these fields on jobs that can havemetadataoutputs.
Added: client tokens for apps and browsers#
POST /v1/client-tokensmints 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_uploadsandclient_token(Errors). - Signed download links answer CORS for any origin, so a web app can
fetchan output.
Added: scene lists and the speed of sound#
- A
scenesoutput andPOST /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.jsonoutput andscdet(Command rules). speednow changes the sound too, keeping its pitch, onvideoandaudiooutputs. Avideooutput withspeedno 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
Outputtype andoutput_extensionsgainscenesand.json; ignore values you do not know.
2026-11-05#
Added: burned-in subtitles#
subtitlesonvideooutputs 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
subtitlesfilter with the file as an input alias (Command rules). - A job's
errormay carry the new reasonssubtitles_invalid(processing_failed) andsubtitles_size(limit_exceeded).
Breaking: Pay as you go replaces Free, credits with one decimal, and GET /v1/credits#
- The plan
freeis gone: an account without a subscription is on Pay as you go (payginGET /v1/meand MCP). It has no monthly credits; your personal organization gets 500 free credits once, valid 30 days, after your email is verified. Readplanas one ofpayg,starter,proandscale. - Credits are counted to one decimal: a job's
usage.creditsand the amounts ofGET /v1/creditsmay 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'sget_usageanswers the same.- When the account's credits leave no room for a job,
POST /v1/jobsanswers402 out_of_credits;usage_cap_reachedstays for a subscriber at the cycle's overage cap.
Added: text layers and fonts#
textonvideo,gifandimageoutputs, 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/fontsand the MCP toollist_fontslist the fonts, without an API key (Fonts).- Commands may use
drawtextwith literal text and a curated font file (Command rules); its quotedtextis 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) takesultrafasttoslow.slower,veryslowandplaceboare refused withcommand_invalid(option_value), and the message lists the presets you can use. Useslowor a faster preset, or lower-crffor a smaller file at the same speed.video.encoder_presettakesultrafasttoslow. An H.264 or H.265 output withoutencoder_presetnow encodes withveryfastinstead ofmedium, and so do the presetsweb-720p,web-1080p,social-verticalandsquare. The jobs finish sooner; at the same CRF the file size and the picture differ slightly frommedium. 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
429or a5xxor dropped the connection, and failures on our side. You pay for the final run only, and a job that fails withinternal_errororservice_unavailableuses no credits. - An output that comes out empty, or not at all, fails the job with
processing_failed(reasonoutput_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(reasontime_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
reasonfor the finer case: Errors. Statuses did not change. Branch on the newcode, and onreasonwhere you need the finer case; the Errors page lists every earlier code and what answers now (job_not_foundisnot_foundwith reasonjob,idempotency_conflictisconflictwith reasonidempotency_key_reused, and so on). - A failed job's
errorgainsreason,retryableandlog_tail; its codes change the same way (ffmpeg_failedisprocessing_failedwith reasonffmpeg_error,time_limitislimit_exceededwith reasonjob_duration). A cancelled job haserror: null. - A body over the size limit is
413 limit_exceeded(reasonrequest_body_size), no longerinvalid_request. - A cancel that could not be confirmed is
503 service_unavailablewith reasoncancel_unconfirmed: read the job before cancelling again. - Every answer carries
Request-Id, and problem details carryrequest_idandretryable. GET /healthanswers{ "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_bitratefailed 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.mixoraudio.codec: "copy"together withaudio.inputnaming another input is now refused with400 invalid_request(ataudio/mixoraudio/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:
| Before | Now |
|---|---|
error.code | code |
error.message | title (the code's sentence); detail says what happened this time |
error.issues[].path, such as body.input_files.in_video | errors[].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[].message | errors[].detail |
error.problems | problems, 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/jobsalso takes an operations body: namedinputsand a list ofoutputs, each an output type with options or a preset. A body with the fields of both kinds, or of neither, is400 invalid_request.- Per-operation endpoints create the same job from one input:
POST /v1/transcode,/v1/thumbnails,/v1/storyboard,/v1/gif,/v1/audioand/v1/concat. POST /v1/jobs/compileanswers the command a body compiles to, without making a job or using credits;GET /v1/presetslists the presets.- Every job has the new fields
kind,operation,specandffmpeg_command, and each output aname. The fields a job had keep their meaning. - An
Idempotency-Keynow remembers the endpoint as well as the body: the same key on another endpoint is409 idempotency_conflict(Idempotency). Keys sent toPOST /v1/jobsbefore this change still answer their retries. - MCP:
create_jobtakes either body; new toolscompile_jobandlist_presets; new promptbuild_video_job.
The API reference lists every field of these requests.