Video generation
Generate video from text, images or video. The endpoints have the shape of the OpenAI videos API, so client.videos.create_and_poll and client.videos.download_content work. To move code that called Sora, see Move from the Sora API.
A render takes up to several minutes, so a video job is asynchronous. You create the job, then poll it or ask the exchange to call you, then download the file.
Quickstart
curl https://router.inference.ai.exchange/v1/videos \
-H "Authorization: Bearer sk-liquid-..." \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-1-5-pro",
"prompt": "a red paper boat on a stream",
"seconds": "5",
"size": "1280x720"
}'
# {"id": "job_...", "object": "video", "status": "in_progress", ...}
curl https://router.inference.ai.exchange/v1/videos/job_... \
-H "Authorization: Bearer sk-liquid-..."
# repeat until "status" is not "in_progress"
curl https://router.inference.ai.exchange/v1/videos/job_.../content \
-H "Authorization: Bearer sk-liquid-..." --output video.mp4
from openai import OpenAI
client = OpenAI(base_url="https://router.inference.ai.exchange/v1", api_key="sk-liquid-...")
video = client.videos.create_and_poll(
model="seedance-1-5-pro",
prompt="a red paper boat on a stream",
seconds="5",
size="1280x720",
)
if video.status == "completed":
client.videos.download_content(video.id).write_to_file("video.mp4")
Examples
How it works
- Create the job.
POST /v1/videosreturns the job with the statusin_progress. The exchange fixes the price cap and holds it. - Wait. Poll
GET /v1/videos/{id}, or sendcallback_urland let the exchange call you. - Read the end state. The job ends as
completed,failed,cancelledorexpired. - Download.
GET /v1/videos/{id}/contentreturns the file of acompletedjob.
You can cancel a job in step 2. Only a completed job costs money.
Find a model
A control works only when an offer for the model declares it. Read what a model supports before you send a control.
| Source | Use it for |
|---|---|
GET /v1/models?kind=video |
One card for each model: parameters, video_seconds and pricing. |
GET /v1/exchange/models?kind=video |
The market view, with no key: sizes, seconds, formats, max_duration_seconds and rate_per_video_second_usd. |
| The model page in the console | The same data, with example videos and a Playground. |
On the card of a video model:
kindisvideo_generationandmodalityisvideo.parameterslists each control that at least one offer supports.secondsis arangeor anenum.sizeandresolutionare anenum.generate_audiois abooleanwhen the model makes both kinds of video.seed,creativity,upscale_factorandprevious_job_idare listed when an offer declared them. A name that is not a key is not supported, so omit it. Modalities describes each descriptor.video_secondsgives the lengths.pricinggives the price for each second, with avariantwhen the price depends on the sound or the resolution.- On the market view, if
default_size_onlyis true, omitsize.
If a request sets a control that no offer declared, the answer is 400 unsupported_parameter. param names the feature, such as video.continue, or the field, such as resolution. The exchange holds nothing. It never drops a control, because you would pay for a video made without it.
Size, shape, resolution and sound
Send size when you know the pixel size. Otherwise describe the video, and the exchange picks a size that an offer declared.
{
"model": "<model>",
"prompt": "a red paper boat",
"seconds": 5,
"aspect_ratio": "16:9",
"resolution": "720p"
}
| You send | The exchange does |
|---|---|
size |
Matches offers that declared that size. |
aspect_ratio |
Picks the smallest declared size that has the shape. |
aspect_ratio and resolution |
Keeps only the sizes whose short side has the pixels of the label, such as 720 for 720p. |
resolution alone |
Picks no size. It limits the offers to those that make that resolution. |
generate_audio |
Matches offers that make that mode. true buys sound, false buys a silent video. |
| None of these | Lets any offer win. It makes its default. |
- If no declared size fits, the answer is
400 unsupported_parameter. - If
sizehas another shape thanaspect_ratio, or another short side thanresolution, the answer is400 conflicting_parametersand names both fields. - A silent video or a lower resolution can cost less. The cap and the charge use the price of the options that you asked for.
Controls
seed, creativity, upscale_factor and previous_job_id are offer features: video.seed, video.creativity, video.upscale_factor and video.continue. Only an offer that declared the feature can win a request that sets the field.
A negative seed, a creativity outside 0 to 1 and an upscale_factor below 2 get 400 invalid_request with the name of the field.
Seed
seed repeats a render on a best-effort basis. An upstream can change its model.
{ "model": "<model>", "prompt": "a red paper boat", "seconds": 5, "seed": 7 }
Upscale
upscale_factor and creativity apply to a model that upscales a video. Send the source as a reference.
{
"model": "<upscaling model>",
"prompt": "the same video, sharper",
"input_references": [{ "type": "video", "url": "https://example.com/clip.mp4" }],
"upscale_factor": 2,
"creativity": 0.3
}
Continue a video
previous_job_id must name a completed video job of your workspace. Each job is charged for its own seconds. The earlier job is not charged again.
{ "model": "<model>", "prompt": "the boat reaches the sea", "previous_job_id": "job_..." }
- An id that is no video job of your workspace, including a job of another workspace, gets
400 invalid_requestwithparamprevious_job_id. - A job that has not completed gets
400 conflicting_parameterswithparamprevious_job_id. - The status response repeats
previous_job_id.
Start image and frames
A video can start from an image. The exchange picks it in this order: the entry with the role first_frame, then the first image with no role in input_references. An entry with the role last_frame fixes the last frame. See Reference inputs.
An upstream crops a start image that does not have the shape of the video, so the exchange refuses one:
- The shape of the video is the shape of
size, or the default size of each offer withoutsize. The two must agree within 1%, so a 1280x720 image fits a 1920x1080 video. - The shape is what a viewer sees: an EXIF turn swaps the width and height of a JPEG.
- With
size, an image of another shape gets400 invalid_request. No money is held. - Without
size, an offer whose default size has another shape cannot win. If none is left, the answer is400 no_eligible_offer, and the message names asizethat fits. - Crop or pad a refused image, or send a
sizeof its shape.
The exchange reads a linked (https:) start image by header only, with Range requests. The frame header must start in the first 256 KiB. The link must use https, name a public host, use the default port and hold no login, and the host must answer within 3 seconds. If the exchange cannot read the link, it refuses the request and states the reason.
Job status
status |
Meaning | Cost |
|---|---|---|
in_progress |
A provider accepted the job and is rendering it. | Held |
completed |
The file is ready to download. | Charged |
failed |
The render failed. error states why. |
None |
cancelled |
You cancelled the job. | None |
expired |
The job did not finish within the video job limit of the exchange. The default is 10 minutes from dispatch. | None |
has_audiois a field of the answer. It states if the video has sound: the mode that you asked for ingenerate_audio, else the default of the offer that won. It is absent when the exchange does not know.- When the job ends, the status response holds the
exchangeblock withvideo_seconds, the seconds that you paid for.
Cancel a job
POST /v1/videos/{id}/cancel ends a job that is in_progress.
curl -X POST https://router.inference.ai.exchange/v1/videos/job_.../cancel \
-H "Authorization: Bearer sk-liquid-..."
- The exchange stops the provider and releases the hold. The job costs nothing. The answer is the job with the status
cancelled. - The provider stops within a few seconds. If the job finishes first, the answer is
409 video_not_cancellable, and the job keeps the statuscompletedand its charge. - A job that is
completed,failedorexpiredalso gets409 video_not_cancellable. - A repeat cancel of a cancelled job returns the job.
- A key that can only read cannot cancel.
Callback
Add callback_url to POST /v1/videos to get a call instead of polling. The response then holds a callback block:
{
"id": "job_...",
"status": "in_progress",
"callback": { "url": "https://example.com/hooks/video", "signing_secret": "whsec_..." }
}
The callback block is only in the response that places the job. Keep the signing_secret: the exchange does not state it again. With no callback block, poll the job.
The address must be an https URL with a public host name, the default port, no login and no IP address. Otherwise the exchange refuses the request and places no job.
Payload
When the job ends, the exchange sends a POST with a JSON body:
{
"id": "job_....video.generation.completed",
"type": "video.generation.completed",
"created_at": 1790000000,
"data": { "id": "job_...", "object": "video", "status": "completed", "exchange": {} }
}
type |
Sent when |
|---|---|
video.generation.completed |
The job is completed. |
video.generation.failed |
The job is failed. |
video.generation.cancelled |
The job is cancelled. |
video.generation.expired |
The job is expired. |
data is the object that GET /v1/videos/{id} returns, with no file. Download the file with GET /v1/videos/{id}/content.
Verify a delivery
| Header | Value |
|---|---|
X-Liquid-Signature |
t=<unix seconds>,v1=<hex>: HMAC-SHA256 of <t>.<body> with signing_secret |
X-Liquid-Event |
The same as type |
X-Liquid-Idempotency-Key |
The same for each attempt of one delivery. Drop a repeat |
Compute the HMAC over the t value, a dot and the raw body. Compare it with v1 in constant time. Refuse a t that is more than five minutes old.
Answer and retries
Answer with a 2xx status within 10 seconds. The exchange does not follow a redirect.
- Another status, or no answer, causes a retry after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. Then it stops. An answer of
410stops it at once. - Delivery is at least once, so your handler must accept a repeat. Polling always works.
Download
GET /v1/videos/{id}/content returns the file. The provider that rendered the video serves the download. You can download again until expires_at, if the response states it.
- A job that is not
completedgets404 video_not_ready. - After
expires_at, the answer is410 video_expired.
Billing
The price is for each second of video that you ask for. The create response returns x-liquid-cap-usd and x-liquid-job-id. The cap is exactly the charge, and the exchange settles when the render is complete. A failed, cancelled or expired job costs nothing.
Request parameters
The body can be JSON or multipart/form-data.
| Field | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | The model to buy, or a preset of the video kind. |
prompt |
string | Yes | The text that describes the video. At most 32,000 characters. |
input_references |
array | No | Reference inputs in one list, with a role for the frames. See Reference inputs. |
seconds |
number or string | No | The length, from 1 to 30. duration is another name for it. |
size |
string | No | A pixel size such as 1280x720. |
aspect_ratio |
string | No | auto, or a shape such as 16:9. |
resolution |
string | No | A label such as 720p or 1080p. |
generate_audio |
boolean | No | true for a video with sound, false for a silent video. |
seed |
integer | No | A whole number, 0 or more, that makes the render repeat. |
creativity |
number | No | From 0 to 1: how far an upscaling model may depart from the source. |
upscale_factor |
integer | No | 2 or more: how many times larger than the source the output is. |
previous_job_id |
string | No | The id of an earlier completed video job of your workspace that this video continues. |
callback_url |
string | No | An https address that the exchange calls when the job ends. See Callback. |
exchange |
object | No | Purchasing rules, such as cap_usd. See Purchasing rules. |
Without seconds, the exchange uses the shortest length that an offer declares, or 4 if none does. The seconds field of the response states the length used.
Errors
| Error | Cause and fix |
|---|---|
400 unsupported_model |
Not a video model. Read /v1/models for kind: video_generation. |
400 unsupported_parameter with param size, seconds, resolution or generate_audio |
No offer makes it. The message or eligibility.excluded lists what the offers make. |
400 unsupported_parameter with param video.seed and the like |
No offer declared the control. Read parameters on the model card. |
400 conflicting_parameters with param size |
size has another shape or short side than aspect_ratio or resolution. |
400 invalid_request with param input_references |
The start image has the wrong shape or is unreadable. Crop, pad or change size. |
400 no_eligible_offer with param input_references |
The image fits no default size. Send the size that the message names. |
400 with param previous_job_id |
The id is no video job of your workspace, or the job has not completed. |
409 video_not_cancellable |
The job has ended and cannot be cancelled. |
404 |
The job belongs to another account. Use the key that made the job. |
See Errors for each code and exclusion reason.
Next steps
- Reference inputs: frames, guidance and voice samples.
- Image generation: the same job for images.
- Purchasing rules: set a cap and limits.