Discord

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()).

Published. Both SDKs are on PyPI and npm as 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.
PythonJavaScript / TypeScript
Packagevideorouter-livevideorouter-live
Installpip install videorouter-livenpm install videorouter-live
RuntimePython 3.9+, httpx is the only dependencyNode 18+ and browsers, zero dependencies, ES modules, TypeScript types included
ClientsLiveVideo (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

  1. 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.
  2. Your browser uses the JS SDK with that token to open a session. VideoRouter holds a credit reservation and mints the host credentials.
  3. The browser hands session.connect to the host’s SDK, which streams video directly from the host.
  4. 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();
}
Modelsconnect contains
fal/happy-oyster-*ticket, token, api_base_url
odyssey/odyssey-2-prosession_id, signaling_url, session_token, expires_in, capabilities
decart/lucy-*api_key, token, base_url, model, expires_at, max_session_duration_seconds

The 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

StatusMeaning
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.
  • connect is 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 with getSession has none — end it and open a new one if you lose it.
  • Heartbeat. Call it every ~5 s (the SDK’s startHeartbeat / start_heartbeat does). 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 the withSession / session() helpers call it in a finally block.
  • Ending while waiting costs nothing; a session that never became ready is settled at $0.

Events

Event (Python · JS)Payload
status_changed · statusChangedthe new status
ended · endedthe session (settled: billed seconds, cost and end reason are set)
heartbeat · heartbeatthe session, after each successful beat
error · 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.

ClassHTTPWhen / what to do
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 end retry (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 ConcurrencyLimitError needs a session to end, and HostCapacityError is yours to back off on (retryAfter).
  • Request timeout: 30 s by default (timeout option).

Running in a browser

  • Never ship your API key. Use createToken on your server and give the browser the token only. The SDK refuses to start without a key, token or tokenProvider.
  • CORS is enabled on /v1/live-video/* (and the /api/v1 alias) 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 tokenProvider to refresh.
  • Bound the blast radius: pass models, max_sessions and max_session_duration_seconds when you mint.