Developer docs
Live video SDKs
The Python and JavaScript SDKs wrap the live-video API: open a session, keep it alive, settle it. They are small on purpose — typed errors, events, a background heartbeat and safe retries — and they follow the shape you would expect from a realtime client (status, sessionId, on / off / once, getLastError()).
videorouter-live. A live-video model can only be booked once its price is verified (see Live video & world models), and the SDKs surface that as PricingUnavailableError.Packagevideorouter-livevideorouter-liveInstallpip install videorouter-livenpm install videorouter-liveRuntimePython 3.9+, httpx is the only dependencyNode 18+ and browsers, zero dependencies, ES modules, TypeScript types includedClientsLiveVideo (sync) and AsyncLiveVideo (asyncio)LiveVideo (promises)What the SDK is — and isn’t
VideoRouter is the session broker. The SDK talks to it: it checks your credits, mints the host’s short-lived credentials, meters the session and settles it. It never carries video. session.connect is the host’s credential, and your browser connects to the host directly with the host’s own SDK — Happy Oyster, Odyssey, Decart. That is why there is no sendCommand or track API here: each host has its own control vocabulary, and we do not wrap it.
How it fits together
- Your server holds your API key and mints a short-lived token for the browser (
create_token/createToken). The token can only drive live-video sessions — it cannot call any billable endpoint and cannot mint more tokens. - Your browser uses the JS SDK with that token to open a session. VideoRouter holds a credit reservation and mints the host credentials.
- The browser hands
session.connectto the host’s SDK, which streams video directly from the host. - The SDK heartbeats every ~5 s. When the user is done,
end()settles the session: you pay for the seconds used and the rest of the hold is released.
Server-to-server is the same minus the token: use the SDK with your key, and pass connect to wherever the host SDK runs.
Quickstart
1. Mint a token on your server
from videorouter_live import LiveVideo
lv = LiveVideo(api_key="llmr_sk_live_...") # or set VIDEOROUTER_API_KEY
# Your server, per page load / per user. The token is safe to hand to a browser.
token = lv.create_token(
models=["fal/happy-oyster-adventure"],
expires_in=300, # seconds
max_sessions=1,
max_session_duration_seconds=60,
)
return {"token": token.token} # e.g. from your /api/live-token route
2. Open and drive a session in the browser
import { LiveVideo } from "videorouter-live";
// tokenProvider is called for the first request and once more if the token has expired (401).
const lv = new LiveVideo({
tokenProvider: async () => (await fetch("/api/live-token", { method: "POST" }).then((r) => r.json())).token,
});
const session = await lv.createSession({
model: "fal/happy-oyster-adventure",
maxDurationSeconds: 60,
options: { world: { prompt: "a rainy cyberpunk city", perspective: "first_person" } },
});
session.on("statusChanged", (status) => console.log(status)); // waiting -> ready -> ended
await session.waitUntilReady(); // a world takes ~30-120 s to build; nothing is metered while "waiting"
session.startHeartbeat(); // every ~5 s in the background
startHostStream(session.connect); // hand the host credentials to the host's own SDK (below)
// ... when the user is done:
await session.end();
console.log(session.billedSeconds, session.costUsd, session.endReason);
3. Hand connect to the host’s SDK
connect is host-specific. Its shape per model family:
import { HappyOysterEngine } from "@happy-oyster/js-sdk";
function startHostStream(connect) {
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") });
return travel.start();
}
fal/happy-oyster-*ticket, token, api_base_urlodyssey/odyssey-2-prosession_id, signaling_url, session_token, expires_in, capabilitiesdecart/lucy-*api_key, token, base_url, model, expires_at, max_session_duration_secondsThe host snippets use each host’s documented SDK calls; run them against your own session before shipping, since the host SDKs evolve independently of ours.
Session lifecycle
waitingThe host is building a world (Happy Oyster: roughly 30–120 s). Nothing is metered. connect is not available yet; waitUntilReady polls for it.readyCredentials issued; the meter runs every second, including idle seconds.endedSettled. billed_seconds, cost_usd and end_reason are final.connectis returned once. It is a short-lived credential and is deliberately not stored or re-served. The SDK keeps it on the session object for you; a session re-fetched withgetSessionhas none — end it and open a new one if you lose it.- Heartbeat. Call it every ~5 s (the SDK’s
startHeartbeat/start_heartbeatdoes). Without it, the server settles the session itself after a short silence window — a closed tab costs at most that window, never the whole cap. - Hard cap. Every session ends at
max_duration_seconds(default and ceiling are per model; a token can lower the ceiling). - End always.
end()is safe to call twice, and thewithSession/session()helpers call it in afinallyblock. - Ending while
waitingcosts nothing; a session that never became ready is settled at $0.
Events
status_changed · statusChangedthe new statusended · endedthe session (settled: billed seconds, cost and end reason are set)heartbeat · heartbeatthe session, after each successful beaterror · errora LiveVideoError from the background heartbeat (also kept as last_error / getLastError())Registering any other event name raises immediately, so a typo can’t silently never fire. Handlers may be async; an exception inside one never breaks the SDK.
Errors
Every failed call throws a LiveVideoError subclass carrying status, code, type and, when the server sent one, retry_after / retryAfter (seconds). Match on the class, not on message text.
AuthenticationError401The key or token is missing, invalid or expired. In a browser: mint a fresh token (the JS tokenProvider does this for you once).PermissionDeniedError403Missing the live_video scope (missing_scope), a model the token isn’t allowed to use (model_not_allowed), or a token used outside live-video (live_token_forbidden).InvalidRequestError400A bad parameter: unknown model, max_duration_seconds above the host cap (max_duration_exceeds_host_limit) or the token’s own (token_max_duration_exceeded), an unsupported option.InsufficientCreditsError402The credit hold for this session isn’t covered, or a spend cap was hit. Top up, or book a shorter session.PricingUnavailableError502The model has no verified price yet (pricing_unavailable). Check model.available first.NotFoundError404No such session — or it belongs to another organization.ConcurrencyLimitError429concurrency_limit / token_session_limit: too many sessions open. End one first; retryAfter is set.HostCapacityError429host_capacity: the host has no free capacity right now. Retry shortly — sessions are never queued.RateLimitError429Any other rate limit. The two classes above are subclasses of it.UpstreamError5xx / networkThe gateway or the host failed, or the network did.SessionStateError—Raised locally: the operation doesn’t fit the session’s state (e.g. a heartbeat on an ended session).TimeoutError—waitUntilReady / wait_until_ready ran out of time.Retries & timeouts
- Reads, heartbeats and
endretry (default 2 times, exponential backoff) on network errors and 502/503/504. - Creating a session or a token is never retried. A retried create could open a second billed session. If a create fails with a network error, call
models()/ check your dashboard before trying again. - 429s are surfaced, not retried: a
ConcurrencyLimitErrorneeds a session to end, andHostCapacityErroris yours to back off on (retryAfter). - Request timeout: 30 s by default (
timeoutoption).
Running in a browser
- Never ship your API key. Use
createTokenon your server and give the browser the token only. The SDK refuses to start without a key, token ortokenProvider. - CORS is enabled on
/v1/live-video/*(and the/api/v1alias) for any origin, because auth is a bearer token with no cookies. Other VideoRouter endpoints keep the browser’s same-origin default. - A token’s lifetime is 1–21,600 s (default 1 h). It only gates new requests; keep minting short ones and use
tokenProviderto refresh. - Bound the blast radius: pass
models,max_sessionsandmax_session_duration_secondswhen you mint.