Console
API reference

Videos

Generate videos.

POST/v1/videos

Generate a video from a text prompt. The route follows the OpenAI Videos API, so client.videos.create works unchanged with the base URL https://demo-api.routerplus.com/v1.

A video takes from ten seconds to several minutes to make. That is longer than any edge keeps a request open, so each video is a job:

  1. 01
    POST /v1/videos answers at once with the job.
  2. 02
    GET /v1/videos/{id} reads the job. Repeat until it is completed.
  3. 03
    GET /v1/videos/{id}/content downloads the MP4.

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

Limits:

  • Replicate serves every video model, on the RouterPlus account. A key bound to your own provider connection cannot make videos.
  • Text-to-video only. RouterPlus refuses a request that sends an image input or reference images.

Examples

Each example creates a job, polls it and downloads the MP4.

curl
curl -s https://dev2-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://dev2-api.routerplus.com/v1/videos/video_… -H "Authorization: Bearer $TM_API_KEY"
curl -s https://dev2-api.routerplus.com/v1/videos/video_…/content -H "Authorization: Bearer $TM_API_KEY" -o video.mp4
python
import os
import time
from openai import OpenAI

client = OpenAI(base_url="https://demo-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
import { writeFileSync } from "node:fs";
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://demo-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 SDK types list OpenAI's own models and lengths. RouterPlus accepts any model id, and any whole number of seconds that the model accepts.

Send your key as Authorization: Bearer tm_vk_... or as x-api-key: tm_vk_.... A missing or invalid key returns 401 with error_type: "auth".

Create a video

POST /v1/videos takes a JSON body of at most 1 MB. RouterPlus checks every rule below before it reserves money or sends anything to a provider. A refused request has no ledger row and no charge.

ParameterTypeBehavior
modelstring, requiredA video model id. See Model discovery. A chat model is a 400 that names the chat routes. An image model is a 400 that names /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"). RouterPlus reads both forms the same way. Each model accepts a range (see Models). If you omit it, RouterPlus sends the model's default length, and the job is billed for the length that was reserved.
durationintegerAnother name for seconds. Send one of the two. Both with different values 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). Other models drop and record it.
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, including require_parameters. RouterPlus reads them and never forwards them. See Routing policies.

Each model publishes its accepted values in /api/models.json under supported_parameters. A value outside the set of a model is a 400. The 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. A dropped image would make, and bill, a different video from the one you asked for.

Dropped and recorded: any other key, including callback_url. RouterPlus does not send provider webhooks to your URL. The x-tm-dropped-params header and the ledger row name the dropped fields. Send "provider": {"require_parameters": true} to get a 400 instead.

The answer is HTTP 200 with the video object and 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 a field was dropped. The provider has 60 seconds to accept the job. After 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
idThe RouterPlus id: video_ and 32 hex digits. 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 RouterPlus does not estimate 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. RouterPlus never keeps the provider's own words.
usage.costThe charge in USD, once the job has ended. 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. Poll 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 to 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. Any other value is a 400. RouterPlus passes a Range header on, so a player can seek. A range the provider cannot satisfy is a 416. Before the job is completed, the call is a 409.
  • RouterPlus polls the provider for you: every 5 seconds for the first two minutes, then every 10 seconds, then every 20. A read between two polls shows the last status RouterPlus has.
  • A video of another organization is a 404, the same as a video that does not exist.
  • The file streams through as it arrives, with the provider's content-type (else video/mp4), content-length, content-range and accept-ranges.
  • RouterPlus never stores the file. Save it when you download it.

Billing

RouterPlus bills a video at the provider's listed price for the job: Replicate's published price per second of video, for the seconds Replicate says it billed. That is never more than 5% past the length you asked for. The price passes through with no margin. See Pricing & billing.

CaseCharge
Before the job is submittedRouterPlus reserves seconds × the per-second ceiling of the model. The models page shows that ceiling as Per second ≤. Your balance must cover it.
The job endsThe reported cost. RouterPlus releases the rest of the hold.
A failed, cancelled or expired jobNothing. You get no video.
A completed job without a reported costThe reservation, marked estimated in GET /v1/generation.
The gateway restarts while a job runsThe reservation, as for any request a crash leaves open. The job continues, and GET /v1/videos/{id} still answers for it.
No end after two hours of pollingThe reservation. The job ends failed with the code video_timeout.

Example 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. RouterPlus releases the rest of the hold.

Models

Model idMade byLengthResolutionsSoundListed pricePer second ≤
wan-3Alibaba2 to 30 s (default 5)480p, 720p, 1080pprovider default; no audio toggle$0.025/s at 480p, $0.05/s at 720p, $0.10/s at 1080p$0.105
grok-imagine-videoxAI1 to 15 s (default 5)480p, 720pprovider default; no audio toggle$0.05/s$0.0525
seedance-2.0ByteDance4 to 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 to 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 to 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
  • Wan 3 and Grok Imagine Video follow the provider's audio defaults. This API has no audio toggle for them.
  • Prices are Replicate's published prices for each model, read on 2026-10-12.
  • Replicate bills the video it returned. That can run a fraction of a second past the length you asked for (5.04 s for 5 s). The fraction is billed, up to 5% of the length.
  • Sora 2 was retired on September 24, 2026. Existing video and usage records stay readable. See OpenAI’s discontinuation notice.

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_foundn/a
The file before the job is completed409invalid_requestn/a
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_errorn/a
The provider accepted the job but it failed200 on the read; status: "failed"n/a$0

Error bodies use the OpenAI error envelope, with the canonical class in error.metadata.error_type. RouterPlus never relays a provider's own error body. A video model sent to POST /v1/chat/completions, POST /v1/messages or POST /v1/images/generations is a 400 that names /v1/videos.

Data policy

RouterPlus keeps the id, model, length, shape, status and charge of a job, so you can retrieve it later. The video file streams from the provider when you download it. See Data policy.

Model discovery

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

Every row of GET /v1/models carries architecture.output_modalities, for example ["video"]. For each video model, the public feed https://demo.routerplus.com/api/models.json carries:

  • supported_parameters
  • max_output_tokens: the per-second ceiling in cost units (one micro-dollar each)
  • price_card: the provider's listed prices

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

For agents: /docs/api-videos.md and /llms.txt