Discord

Real-time

Live video & world models

Some video models are not “submit a prompt, get a clip.” An interactive world model holds a GPU for you while you steer it — walk around, change the scene, give it instructions — and streams the result live. /v1/live-video/sessions opens one of those sessions and bills it per second the GPU is held.

VideoRouter is the session broker, not the video pipe. It checks your key and credits, asks the host for a short-lived credential, and returns it to you. Your browser or app then connects directly to the host with the host’s own SDK — the video never passes through our servers, and the host API key never reaches your browser. We meter the session and settle it when it ends. Prefer a client library? There are Python and JavaScript SDKs — see the SDK overview.

Preview. Live video is rolling out model by model. A model is only bookable once its per-second price has been verified against the host’s own billing; until then GET /v1/live-video/models lists it with available: false and creating a session returns 502 pricing_unavailable instead of guessing a price. The browser snippet below has not been run end to end against a live session yet.

1. Open a session (your server)

from videorouter_live import LiveVideo

lv = LiveVideo(api_key="llmr_sk_live_...")     # or set VIDEOROUTER_API_KEY

# On YOUR server: open a session. Entering the block waits until the world is built
# (nothing is metered while it is "waiting"), then heartbeats in the background.
with lv.session(
    "fal/happy-oyster-adventure",
    max_duration_seconds=60,
    options={"world": {"prompt": "a rainy cyberpunk city", "perspective": "first_person"}},
) as s:
    # {"ticket": "...", "token": "...", "api_base_url": "..."} - returned once; hand it to the browser
    send_to_browser(s.connect)
    wait_for_user_to_finish()

# Settled on exit, even if your code raised.
print(s.status, s.billed_seconds, s.cost_usd, s.end_reason)   # ended 41 ... client

connect is returned once, in the response that created (or first saw ready) the session. It is a short-lived credential and is deliberately not stored or re-served — if you lose it, end the session and open a new one.

2. Stream it (your browser)

Hand connect to the host’s SDK. For Happy Oyster that is @happy-oyster/js-sdk. Each host has its own SDK and its own control vocabulary (movement, instructions, prompts, pause/rewind) — we do not wrap them.

import { HappyOysterEngine } from "@happy-oyster/js-sdk";

// `connect` is what your server got back from VideoRouter. Never ship your llmr_sk_ key to the browser.
const engine = new HappyOysterEngine({ APIHost: connect.api_base_url, model: "happyoyster-1.0-adventure" });
engine.updateToken(connect.token);

const travel = engine.createTravel({ ticket: connect.ticket, videoElement: document.getElementById("player") });
await travel.start();
travel.sendCommand({ translation: "Front", rotation: "Mouse_Left", interaction: "Jump" });
// ...and when the user leaves:
await travel.end();

Lifecycle & billing

StatusMeaning
waitingThe host is building a world (Happy Oyster: roughly 30–120 s). Nothing is metered. Poll GET until it flips.
readyA host session exists and you have credentials. The meter runs every second until the session ends, including idle seconds.
endedSettled. billed_seconds, cost_usd and end_reason are final.
  • Hold, then settle. Opening a session holds max_duration_seconds × price_per_second against your credits (402 if you can’t cover it). Ending it debits the real cost and releases the rest of the hold.
  • What you’re billed. Whole seconds, the larger of the host’s own reported duration and our measured ready→end time, never more than your cap on our side. Never the client’s claim. Vidu is the exception: its meter starts only when your browser’s control connection is acknowledged (not while the session is being set up) and is exact, so for Vidu we bill the host’s own figure — a session you opened but never started streaming costs $0.
  • Heartbeat. Call POST .../heartbeat about every 5 s. For Happy Oyster the host key lives with us, so the beat is forwarded to the host; for Decart, Odyssey and Vidu it is purely our liveness signal (Vidu’s own control connection is the browser’s business). If beats stop, we settle the session at the host’s own timeout — a forgotten tab costs at most the silence window, not the whole cap.
  • Hard cap. Vidu has no duration setting of its own, so for Vidu we hang the live up at your cap, from our servers, even while your browser is still connected. Every session ends at max_duration_seconds (default and ceiling are per model, below; Decart needs at least 10 s). Pass host_travel_id to /end to settle on the host’s authoritative duration and to get a replay.
  • A session that never became ready costs $0 and releases its whole hold.
  • Replays. Where the host composes a replay video, recording_url is populated on the ended session and mirrored to our storage.

Options

options accepts exactly these keys, per host (Happy Oyster takes the first three, Decart takes speed, Vidu takes session_params only, Odyssey takes none — its prompt and image go to the SDK in the browser); anything else is a 400.

OptionMeaning
worldObject. Build a new world on the host first (prompt, perspective, first_frame_image_url, … — passed through to the host). The session starts waiting. Cannot be combined with the two below.
world_idString. Reuse a world you built earlier; the session is ready immediately.
session_paramsObject. Happy Oyster: forwarded verbatim to the host’s session call. Vidu: required — the avatar takes avatar (persona and image_uri required; voice, greeting_instruction, farewell_enabled, persona_enhance optional), call_mode (video or audio), voice_model, audio, vad, llm, idle_motion and idle_timeout_seconds; editing takes image_url (required) and editing_type; the component avatar takes image_uri (required) and rtc_info (see Component avatar). Images are http(s) URLs or data:image/ URIs. Any other key is a 400.
speedstandard (default) or fast. Decart Lucy 2.5 / VTON only — fast bills at 2× the standard rate. It is a client-side connect option we cannot enforce, so declare the one you will use.

Prompts in options.world and a Vidu avatar’s persona and greeting go through the same content moderation as video and image requests. Live steering you send straight to the host afterwards is between you and the host — we cannot see it.

Component avatar (bring your own RTC)

vidu/vidu-s2-avatar-component is for teams that already run their own real-time channel (Alibaba, Tencent, Agora or Volcano Engine RTC) and their own speech pipeline. You stream the character’s speech and text to us; Vidu renders the character and pushes the video into your channel. Nothing else about the session differs: same hold, same settle, same limits.

  1. Open it with options.session_params: image_uri (required, http(s) or data:image/) and optionally rtc_info — provider (artc, trtc, agora, volcengine) and token are required, app_id, channel_id, user_id optional strings. Without rtc_info Vidu hosts a channel and connect.managed_rtc carries the credentials to join it and watch. Your join token is forwarded to Vidu and never stored in our logs.
  2. Stream to wss://<host>/api/v1/live-video/sessions/{id}/stream?conn_id=<random> (the path is in connect.stream_path). Servers authenticate with Authorization: Bearer; a browser, which cannot set headers, passes a short-lived live-video token as ?token= — a long-lived API key in a URL is refused.
  3. Frames. Text frames are JSON signals, binary frames are raw audio. First send {"type":1,"seq_id":1,"payload":{"conn_init":{"version":1}}} and wait for conn_init_ack.success; on NOT_READY just send it again a couple of seconds later (do not reconnect). Then stream PCM 16-bit signed little-endian, 24 kHz, mono (48 bytes per millisecond; 20 ms or 100 ms chunks suit it), plus type 9 (what the user said) and type 10 (what your model replied) as {"payload":{"text_msg":{"msg_id","content","timestamp"}}}, type 7 to interrupt, type 5 to hang up. We forward every frame verbatim in both directions.
Close codeMeaning
1000Normal: you hung up, the session was ended over HTTP, or the host ended it. The session is settled.
4410Your max_duration_seconds was reached. We hung the host up and settled the session.
4409The session is not open, already has a stream attached, or the host refused the stream. Nothing was settled; you may retry.
4401 / 4403 / 4404 / 4400Bad credentials / missing live_video scope / no such session (or another organization’s) / the model has no stream.
4502We could not reach the host. Nothing was settled; you may retry.
  • Why it is relayed. Vidu’s own data socket admits one connection, so whoever held it could never be cut at the session cap by anyone else. The relay holds that connection for you, which is what makes the cap, the hold and the settle real. Only audio and text cross it; the video goes straight to your RTC channel.
  • One stream per session, and detaching ends it. Closing the stream hangs the host up immediately and settles the session; to continue, open a new session. The stream itself counts as the heartbeat, so you do not call POST .../heartbeat.
  • Billing is the host’s own meter, as for the other Vidu models: 1 credit per second from the host’s acknowledgement of conn_init to the hang-up, with a short warm-up that is not billed.

Models

ModelKindMax sessionResolution
fal/happy-oyster-adventureWorld · steer in browser (pricing soon)60 s480p
fal/happy-oyster-directingWorld · steer in browser (pricing soon)180 s480p / 720p
odyssey/odyssey-2-proWorld · steer in browser (pricing soon)150 s720p
decart/lucy-2.5Real-time edit of a live stream600 s720p
decart/lucy-vton-3.6Real-time edit of a live stream600 s720p
decart/lucy-restyle-2Real-time edit of a live stream600 s704p
vidu/vidu-s2-avatarReal-time digital character · voice chat1800 s720p
vidu/vidu-s2-avatar-componentReal-time digital character · your own RTC channel1800 s720p
vidu/vidu-s2-editingReal-time edit of a live camera stream1800 s720p
decart/oasis-3-previewWorld · server-driven (not bookable yet)600 s768x512

Prices are live data, not a doc table: GET /v1/live-video/models returns price_per_second_usd and price_per_hour_usd per row. Session limits are the host’s own and differ by mode.

Limits & errors

Status / codeWhen
429 concurrency_limitToo many live sessions open for your organization. Retry-After is set; ending a session frees its slot.
429 host_capacityNo capacity on that model right now. Retry shortly — sessions are never queued, a queued real-time session is useless.
402 insufficient_creditsThe hold for this session isn’t covered by your balance.
400 max_duration_exceeds_host_limitmax_duration_seconds is above the model’s host cap.
400 invalid_session_paramsVidu: options.session_params is missing, has an unsupported key, or an image is not an http(s) / data:image/ URI.
400 host_rejected_requestVidu refused the request itself — typically the character or reference image failed its content review.
502 host_unavailableThe model cannot start sessions right now. Nothing was held or billed; retry later.
502 pricing_unavailableNo verified price for that model yet (see Preview note).
404No such session, or it belongs to another organization.
403 missing_scopeThe API key lacks the live_video scope.

Full request and response shapes: API reference below.

API reference

All endpoints need an API key with the live_video scope. Example values are placeholders; the price_per_second_usd you see is the model’s real price.

POST/v1/live-video/tokens

Mint a short-lived credential for a browser. Call it from your server with your own key and hand the result to the client: it can only call /v1/live-video session endpoints (never billable endpoints, never this one), so a leaked token is bounded by its lifetime and limits. Sessions opened with it are billed to your organization.

Parameters

ParameterTypeDescription
modelsstring[]Optional. Restrict the token to these live-video model ids (1–20).
expires_inintegerOptional. Lifetime in seconds, 1–21,600 (default 3,600). Out-of-range values are rejected, never silently clamped.
max_sessionsintegerOptional. Sessions this token may hold open at once (1–20) → 429 token_session_limit.
max_session_duration_secondsintegerOptional. Ceiling on a session’s max_duration_seconds (1–3,600) → 400 token_max_duration_exceeded.

Example response

{
  "object": "live_video.token",
  "token": "llmr_sk_live_...",
  "expires_at": "2026-10-13T13:00:00+00:00",
  "models": ["fal/happy-oyster-adventure"],
  "max_sessions": 1,
  "max_session_duration_seconds": 60
}

Using the token anywhere outside /v1/live-video returns 403 live_token_forbidden; using a model it wasn’t minted for returns 403 model_not_allowed. The caller’s key needs the live_video scope.

GET/v1/live-video/models

The catalog. Never exposes the host. A row with available: false has no verified price yet and can’t be booked.

Example response

{
  "object": "list",
  "data": [{
    "id": "fal/happy-oyster-adventure", "object": "live_video.model",
    "kind": "world", "control": "stream", "resolutions": ["480p"],
    "max_session_sec": 60, "available": true,
    "price_per_second_usd": 0.0, "price_per_hour_usd": 0.0
  }]
}

kind is generate/world/avatar/edit; control is stream (your browser steers over the host’s channel) today.

POST/v1/live-video/sessions

Open a session. Holds max_duration_seconds × price_per_second against your credits and mints the host credentials (immediately, or once the world is built).

Parameters

ParameterTypeDescription
modelstringRequired. A slug from GET /v1/live-video/models.
max_duration_secondsintegerOptional. 1 to the model’s max_session_sec; defaults to that cap. Above it → 400 max_duration_exceeds_host_limit.
options.worldobjectBuild a new world first (host-specific fields passed through). Session starts waiting. Exclusive with world_id/session_params.
options.world_idstringReuse an existing world; session is ready at once.
options.session_paramsobjectForwarded verbatim to the host’s session call.

Example response

{
  "id": "lvs_...",
  "object": "live_video.session",
  "model": "fal/happy-oyster-adventure",
  "kind": "world",
  "control": "stream",
  "status": "ready",
  "price_per_second_usd": 0.0,
  "reserved_usd": 0.0,
  "max_duration_seconds": 60,
  "created_at": "2026-10-13T12:00:00+00:00",
  "ready_at": "2026-10-13T12:00:01+00:00",
  "expires_at": "2026-10-13T12:01:01+00:00",
  "heartbeat_interval_seconds": 5,
  "world_id": null,
  "billed_seconds": null,
  "cost_usd": null,
  "end_reason": null,
  "recording_url": null,
  "connect": { "ticket": "...", "token": "...", "api_base_url": "..." }
}

connect appears only in the response that minted it. A waiting session has no connect, ready_at or expires_at yet.

GET/v1/live-video/sessions/{id}

Status and billing so far. If the session is waiting, this call checks the host and, when the world is built, mints the host session — that one response carries connect. Ownership-checked: another organization’s id returns 404.

Parameters

ParameterTypeDescription
idstring (path)The id from POST /v1/live-video/sessions.

status: waiting → ready → ended. After ending, billed_seconds, cost_usd, end_reason and (where available) recording_url are set.

POST/v1/live-video/sessions/{id}/heartbeat

Keep the host lease alive; call about every heartbeat_interval_seconds. If the session is past its cap it is settled instead (end_reason: max_duration), and if the host reports it gone, host_limit. Returns the session object.

Parameters

ParameterTypeDescription
idstring (path)The session id.

POST/v1/live-video/sessions/{id}/end

Settle the session now: debit the real cost, release the rest of the hold. Safe to repeat — a second call returns the same ended session and never debits twice. Ending a waiting session costs nothing.

Parameters

ParameterTypeDescription
idstring (path)The session id.
host_travel_idstring (body)Optional. The host’s own travel/run id. When given we ask the host for its authoritative duration (billed if larger than our clock) and fetch the replay.

end_reason is one of client, max_duration, heartbeat_timeout, host_error, host_limit, or abandoned (a world that never became ready).