Loading API documentation
This model is no longer supported.
This model is no longer supported.
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-sdkControl-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.
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 }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();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 blocksGate your UI with travel.can("pause") etc. so you never offer an action
the current travel state rejects.
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 sideawait 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.
The client provides a convenient way to interact with the model API.
npm install --save @fal-ai/clientThe @fal-ai/serverless-client package has been deprecated in favor of @fal-ai/client. Install the new package and update your imports — see client setup.
Set FAL_KEY as an environment variable in your runtime.
export FAL_KEY="YOUR_API_KEY"The client API handles the API submit protocol. It will handle the request status updates and return the result when the request is completed.
import { fal } from "@fal-ai/client";
const result = await fal.subscribe("alibaba/happy-oyster/worlds/create", {
input: {
mode: "adventure"
},
logs: true,
onQueueUpdate: (update) => {
if (update.status === "IN_PROGRESS") {
update.logs.map((log) => log.message).forEach(console.log);
}
},
});
console.log(result.data);
console.log(result.requestId);The API uses an API Key for authentication. It is recommended you set the FAL_KEY environment variable in your runtime when possible.
import { fal } from "@fal-ai/client";
fal.config({
credentials: "YOUR_FAL_KEY"
});When running code on the client-side (e.g. in a browser, mobile app or GUI applications), make sure to not expose your FAL_KEY. Instead, use a server-side proxy to make requests to the API. For more information, check out our server-side integration guide.
The client API provides a convenient way to submit requests to the model.
import { fal } from "@fal-ai/client";
const { request_id } = await fal.queue.submit("alibaba/happy-oyster/worlds/create", {
input: {
mode: "adventure"
},
webhookUrl: "https://optional.webhook.url/for/results",
});You can fetch the status of a request to check if it is completed or still in progress.
import { fal } from "@fal-ai/client";
const status = await fal.queue.status("alibaba/happy-oyster/worlds/create", {
requestId: "764cabcf-b745-4b3e-ae38-1200304cf45b",
logs: true,
});Once the request is completed, you can fetch the result. See the Output Schema for the expected result format.
import { fal } from "@fal-ai/client";
const result = await fal.queue.result("alibaba/happy-oyster/worlds/create", {
requestId: "764cabcf-b745-4b3e-ae38-1200304cf45b"
});
console.log(result.data);
console.log(result.requestId);Some attributes in the API accept file URLs as input. Whenever that's the case you can pass your own URL or a Base64 data URI.
You can pass a Base64 data URI as a file input. The API will handle the file decoding for you. Keep in mind that for large files, this alternative although convenient can impact the request performance.
You can also pass your own URLs as long as they are publicly accessible. Be aware that some hosts might block cross-site requests, rate-limit, or consider the request as a bot.
We provide a convenient file storage that allows you to upload files and use them in your requests. You can upload files using the client API and use the returned URL in your requests.
import { fal } from "@fal-ai/client";
const file = new File(["Hello, World!"], "hello.txt", { type: "text/plain" });
const url = await fal.storage.upload(file);The client will auto-upload the file for you if you pass a binary object (e.g. File, Data).
Read more about file handling in our file upload guide.
Create a world; prompts and reference images are moderated by
the unified safety checker before _create_world runs.
mode ModeEnum* requiredExperience mode: adventure (explore/play) or directing (story).
Possible enum values: adventure, directing
prompt stringNatural-language world description. Required for simple creation unless upload_mode is scenario_role.
event_style EnumEvent style for generated events.
Possible enum values: normal, dramatic
ref_world_id stringEncrypted ID of a template world to derive this world from.
sync booleanWhen true the upstream build is awaited (up to ~120s) before returning; otherwise poll /worlds/build-status.
perspective EnumCamera perspective. Required in adventure mode.
Possible enum values: first_person, third_person
upload_mode EnumAdventure creation sub-mode; default first_frame.
Possible enum values: first_frame, scenario_role
scene_prompt stringScene description (scenario_role).
role_prompt stringRole description (scenario_role).
scene_image_url stringScene reference image URL (scenario_role).
role_image_url stringRole reference image URL (scenario_role).
resolution EnumVideo resolution. Required in directing mode.
Possible enum values: 480p, 720p
layout EnumCamera movement style (directing mode).
Possible enum values: Stable, Fast
narrative EnumNarrative style (directing mode).
Possible enum values: Calm, Dramatic, Normal
Structured script (subjects + up to 45 acts); selects scriptlist creation. Directing mode only.
first_frame_image_url stringFirst-frame anchor image URL.
Additional reference image URLs.
enable_safety_checker booleanAuthorized callers may set false to request relaxed screening. Default value: true
{
"mode": "adventure",
"prompt": "A detective adventure in a rainy cyberpunk city",
"enable_safety_checker": true
}encrypted_world_id string* requiredstatus string* requiredgenerating, ready, or failed.
first_frame stringFirst-frame image URL once available.
name stringWorld title once built.
mode EnumPossible enum values: adventure, directing
Open to any holder of the world id — clients must poll build readiness before they can enter (travel) a shared world.
Poll every few seconds until status is ready (builds take ~30–120s).
encrypted_world_id string* requiredEncrypted world ID returned by /worlds/create.
{
"encrypted_world_id": ""
}encrypted_world_id string* requiredstatus string* requiredgenerating, ready, or failed.
first_frame stringFirst-frame image URL once available.
name stringWorld title once built.
mode EnumPossible enum values: adventure, directing
World Detail
encrypted_world_id string* requiredEncrypted world ID returned by /worlds/create.
{
"encrypted_world_id": ""
}encrypted_world_id string* requiredstatus string* requiredgenerating, ready, or failed.
first_frame stringFirst-frame image URL once available.
name stringWorld title once built.
mode EnumPossible enum values: adventure, directing
prompt stringcreation_model stringcreated_at stringupdated_at stringWMA session start with a self-describing session id.
The local record backs the temporary single-runner heartbeat/reaper. The client id additionally carries issue time and expiry, but does not make this REST shim cross-runner correct.
The connection object carries the one-time ticket, the short-lived token, and the api_base_url to hand to @happy-oyster/js-sdk in the browser.
Mutable session parameters shared between client and runner. The app validates and consumes app-specific keys.
Optional WebRTC SDP offer. Only used by apps that terminate media on the runner; control-plane apps ignore it.
{}session_id string* requiredServer-issued session identifier.
session_timeout_sec float* requiredSeconds of heartbeat silence after which the session expires.
heartbeat_interval_sec float* requiredRecommended interval between heartbeat calls, in seconds.
WebRTC SDP answer; present only when an offer was sent.
App-specific connection material returned by on_connect.
Heartbeat for the temporary single-runner session lifecycle.
Call every ~5 seconds while the travel runs. alive: false means the session expired — start a fresh one.
session_id string* requiredSession identifier from POST /session.
{
"session_id": ""
}session_id string* requiredalive boolean* requiredFalse when the session is unknown or expired. Sessions are not resumable: create a fresh session instead of retrying.
heartbeat_interval_sec float* requiredAssociate a travel with the session for close/expiry cleanup.
Returns a refreshed session id embedding the travel — clients must adopt it for subsequent heartbeat/close calls. The local binding lets the single runner's expiry reaper clean up abandoned travel.
Report the travel id from travel.start() so the platform can end the travel if your client disappears.
session_id string* requiredSession ID from POST /session.
encrypted_travel_id string* requiredTravel to associate with the session; it is ended upstream when the session closes or expires.
{
"session_id": "",
"encrypted_travel_id": ""
}session_id string* requiredRefreshed session id embedding the bound travel; use it for all subsequent heartbeat/close calls.
bound boolean* requiredFalse when the session id is not one of ours.
Close the session and normally end its bound travel.
After the SDK reports completion, travel_completed suppresses
the duplicate upstream end while still tearing down local session
state and stopping heartbeat/token work in the client.
session_id string* requiredSession identifier from POST /session.
travel_completed booleanTrue only after the SDK reports the travel completed. Closes the server session without sending a duplicate upstream end request.
{
"session_id": ""
}session_id string* requiredclosed boolean* requiredFalse when the session was already gone (call is idempotent).
Mint a fresh temporary token (e.g. for updateToken on expiry).
Tickets are not reissued here; start a new /session for that.
Refresh before expiry and inject via engine.updateToken().
expire_in_seconds integerTemporary token validity. Prefer the maximum so the token outlives the session; refresh via this endpoint on expiry. Default value: 1800
{
"expire_in_seconds": 1800
}token string* requiredShort-lived Open Platform API-key token.
expires_in integer* requiredRequested validity in seconds.
api_base_url string* requiredOpen Platform base URL for the client SDK (apiBaseUrl).
Travel Status
encrypted_travel_id string* requiredEncrypted travel ID from the client's enter-travel.
client_stream_status stringOptional client RTC playback status heartbeat (DISCONNECTED / CONNECTING / CONNECTED / PLAYING / BUFFERING / PAUSED / RECONNECTING).
client_stream_status_time_ms integerClient status change timestamp (ms epoch).
{
"encrypted_travel_id": ""
}encrypted_travel_id string* requiredstatus string* requiredinit, pending, running, failed, or completed.
rtc_status stringupdate_time stringPause Travel
Programmatic control for server-side clients — interactive browsers should use the SDK's equivalent instead, so its local state stays in sync with the stream.
encrypted_travel_id string* requiredEncrypted travel ID from the client's enter-travel.
{
"encrypted_travel_id": ""
}encrypted_travel_id string* requiredstatus string* requiredResume Travel
Programmatic control for server-side clients — interactive browsers should use the SDK's equivalent instead, so its local state stays in sync with the stream.
encrypted_travel_id string* requiredEncrypted travel ID from the client's enter-travel.
{
"encrypted_travel_id": ""
}encrypted_travel_id string* requiredstatus string* requiredRewind Travel
Programmatic control for server-side clients — interactive browsers should use the SDK's equivalent instead, so its local state stays in sync with the stream.
encrypted_travel_id string* requiredEncrypted travel ID from the client's enter-travel.
rewind_to_sec float* requiredTarget position in seconds; the server may round it down.
{
"encrypted_travel_id": ""
}encrypted_travel_id string* requiredstatus string* requiredresumed_at_sec floatInstruct Travel
Programmatic control for server-side clients — interactive browsers should use the SDK's equivalent instead, so its local state stays in sync with the stream.
encrypted_travel_id string* requiredEncrypted travel ID from the client's enter-travel.
content string* requiredDirecting-mode text instruction (e.g. a story beat).
enable_safety_checker booleanAuthorized callers may set false to request relaxed screening. Default value: true
{
"encrypted_travel_id": "",
"content": "A giant robotic dinosaur suddenly appears",
"enable_safety_checker": true
}encrypted_travel_id string* requiredcontent string* requiredaccepted boolean* requiredEnd Travel
Programmatic control for server-side clients — interactive browsers should use the SDK's equivalent instead, so its local state stays in sync with the stream.
encrypted_travel_id string* requiredEncrypted travel ID from the client's enter-travel.
fail_code stringPass TRAVEL_NO_STREAM_AUTO_END when ending because no stream arrived in time; the travel is then marked failed and produces no replay artifact.
{
"encrypted_travel_id": ""
}encrypted_travel_id string* requiredstatus string* requiredended_at stringduration_sec integererror_code stringerror_message stringTravel Artifacts
Replay video variants for an ended travel; poll while compose_status is processing.
encrypted_travel_id string* requiredEncrypted travel ID from the client's enter-travel.
{
"encrypted_travel_id": ""
}encrypted_travel_id string* requiredcompose_status stringready, partial, or processing.
Variants keyed original / withWatermark / withInstruction / withInstructionAndWatermark, each with url/status/resolution.
Info
api_base_url string* requiredticket_expires_in integer* requiredmax_token_expire_seconds integer* requiredsession_timeout_sec float* requiredheartbeat_interval_sec float* requiredsdp string* requiredSession description protocol payload.
type string* requiredSDP message type: offer or answer.
sdp string* requiredSession description protocol payload.
type stringSDP message type; must be offer. Default value: "offer"