Oasis3 API Onboarding
Enter the password to access the onboarding guide.
OASIS 3 · REALTIME WORLD MODEL

From zero to a live
playable world

Open a live connection, set the scene with a prompt, and send controls. Oasis 3 streams a playable world back to your browser in realtime.

▶ Try it live in your browser ↗
Decart oasis-3-preview Live
How it works

A live connection, four moves

Oasis 3 is realtime. Your app opens one live connection, receives generated video continuously, and sends your controls back many times a second. There's no SDK to install; it's plain browser JavaScript (WebSocket and WebRTC).

1Open a live connection. Connect to the streaming endpoint with your key (or a short-lived token).
2Receive video. A WebRTC handshake wires the model's live video straight into a <video> element.
3Set the scene. Send a prompt to open the world. Send another anytime to change it live.
4Send controls. Stream inputs ~30× per second; the world responds in realtime.
The vision

One image in, a world you can play

Hand Oasis 3 a single image and it becomes a live world your audience can move through in realtime.

🦖 Jurassic Park
Jurassic Park starting frame
1 image in
live · playable
🔮 Stranger Things
Stranger Things starting frame
1 image in
live · playable

One generated frame becomes a world you can play. Any place your audience already loves can become a level. Image input is coming to the API; today's steps use text prompts.

Step 01

Get your API key

Create a key in the platform dashboard. It takes under a minute.

platform.decart.ai/api-keys
Sign in → API Keys → Create API key
In production, don't ship the raw key to the browser. Mint a short-lived token on your server and hand that to the client instead (below). For a quick local test you can use the key directly.
mint-token
// Server-side only — exchange your secret key for a short-lived client token.
const res = await fetch("https://api.decart.ai/v1/client/tokens", {
  method: "POST",
  headers: {
    "X-API-KEY": process.env.DECART_API_KEY,   // stays on the server, never in the browser
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    allowedModels: ["oasis-3-preview"],
    constraints: { realtime: { maxSessionDuration: 150 } },   // seconds
  }),
});
const { apiKey } = await res.json();   // a short-lived key. Hand THIS to the browser; it goes in the same api_key slot as the raw key.
Step 02 · Optional, but recommended

Building with a coding agent?

Using Claude Code, Codex, Cursor, or another AI assistant? Give it this guide before you write a line, so it gets the connection flow and message formats right the first time. Just hit Copy for agents up top. It drops the whole guide onto your clipboard, ready to paste into your agent.

Step 03

Connect and receive video

Open the streaming WebSocket, do a quick WebRTC handshake over it, and the model's live video lands in a <video> element. This is the whole connection.

const STREAM_URL = "wss://api.decart.ai/v1/stream";
const MODEL = "oasis-3-preview";
const apiKey = "dct_...";   // your key from Step 01 (use a short-lived token in production)

const url = new URL(STREAM_URL);
url.searchParams.set("model", MODEL);
url.searchParams.set("api_key", apiKey);
const ws = new WebSocket(url);

let pc;
ws.onopen = async () => {
  // Set up WebRTC to RECEIVE the model's video stream
  pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
  pc.addTransceiver("video", { direction: "recvonly" });

  // Live video arrives here. Wire it into your video element.
  pc.ontrack = (e) => { document.getElementById("video").srcObject = e.streams[0]; };

  // Trickle our network candidates to the server
  pc.onicecandidate = (e) => {
    if (e.candidate) ws.send(JSON.stringify({ type: "ice-candidate", candidate: { candidate: e.candidate.candidate, sdpMid: e.candidate.sdpMid, sdpMLineIndex: e.candidate.sdpMLineIndex } }));
  };

  // Send our offer to start the handshake
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  ws.send(JSON.stringify({ type: "offer", sdp: offer.sdp }));
};

ws.onmessage = async (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === "answer") {
    await pc.setRemoteDescription({ type: "answer", sdp: msg.sdp });
    // Connected. Now set the scene (Step 04).
  }
  if (msg.type === "generation_started") {
    // World is live. Start sending controls (Step 05).
  }
};
Add a video element. The stream needs a <video id="video" autoplay muted playsinline> on your page. The autoplay muted part is required, or the browser won't start playing it.
Low latency. Inside ontrack, set e.receiver.playoutDelayHint = 0 and e.receiver.jitterBufferTarget = 0 to keep the stream as live as possible.
You only send your own network info. Oasis bundles its connection details into the answer, so there's no incoming ice-candidate message to handle. Just trickle yours, as shown.
On a locked-down network? This guide uses regular WebRTC. Oasis 3 also supports LiveKit transport, which includes TURN relays for reliable connections behind strict firewalls. Ask the Decart team to enable it if you hit connection trouble.
Step 04

Set and change the scene

Once connected, send a prompt message to open the world. Send another one anytime to change it live, mid-session, with no reconnect. Use it to restyle, switch environments, or move to a new stage.

// Send this once you're connected (from Step 03's answer handler). enhance_prompt fills in detail for a terse prompt.
ws.send(JSON.stringify({
  type: "prompt",
  prompt: "a rainy downtown avenue at night, wet reflections",
  enhance_prompt: true,
}));

// ...then change it live anytime, no reconnect (send one at a time; these are separate examples)
ws.send(JSON.stringify({ type: "prompt", prompt: "the same street, heavy fog", enhance_prompt: true }));
ws.send(JSON.stringify({ type: "prompt", prompt: "a black-and-white film-noir version", enhance_prompt: true }));
enhance_prompt: use true for short prompts you write yourself (Oasis fills in the detail), and false for prompts that are already detailed, like the gallery ones below.

Name the setting

Environment, time of day, weather, surface. Like "snowy mountain pass at night."

Restyle as a lever

Fog, black-and-white, or a season change makes the same world easier or harder.

Change = new stage

A fresh prompt reshapes the world live, so use it to advance a mission or bump difficulty.

Keep it concrete

Describe what's on screen. Skip negatives ("no cars") and vague asks ("make it cool").

Prompt gallery

Real scene prompts from the Oasis 3 preview. Copy one to start, then tweak it. These are already enhanced, so send them with enhance_prompt: false.

🌧️ Rainy downtown night

A rainy night covers a dense downtown avenue under traffic lights, storefront signs, and bright reflections from wet pavement. The asphalt is slick, divided by clear lane markings, curb lanes, crosswalks, and raised sidewalks on both sides. Moderate traffic is present, with yellow taxis, passenger cars, and a delivery van spaced across the lanes while pedestrians wait at the corners. Glass storefronts, bus shelters, signal poles, street trees, and parking signs frame the block. The scene remains a realistic urban roadway with ordinary wet-weather conditions and no unusual obstacles.

🌫️ Foggy coastal bridge

A foggy early morning covers a coastal bridge approach under pale gray light and low visibility. The road surface is slightly damp, with solid white edge lines, dashed lane markings, concrete barriers, and narrow shoulders. Light traffic is present, including compact cars and a city bus spaced across the lanes, with no pedestrians on the bridge. Bridge railings, overhead lamps, warning signs, sea mist, and faint outlines of water beyond the barriers define the surroundings. The scene appears consistent with a common coastal commute and no unusual obstructions.

🏜️ Desert highway sunset

A warm sunset illuminates a dry desert highway with orange light spreading across a long straight roadway. The asphalt surface is dry, with bright lane dividers, rumble-strip shoulders, and low guardrails along sandy embankments. Traffic is sparse, with a pickup truck, an SUV, and a tractor trailer visible at long distances. Utility poles, small highway signs, scrub vegetation, and distant mesas surround the open roadside. The scene remains a realistic southwestern highway setting with clear visibility and no abnormal obstacles in the lanes.

More presets live in the Oasis 3 preview ↗.

Step 05

Send controls

Once you get generation_started, stream action messages, about 30 times a second. Each carries a control value from -1 to 1. Map keyboard or gamepad input into those values and you have live gameplay.

// Current control values, updated from your input (e.g. WASD)
let control = { throttle: 0, steering: 0 };   // each in [-1, 1]

// Example: keyboard -> control values
const keys = {};
addEventListener("keydown", (e) => keys[e.key.toLowerCase()] = true);
addEventListener("keyup", (e) => keys[e.key.toLowerCase()] = false);
function readInput() {
  control.throttle = keys["s"] ? -1 : (keys["w"] ? 1 : 0);
  control.steering = (keys["a"] ? 1 : 0) + (keys["d"] ? -1 : 0);
}

// Call startControls() once you receive `generation_started` (see Step 03)
let seq = 0, controlLoop;
function startControls() {
  if (controlLoop) return;   // don't start twice
  controlLoop = setInterval(() => {
    if (ws.readyState !== WebSocket.OPEN) return;
    readInput();
    ws.send(JSON.stringify({ type: "action", seq: ++seq, action: control }));
  }, 1000 / 30);
}
Smooth it out. The preview eases each control toward its target instead of snapping, which feels much smoother on screen and is worth copying.
Step 06

The complete client

Everything above in one standalone HTML file: connect, receive video, set a scene, and stream controls. Save it, add your key, open it in a browser, and you have a playable world.

<!doctype html>
<video id="video" autoplay muted playsinline style="width:100%"></video>
<script>
const apiKey = "dct_...";   // your key from Step 01 (use a short-lived token in production)
const MODEL = "oasis-3-preview";
const video = document.getElementById("video");
const control = { throttle: 0, steering: 0 };

const url = new URL("wss://api.decart.ai/v1/stream");
url.searchParams.set("model", MODEL);
url.searchParams.set("api_key", apiKey);
const ws = new WebSocket(url);
let pc;

ws.onopen = async () => {
  pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
  pc.addTransceiver("video", { direction: "recvonly" });
  pc.ontrack = (e) => {
    // keep it as live as possible (no buffering delay)
    if ("playoutDelayHint" in e.receiver) e.receiver.playoutDelayHint = 0;
    if ("jitterBufferTarget" in e.receiver) e.receiver.jitterBufferTarget = 0;
    video.srcObject = e.streams[0];
    video.play().catch(() => {});
  };
  pc.onicecandidate = (e) => {
    if (e.candidate) ws.send(JSON.stringify({ type: "ice-candidate", candidate: { candidate: e.candidate.candidate, sdpMid: e.candidate.sdpMid, sdpMLineIndex: e.candidate.sdpMLineIndex } }));
  };
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  ws.send(JSON.stringify({ type: "offer", sdp: offer.sdp }));
};

ws.onmessage = async (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === "answer") {
    await pc.setRemoteDescription({ type: "answer", sdp: msg.sdp });
    ws.send(JSON.stringify({ type: "prompt", prompt: "a desert highway at sunset", enhance_prompt: true }));
  }
  if (msg.type === "generation_started") startControls();
  if (msg.type === "error") console.error("Oasis error:", msg.error_type);
};

const keys = {};
addEventListener("keydown", (e) => keys[e.key.toLowerCase()] = true);
addEventListener("keyup", (e) => keys[e.key.toLowerCase()] = false);

let seq = 0, controlLoop;
function startControls() {
  if (controlLoop) return;   // don't start twice
  controlLoop = setInterval(() => {
    if (ws.readyState !== WebSocket.OPEN) return;
    control.throttle = keys["s"] ? -1 : (keys["w"] ? 1 : 0);
    control.steering = (keys["a"] ? 1 : 0) + (keys["d"] ? -1 : 0);
    ws.send(JSON.stringify({ type: "action", seq: ++seq, action: control }));
  }, 1000 / 30);
}
</script>
Before you ship. Swap the raw key for a short-lived token minted on your server (Step 01) so the secret never reaches the browser.
Reference

The message protocol

Every message exchanged over the live connection: what your app sends, and what Oasis sends back.

↑ Your app sends

offer + ice-candidateA quick handshake that opens the video connection. Happens once, at the start.
promptTells Oasis what world to show. Send it again anytime to change the scene.
actionYour controls (steering, throttle) — sent many times a second, so the world reacts instantly.

↓ Oasis sends back

answerConfirms the handshake — the video starts streaming.
generation_startedThe world is ready — now you can start sending controls.
errorSomething went wrong. The reason is in the error_type field.
Roadmap
In progress · Image start

Start from an image, not just a prompt

Today a session opens from a text prompt. Next, you'll be able to boot the world from an image: hand the model a still and it becomes your first frame, so you can drop players straight into a specific place or look, then play from there with the same prompts and controls.

Early results are promising; the public API for image start isn't live yet. It'll slot in here once shipped.

What can you build

Games on a world model

Oasis 3 gives you three basics: set a scene, send controls, and get live video. Layer game logic on top (a vision model watching the stream, counters, timers) and you get real gameplay loops:

The loop is always the same: a player acts, you detect a condition, you change the world, and move to the next stage.