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

  1. Create the job. POST /v1/videos returns the job with the status in_progress. The exchange fixes the price cap and holds it.
  2. Wait. Poll GET /v1/videos/{id}, or send callback_url and let the exchange call you.
  3. Read the end state. The job ends as completed, failed, cancelled or expired.
  4. Download. GET /v1/videos/{id}/content returns the file of a completed job.

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:

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.

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_..." }

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 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

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-..."

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.

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.

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