Happy Oyster is an interactive world model: you create a world (a durable,
explorable environment generated from a prompt), then run travels — live,
realtime video sessions inside it that you steer as they stream. This API is
the control plane; the realtime video is delivered over WebRTC by the world
engine's browser SDK
(@happy-oyster/js-sdk)
using short-lived credentials minted here. Media never transits your backend,
and your fal key never ships to the browser.
Install both halves — the fal client for the control plane (server-side) and
the world engine SDK for the stream (browser):
npm install @fal-ai/client @happy-oyster/js-sdk
1. Create a world (server)
Control-plane calls authenticate with your fal key, so they belong on your
server. Build a world in adventure (explore/play) or directing (story)
mode, then poll until it's ready (builds take ~30–120s):
import { fal } from "@fal-ai/client";
const { data: created } = await fal.run(
"alibaba/happy-oyster/worlds/create",
{
input: {
mode: "adventure",
prompt: "A detective adventure in a rainy cyberpunk city",
perspective: "first_person",
},
}
);
let world = created;
while (world.status === "generating") {
await new Promise((resolve) => setTimeout(resolve, 4000));
({ data: world } = await fal.run(
"alibaba/happy-oyster/worlds/build-status",
{ input: { encrypted_world_id: created.encrypted_world_id } }
));
}
// world.status is now "ready" (or "failed")
Worlds are durable — keep encrypted_world_id and travel through the same
world again later.
2. Mint session credentials (server)
A session exchanges your fal key for short-lived realtime credentials: a
one-time ticket, a refreshable token, and the engine's
api_base_url. Token grants bill at mint for the seconds granted, so
request what you expect to stream and refresh to extend (step 5) — don't
front-load a long grant. Hand the connection object to your browser
client — it contains no long-lived secrets:
const { data: session } = await fal.run("alibaba/happy-oyster/session", {
input: {
session_params: {
encrypted_world_id: world.encrypted_world_id,
token_expire_seconds: 60,
},
},
});
// session.session_id — keep server-side for heartbeats
// session.heartbeat_interval_sec
// session.connection — { api_base_url, ticket, token, token_expires_in }
3. Connect and stream (browser)
The SDK opens the WebRTC connection straight to the world engine and plays
the live video into your <video> element:
import { HappyOysterEngine } from "@happy-oyster/js-sdk";
const engine = new HappyOysterEngine({
APIHost: new URL(connection.api_base_url).host,
});
engine.updateToken(connection.token);
const travel = engine.createTravel({
ticket: connection.ticket,
videoElement: document.querySelector("video"),
});
travel.on("statusChanged", (status) => console.log("travel:", status));
const { encryptedTravelId } = await travel.start();
4. Drive the travel
Adventure mode takes movement commands — components are sent on change and
"None" releases them:
// walk forward while panning right, then stop
await travel.sendCommand({
translation: "Front",
rotation: "Mouse_Right",
interaction: "None",
});
await travel.sendCommand({
translation: "None",
rotation: "None",
interaction: "None",
});
Directing mode takes free-text instructions plus transport control:
await travel.sendInstruct({
content: "A giant robotic dinosaur suddenly appears",
});
await travel.pause();
await travel.resume();
await travel.rewind({ rewindToSec: 12 }); // pause first; snaps to 4s blocks
Gate your UI with travel.can("pause") etc. so you never offer an action
the current travel state rejects.
5. Keep it alive (server)
Two loops run while the travel streams. Heartbeat the session at the
returned interval, and bind the travel id so the platform can clean up if
your client disappears:
await fal.run("alibaba/happy-oyster/session/bind-travel", {
input: { session_id: session.session_id, encrypted_travel_id: encryptedTravelId },
});
setInterval(async () => {
const { data } = await fal.run("alibaba/happy-oyster/session/heartbeat", {
input: { session_id: session.session_id },
});
if (!data.alive) {
// session expired — end the travel and start a fresh session
}
}, session.heartbeat_interval_sec * 1000);
Tokens expire after token_expires_in seconds; refresh before expiry and
inject the new one without interrupting the stream:
const { data: refreshed } = await fal.run("alibaba/happy-oyster/tokens/issue", {
input: { expire_in_seconds: 60 },
});
engine.updateToken(refreshed.token); // browser side
6. End and fetch the replay
await travel.end(); // browser
// server: release the session, then poll for the composed replay
await fal.run("alibaba/happy-oyster/session/close", {
input: { session_id: session.session_id },
});
const { data: artifacts } = await fal.run(
"alibaba/happy-oyster/travels/artifacts",
{ input: { encrypted_travel_id: encryptedTravelId } }
);
// artifacts.video.original.url once compose_status is "ready"
Exact request/response shapes for every route are documented in the
Schema sections below, in flow order.