POST /v1/media/videos does not wait: it answers 202 with a media job in queued, and you read the job back until it is completed or failed. The finished clip is a file. Images are the synchronous case — see Images. The same job is what an agent’s generate_video tool submits; the catalogue is shared, and it is its own — a video model is never an agent’s model.
The media job object
string
med_….string
Always
media_job.string
video. Other async media kinds share the object and the routes below.string
queued → processing → completed | failed.string | null
The model rendering it.
object
The request as accepted.
object | null
On
completed: { files: [{ file_id, content_type, width, height, duration_seconds }] }. Dimensions are null where the model did not report them.object | null
On
failed: { code, message } — the reason, safe to repeat, so a refusal and an outage read differently.integer | null
What was debited, in integer micro-USD, once the job finished:
null while it runs, 0 on a failure — a job that fails is not billed.string | null
The session whose agent submitted it;
null for a job submitted through this API.string
Submission time.
string | null
When it reached a terminal status.
Generate a video
POST /v1/media/videos → 202 Accepted — scope agents:write, Idempotency-Key required.
string
A model id from
GET /v1/media/models?kind=video. Omitted, the first of the platform’s video defaults the catalogue publishes — black-forest-labs/flux-3-max, minimax/hailuo-3, bytedance/seedance-2.5, then alibaba/wan-3.0/text-to-video; for an edit (video_url), only one the catalogue says takes a video. Otherwise the request is refused and you name one.string
required
What to render.
string[]
One image, used as the first frame the video animates from: a stored image’s
fil_ id — say a still rendered as a character — or a public http(s) URL. A fil_ this organization does not hold is not_found; one that is not an image, a private or loopback host, and a data: or file: URL are validation_failed. The job’s input keeps the fil_ you sent.string
Put a trained character on video, by
chr_ id or name. It must be ready, and image_urls must be one fil_ still this organization rendered as that character — anything else is validation_failed — because the face on screen is the still’s. Only a model whose catalogue entry says it animates a still is used; a named one that does not is validation_failed. The character is recorded on the job, never sent to the model.integer
Target duration, 1–60. Models honour it as they can — each renders a set of lengths of its own, and clamps to what it supports.
string
Such as
16:9 or 9:16. On a model that publishes the ratios it takes, one it does not is clamped to the nearest it does.integer
Repeat an earlier result, on models that honour a seed.
black-forest-labs/flux-3-max, renders 5 to 20 seconds (a shorter or longer seconds is clamped into that), at 21:9, 2:1, 16:9, 4:3, 1:1, 3:4 or 9:16 (with a first frame and no aspect_ratio, the frame’s own shape), with audio generated with the picture — speech written into the prompt is spoken. It is priced per generated second at 720p: $0.0425 per second until 7 November 2026 (00:00 UTC), then $0.17 per second, so a 5-second clip costs about $0.21, then $0.85. The price is the one in force when the job is submitted.
That is the whole request. A model priced per generated second renders at 720p. Not on it today: a resolution or size, a last frame, style or subject reference images beyond the first frame, an audio on/off switch, a completion callback URL, and any per-model or per-provider passthrough. A body that carries one of them is validation_failed, not quietly dropped. Completion is read by polling the job or, for an agent’s job, from the session’s events; a job submitted here does not fire a webhook — clip jobs do, video jobs do not yet.
Retrieve a job
GET /v1/media/{id} → 200 OK — scope agents:read. Poll it; a job left rendering for over thirty minutes is failed and not billed. Any med_ reads here, a clip job included — the same object GET /v1/media/clips/{id} answers.
Response
GET /v1/files/{file_id}?download=true. The file carries source.type = video_generation and source.job_id = this job.
A job an agent submitted with generate_video carries its session_id, and that session’s event stream announces it: media.job.queued, then media.job.completed or media.job.failed. A job submitted here has no session, so poll it.
List jobs
GET /v1/media → 200 OK, cursor-paginated — scope agents:read. See Pagination.
string
video (or another async kind).string
queued, processing, completed or failed.string
Jobs an agent submitted from one session.
number
Page size, 1–100.
string
The
next_cursor of the previous page.Billing
A job is billed what it cost, on the media tier, when it completes. Nothing is held beforehand, because the price is only known once the model accepts the job; instead, a submit on an empty balance is refused withinsufficient_credits (402) before it reaches a model. On a model priced per generated second, that is the clip’s length at the resolution it rendered; on one that reports a cost, what it reported. The debit settles under the job id at completion, exactly once, and the request’s idempotency key reaches the model’s provider where it takes one, so a retried submit is not rendered twice — black-forest-labs/flux-3-max’s takes none, so there only this API’s own replay (the same Idempotency-Key) guards against a second render. A job that fails, is refused, or is abandoned is not billed.
In a session
An agent submits with thegenerate_video tool — the same job, the same catalogue, the same fields (model?, prompt, image_urls?, character?, seconds?, aspect_ratio?, seed?). The tool does not block: it returns the job’s handle in the same turn and the agent carries on; when the job finishes, the file is saved and the session is woken with its file_id. Three differences from calling the route yourself:
modelmay be left to the agent’s configuration.tools.configs.generate_video.config.modelson the agent names the model that runs when the agent does not choose one. It is a default, not an allow-list; with none pinned and none named, the platform’s video default runs, as here.- Completion is an event, not a poll. The session’s event stream announces
media.job.queued, thenmedia.job.completedormedia.job.failed; the job also reads back here with itssession_idset. - The bill lands on the agent. The debit carries the
session_idand settles against the agent’s budget as themediacomponent; a job through this route carries no session and settles against the organization.