Move from the Sora API

OpenAI shut down the Sora 2 models and its Videos API on 24 September 2026. The exchange serves the same three routes: create a video, read its status and download the file. Change the base URL, the API key and the model. The poll loop and the download stay the same.

The models are not Sora, so one prompt gives a different clip. Test your prompts first.

Change these four things

  1. Issue a spend key on the API keys page. See API keys.
  2. Set the base URL to https://router.inference.ai.exchange/v1 (or OPENAI_BASE_URL). Send the key as Authorization: Bearer sk-liquid-… (or OPENAI_API_KEY).
  3. Change model from sora-2 or sora-2-pro to a video model such as seedance-1-5-pro. See Model names.
  4. Set a price cap with the x-liquid-cap-usd header. See Set a cap.

Then remove each Sora feature that the exchange lacks, check your length and size, and poll less often.

What differs

Example

Python

The Sora call is OpenAI().videos.create_and_poll(model="sora-2", ...). The same call for the exchange:

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://router.inference.ai.exchange/v1",
    api_key=os.environ["LIQUID_API_KEY"],
)

video = client.videos.create_and_poll(
    model="seedance-1-5-pro",
    prompt="a red paper boat on a stream",
    seconds="8",
    size="1280x720",
    poll_interval_ms=10_000,
    extra_headers={"x-liquid-cap-usd": "2.00", "idempotency-key": "boat-001"},
)
if video.status == "completed":
    client.videos.download_content(video.id).write_to_file("boat.mp4")
else:
    print(video.error)

The model name is an illustration. The SDK marks the video methods as deprecated, but they still work.

curl

The full request, with jq to read the video id. It polls each 10 seconds, prints the result and downloads the file:

id=$(curl -s https://router.inference.ai.exchange/v1/videos \
  -H "Authorization: Bearer $LIQUID_API_KEY" \
  -H "x-liquid-cap-usd: 2.00" \
  -H "idempotency-key: boat-001" \
  -F model="seedance-1-5-pro" \
  -F prompt="a red paper boat on a stream" \
  -F seconds="8" \
  -F size="1280x720" | jq -r '.id // error(.error.message)')

while [ "$(curl -s https://router.inference.ai.exchange/v1/videos/$id \
  -H "Authorization: Bearer $LIQUID_API_KEY" | jq -r .status)" = in_progress ]; do
  sleep 10
done

curl -s https://router.inference.ai.exchange/v1/videos/$id \
  -H "Authorization: Bearer $LIQUID_API_KEY" | jq '{status, error, exchange}'

curl -f https://router.inference.ai.exchange/v1/videos/$id/content \
  -H "Authorization: Bearer $LIQUID_API_KEY" --output boat.mp4

Reference

Model names

Send the name of a model that renders video. GET /v1/exchange/models lists each model, and a video model has video in output_modalities. The exchange does not use a different model in place of a Sora name.

The video entry gives the lengths and sizes that the offers render. For example, seedance-1-5-pro renders 4 to 12 seconds and accepts a size. A model with default_size_only: true, such as veo3.1, renders only its default size.

A chat deployment is excluded with the reason job_kind_not_served.

Set a cap

The price is for each second of video. So the cap that the exchange fixes is exactly the charge. Set x-liquid-cap-usd to the most that you agree to pay for one video. An offer whose charge is more than the cap cannot win.

The OpenAI SDK sends videos.create as a form, not as JSON. In a form, the exchange reads the exchange block only as one field that holds JSON text. It ignores fields such as exchange[cap_usd], which the SDK makes from extra_body={"exchange": {...}}. Use headers, or send the block as text:

import json

extra_body = {"exchange": json.dumps({"cap_usd": 2.0, "privacy": {"retention": "none"}})}

For several rules in one header, use x-liquid-rules. See Purchasing rules.

Length and size

Sora setting Exchange setting
seconds: 4, 8 or 12 From 1 to 30, as a number or a string. An offer that declares its lengths must list the value. Default: the shortest declared length, or 4.
size, such as 720x1280 <width>x<height>. An offer that declares its sizes must list the value. Without size, the provider uses its default size.
input_reference, a file The image that the video starts from. Only an offer that reads image input can win. The image must have the shape of the video within 1%. See Start image in Video generation.
input_reference, {"image_url"} Applied in a JSON body. The URL is https: or data:image/. {"file_id"} is refused, because the exchange has no Files API.

For more reference inputs, such as audio or a second image, send references. See Start from reference inputs.

Poll the status

POST /v1/videos returns when a provider accepts the job. The response has status: "in_progress" and the x-liquid-cap-usd and x-liquid-job-id headers. The video id is the job id.

Send GET /v1/videos/{id} until status is completed or failed. A render takes up to several minutes, so poll each 10 to 20 seconds. create_and_poll polls each second unless you set poll_interval_ms.

When the job ends, the response holds the exchange block with the charge and video_seconds. A failed job holds error with code and message.

Send GET /v1/videos/{id}/content to download the file. The provider that rendered the video serves the file, and the exchange keeps nothing. You can download it again until expires_at, if the response states it. Only your account can read it.

Sora features

Sora feature What the exchange does
POST /v1/videos, JSON or multipart/form-data Applied. prompt can have up to 32,000 characters.
GET /v1/videos/{id} Applied. status is in_progress, completed or failed, never queued. progress is 0 until complete, then 100.
GET /v1/videos/{id}/content Applied. 404 video_not_ready before the job is complete, 410 video_expired after the provider deletes the file.
variant=thumbnail or spritesheet, unknown fields Ignored. The route sends the video.

Troubleshooting

Error Cause Fix
404 model_not_found The model is a Sora name, or a name that the exchange does not list. Pick a video model. See Model names.
400, param is seconds or size The length is not from 1 to 30, or the size is not <width>x<height>. Send a value in range, such as seconds="8" and size="1280x720".
400, param is input_reference The start image does not have the shape of the video, or the exchange could not read its link. Crop or pad the image, or send a size with the shape of the image.
400 no_eligible_offer The cap is less than the charge of each offer. Raise the cap, or ask for fewer seconds.
503 no_eligible_offer or 400 unsupported_parameter No offer for the model renders the length, size or reference input. Read eligibility.excluded in the error. Change the length, remove size, or pick another model.
402 insufficient_balance The charge is more than the balance. needed_usd and balance_usd give both. Add funds, or ask for fewer seconds.
404 video_not_ready on the download The job is not complete. Poll until completed.
410 video_expired The provider deleted the file. Download soon after the job is complete.
503 provider_unavailable on the download The provider that rendered the video is not connected. Only it has the file. Try again later.
409 idempotency_no_result or idempotency_conflict A retry used the idempotency key of an earlier create. The SDK sends a new form on each try, so the body differs. Do not create the video again. Poll the job id from the first response. See Purchasing rules.