Console
API reference/POST /v1/videos

POST /v1/videos

OpenAI Videos API: a job, then the file; billed at the provider's price.

/llms.txt

Generate a video from a text prompt. This is the OpenAI Videos API surface: point client.videos.create at https://api.routerplus.com/v1 and the call works unchanged.

A video takes from ten seconds to several minutes to make, longer than any edge keeps a request open. So a video is a job. POST /v1/videos answers at once with the job; you read the job with GET /v1/videos/{id} until it is completed; then you download the MP4 from GET /v1/videos/{id}/content.

To try it without writing code, pick a video model in the playground.

Today every video model is served through Replicate, on the marketplace's own account: a key bound to your own provider connection cannot make videos yet. Text-to-video only: image-to-video and reference images are not offered yet, and a request that sends one is refused.

Authentication

HeaderFormat
AuthorizationBearer tm_vk_... (the standard OpenAI-style header)
x-api-keytm_vk_... — also accepted, same key

A missing or invalid key returns 401 with error_type: "auth".

Create a video

POST /v1/videos with a JSON body, capped at 1 MB. Every rule below is checked before anything is reserved or sent to a provider, so a refused request has no ledger row and no charge.

ParameterTypeBehavior
modelstring, requiredA video model id — see Model discovery. A chat model here is a 400 naming the chat routes; an image model is a 400 naming /v1/images/generations; an id that is not a video model anywhere is a 404.
promptstring, requiredNon-empty, at most 32 000 characters.
secondsinteger, or a string of digitsThe length. OpenAI's SDKs send a string ("8"), and both forms are read the same way. Each model accepts a range (below). Omitted = the model's default length, which is always sent, so the job is billed for the length that was reserved.
durationintegerAnother name for seconds. Send one of the two; if you send both and they differ, it is a 400.
resolutionstringPer model, for example 480p, 720p, 1080p.
aspect_ratiostringPer model, for example 16:9, 9:16, 1:1.
sizestringExact pixels, for example 1280x720, on the models that declare sizes (Seedance). Elsewhere it is dropped and recorded.
generate_audiobooleanOn the models that make sound.
seedintegerOn the models that declare one.
userstringYour end-user id, at most 256 characters. Kept with the job and returned on it; never sent to the provider.
providerobjectRouting controls read by the gateway and never forwarded — require_parameters and the rest, see Routing policies.

The accepted values are per model and published in /api/models.json under supported_parameters. A value outside a model's declared set is a 400 whose message starts with the field name and lists what that model accepts. An explicit null means "not provided", as on the other routes.

Refused, never dropped: input_reference, frame_images and input_references. To drop an image would make, and bill, a different video from the one asked for.

Dropped and recorded: any other key, including callback_url — the gateway does not send provider webhooks to your URL. The names are in the x-tm-dropped-params header and on the ledger row. Send "provider": {"require_parameters": true} to turn a drop into a 400 instead.

The answer is HTTP 200 with the video object, status: "queued". It carries the same headers as an image response: x-request-id, x-tm-provider, x-tm-attempts, x-tm-upstream-status, and x-tm-dropped-params when something was dropped. The provider has 60 seconds to accept the job; past that the request is a 504 upstream_error.

The video object

json
{
  "id": "video_7f3c2a9b1e4d4c6a8b0f2e1d3c5a7b9e",
  "object": "video",
  "model": "wan-3",
  "status": "completed",
  "progress": 100,
  "created_at": 1790000000,
  "completed_at": 1790000094,
  "expires_at": null,
  "seconds": "5",
  "size": null,
  "resolution": "720p",
  "aspect_ratio": "16:9",
  "error": null,
  "usage": { "cost": 0.5 }
}
FieldMeaning
idOurs, video_ and 32 hex digits. It is never the provider's id.
statusqueued → in_progress → completed or failed.
progress0 until the video is done, then 100. The providers report no progress, and the gateway does not invent one.
secondsA string, as in OpenAI's object.
errorOn a failed job: {code, message}. The code is one of video_generation_failed, content_policy_violation, video_cancelled, video_expired, video_timeout. The provider's own words are never kept.
usage.costUSD, once the job has ended: the charge. Absent while the job runs, and on static dev keys.
userYour user, when you sent one.

Read, list and download

CallAnswer
GET /v1/videos/{id}The video object. Ask every few seconds; a job takes minutes.
GET /v1/videosYour organization's videos, newest first: {object: "list", data, first_id, last_id, has_more}. Query: limit (1–100, default 20), order (desc or asc), after (a video id).
GET /v1/videos/{id}/contentThe MP4, streamed from the provider. ?variant=video is the only variant (anything else is a 400). A Range header is passed on, so a player can seek; a range the provider cannot satisfy is a 416. Before the job is completed it is a 409.

The gateway follows a job itself: it asks the provider every 5 seconds for the first two minutes, then every 10 seconds, then every 20. A read between two of those asks shows the last status the gateway knows. A video of another organization is a 404, exactly like a video that does not exist. The file is streamed through as it arrives, with the provider's content-type (else video/mp4), content-length, content-range and accept-ranges, and is never stored by the marketplace — save it when you download it.

Billing

Video models are billed at the provider's listed price for the job: Replicate's published price per second of video, for the seconds Replicate says it billed (never more than 5% past the length you asked for), pass-through, with no margin. See Pricing & billing.

  • Reserve, before the job is submitted: seconds × the model's per-second ceiling. The models page shows that ceiling as Per second ≤. Your balance must cover it.
  • Settle, when the job ends: the reported cost, exactly. The hold is then released.
  • A failed, cancelled or expired job costs nothing. You get no video, so you pay nothing.
  • A completed job without a reported cost is charged its reservation, marked estimated in GET /v1/generation.
  • If the gateway restarts while a job runs, the job is charged its reservation, as for any request a crash leaves open. The job itself continues, and GET /v1/videos/{id} still answers for it. A job the gateway follows for two hours without an end is also charged its reservation, and ends failed with the code video_timeout.

Worked numbers on wan-3 (per-second ceiling $0.105): a 5-second request reserves $0.525 (5 × $0.105). At 480p Replicate bills $0.025 a second, so the charge is $0.125 and the rest of the hold is released.

Models

Model idMade byLengthResolutionsSoundListed pricePer second ≤
wan-3Alibaba2–30 s (default 5)480p, 720p, 1080pno$0.025/s at 480p, $0.05/s at 720p, $0.10/s at 1080p$0.105
grok-imagine-videoxAI1–15 s (default 5)480p, 720pno$0.05/s$0.0525
seedance-2.0ByteDance4–15 s (default 5)480p, 720p, 1080pyes, at no extra cost$0.08/s at 480p, $0.18/s at 720p, $0.45/s at 1080p$0.48
seedance-2.0-fastByteDance4–15 s (default 5)480p, 720pyes, at no extra cost$0.07/s at 480p, $0.15/s at 720p$0.16
kling-v3-videoKuaishou3–15 s (default 5)720p, 1080p, 4kgenerate_audio, off by default$0.168/s at 720p ($0.252 with sound), $0.224/s at 1080p ($0.336 with sound), $0.42/s at 4k$0.441
veo-3.1-fastGoogle4, 6 or 8 s (default 8)720p, 1080pon by default; generate_audio: false turns it off$0.15/s with sound, $0.10/s without$0.158
sora-2OpenAI4, 8 or 12 s (default 4)— (16:9 or 9:16)yes$0.10/s$0.105

Prices are Replicate's published prices for each model, read on 2026-10-12. Replicate bills the video it returned, which can run a fraction of a second past the length asked for (5.04 s for 5 s); that fraction is billed, up to 5% of the length.

Errors

CaseStatuserror_typeBilled
A field outside the model's accepted values, an image input, a chat or image model on this route400invalid_requestno row
The model is listed but has no price row400model_not_pricedno row
Not a video model in the catalog404model_unavailableno row
Request body over 1 MB413request_too_largeno row
Unknown id, or another organization's video404not_found—
The file before the job is completed409invalid_request—
Key RPM or a shared limit, balance too low for the reservation, or a monthly spend cap429rate_limit / insufficient_quotano row
The provider refused the submitthe provider'supstream_error$0
The provider did not accept the job within 60 s504upstream_error$0
The provider accepted the job but named no job id, or its answer could not be read502upstream_error$0
Every deployment serving the model is cooling down503gateway_errornothing dispatched; retry-after: 5
The provider did not send the file on a download502upstream_error—
The provider accepted the job but it failed200 on the read; status: "failed"—$0

Bodies follow the OpenAI error envelope with the canonical class in error.metadata.error_type; a provider's own error body is never relayed. The reverse refusal also holds: a video model sent to POST /v1/chat/completions, POST /v1/messages or POST /v1/images/generations is a 400 naming /v1/videos.

Examples

curl
Create, read, download
curl -s https://api.routerplus.com/v1/videos \
  -H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
  -d '{"model":"seedance-2.0-fast","prompt":"a paper boat drifting down a rainy street","seconds":"5"}'
# → {"id":"video_…","status":"queued",…}
curl -s https://api.routerplus.com/v1/videos/video_… -H "Authorization: Bearer $TM_API_KEY"
curl -s https://api.routerplus.com/v1/videos/video_…/content -H "Authorization: Bearer $TM_API_KEY" -o video.mp4
python
The official openai SDK with the base URL swapped
import os
import time
from openai import OpenAI

client = OpenAI(base_url="https://api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"])

video = client.videos.create(model="wan-3", prompt="a lighthouse at dusk", seconds="5")
while video.status in ("queued", "in_progress"):
    time.sleep(5)
    video = client.videos.retrieve(video.id)
if video.status == "completed":
    client.videos.download_content(video.id).write_to_file("video.mp4")
typescript
The official openai package
import { writeFileSync } from "node:fs";
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.routerplus.com/v1", apiKey: process.env.TM_API_KEY });

let video = await client.videos.create({ model: "veo-3.1-fast", prompt: "a fox leaping into snow", seconds: "4" });
while (video.status === "queued" || video.status === "in_progress") {
  await new Promise((r) => setTimeout(r, 5000));
  video = await client.videos.retrieve(video.id);
}
if (video.status === "completed") {
  const file = await client.videos.downloadContent(video.id);
  writeFileSync("video.mp4", Buffer.from(await file.arrayBuffer()));
}

The SDKs' types list OpenAI's own models and lengths; the gateway reads any model id and any whole number of seconds the model accepts.

Data policy

The gateway keeps a job's id, model, length, shape, status and charge so you can retrieve it later. The video file streams from the provider when downloaded. See Data policy for how request data is handled.

Model discovery

bash
curl -s "https://api.routerplus.com/v1/models?output_modalities=video" \
  -H "Authorization: Bearer $TM_API_KEY"

Every row of GET /v1/models carries architecture.output_modalities: ["text"], ["image"] or ["video"]. The public feed https://app.routerplus.com/api/models.json carries supported_parameters for each video model, max_output_tokens as its per-second ceiling in cost units (one micro-dollar each), and price_card, the provider's listed prices.

See also: POST /v1/images/generations · Pricing & billing · Errors

Markdown source for agents: /docs/api-videos.md · index at /llms.txt