Skip to content
Atlas World Model
Menu

Marble World API quickstart in TypeScript

By Atlas World Model EditorialUpdated 3 min read

Key facts

EndpointPOST /marble/v1/worlds:generate, model marble-1.1(as of 2026-09-03)source
Credits endpointGET /marble/v1/credits(as of 2026-09-03)source
Cost$1 = 1,250 credits; 1,500 per world; $5 minimum(as of 2026-09-03)source
Exports.spz / .ply splats (PLY free), mesh, video(as of 2026-09-03)source
RuntimeNode 22+ (native fetch, AbortSignal.timeout)(as of 2026-09-03)source

This tutorial gets you from zero to a downloaded Gaussian splat world using the World Labs Marble World API and plain TypeScript. It is written by an independent site, not affiliated with World Labs; the endpoint and model name come from the official docs, and response field names below are illustrative, so check them against the reference as you go.

Prerequisites

  • A World Labs developer account and an API key from the platform’s API keys page.
  • Credits: $1 buys 1,250, the minimum purchase is $5 for 6,250, and one Marble 1.1 world costs 1,500, about $1.20, as of September 3, 2026 (Radiance Fields). Full details on the pricing page.
  • Node 22 or later, for native fetch and AbortSignal.timeout.
  • Ten minutes.

Project setup

mkdir marble-quickstart && cd marble-quickstart
pnpm init
pnpm add -D typescript tsx @types/node
echo 'WORLDLABS_API_KEY=your-key-here' > .env

Never commit the .env file. Load it with node --env-file=.env or your preferred loader.

Step 1: check your credits

// src/credits.ts
const API = process.env.WORLDLABS_API_BASE ?? 'https://api.worldlabs.ai'; // copy the base URL from your dashboard
const headers = { Authorization: 'Bearer ' + process.env.WORLDLABS_API_KEY };

const res = await fetch(API + '/marble/v1/credits', { headers, signal: AbortSignal.timeout(15_000) });
if (!res.ok) throw new Error('credits request failed: ' + res.status);
console.log(await res.json()); // your remaining balance
curl https://api.worldlabs.ai/marble/v1/credits \
-H "Authorization: Bearer $WORLDLABS_API_KEY"

Run it with pnpm exec tsx --env-file=.env src/credits.ts. If you see a 401, the key is wrong or missing the Bearer prefix.

Step 2: generate a world from text

// src/generate.ts — request shape is illustrative; verify at docs.worldlabs.ai/api
const API = process.env.WORLDLABS_API_BASE ?? 'https://api.worldlabs.ai';
const headers = {
Authorization: 'Bearer ' + process.env.WORLDLABS_API_KEY,
'Content-Type': 'application/json',
};

export async function generateWorld(prompt: string) {
const res = await fetch(API + '/marble/v1/worlds:generate', {
  method: 'POST',
  headers,
  body: JSON.stringify({ model: 'marble-1.1', prompt }),
  signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error('generate failed: ' + res.status + ' ' + (await res.text()));
return (await res.json()) as { operation_url: string; operation_id: string };
}

const op = await generateWorld('a sunlit greenhouse full of ferns, glass roof, wooden benches, soft morning haze');
console.log('operation:', op.operation_id);
curl -X POST https://api.worldlabs.ai/marble/v1/worlds:generate \
-H "Authorization: Bearer $WORLDLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"marble-1.1","prompt":"a sunlit greenhouse full of ferns, glass roof, wooden benches"}'

The call returns quickly. It does not return a world; it returns an operation that describes a job in progress. For image input, replace the prompt with an image reference as described in the Marble World API guide.

Step 3: wait for the operation

Generation takes minutes. Poll with a growing interval so you are not hammering the API.

// src/poll.ts
export async function waitForOperation(operationUrl: string, headers: HeadersInit) {
  let delay = 3_000;
  const deadline = Date.now() + 20 * 60_000; // give up after 20 minutes
  while (Date.now() < deadline) {
    const res = await fetch(operationUrl, { headers, signal: AbortSignal.timeout(15_000) });
    if (res.status >= 500) {
      await new Promise((r) => setTimeout(r, delay));
      continue; // transient; retry
    }
    if (!res.ok) throw new Error('poll failed: ' + res.status);
    const status = (await res.json()) as { done: boolean; error?: string; world?: { id: string; exports: Record<string, string> } };
    if (status.error) throw new Error('generation failed: ' + status.error);
    if (status.done && status.world) return status.world;
    await new Promise((r) => setTimeout(r, delay));
    delay = Math.min(delay * 2, 15_000);
  }
  throw new Error('timed out waiting for the world');
}

The done, error and world fields stand in for whatever the official response uses. The shape of the loop is the point: bounded, backing off, tolerant of 5xx, strict on real errors.

Step 4: download and view the splat

When the operation completes, the world exposes export links. PLY splat export is free (Radiance Fields); .spz is the compressed form for the web.

// src/download.ts
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { Readable } from 'node:stream';

export async function download(url: string, file: string, headers: HeadersInit) {
  const res = await fetch(url, { headers, signal: AbortSignal.timeout(120_000) });
  if (!res.ok || !res.body) throw new Error('download failed: ' + res.status);
  // stream to disk; a .ply can be hundreds of megabytes
  await pipeline(Readable.fromWeb(res.body as never), createWriteStream(file));
}

Then open the file in the free splat viewer: drag it onto the page and orbit. Nothing is uploaded; it renders locally with Spark, World Labs’ MIT-licensed three.js renderer (GitHub). To embed it in your own page, load Spark and point a SplatMesh at the .spz URL.

Step 5: check credits again

Re-run the credits script. The balance should have dropped by 1,500 for a Marble 1.1 world. If a generation failed, confirm whether it was charged rather than assuming.

Error handling and retries

  • Timeouts. Every fetch above sets AbortSignal.timeout. A hung request is the most common failure in long-running pipelines.
  • 5xx and 429. Retry with exponential backoff, three attempts, then surface the error.
  • Operation errors. Do not retry blindly; the same input usually fails the same way. Log the operation identifier and inspect.
  • Key handling. Keep the key in server-side environment variables. If you are building a web app, put this code behind your own endpoint, as this site does.

Putting it together

A minimal end-to-end script wires the pieces above:

// src/main.ts
import { generateWorld } from './generate';
import { waitForOperation } from './poll';
import { download } from './download';

const headers = { Authorization: 'Bearer ' + process.env.WORLDLABS_API_KEY };
const op = await generateWorld('a quiet reading room with tall shelves and a window seat');
console.log('queued', op.operation_id);
const world = await waitForOperation(op.operation_url, headers);
console.log('world ready', world.id);
await download(world.exports.spz ?? world.exports.ply, 'world.spz', headers);
console.log('saved world.spz');

Run it with pnpm exec tsx --env-file=.env src/main.ts, wait a few minutes, and drop the resulting file into the viewer. Total cost: 1,500 credits, about $1.20.

Going to production

The script above is fine for a laptop and wrong for a server. In production, the generate call belongs in a request handler that returns immediately, the polling belongs in a queue consumer or a scheduled job, and the download should stream straight into object storage rather than a local file. Reserve credits in your own ledger when the job is queued and settle them when it completes, so a failed generation refunds the user without a support ticket. The Atlas World Model API page describes how this site implements exactly that behind a provider abstraction, so the same code will target Atlas when its endpoint appears.

Troubleshooting

If the generate call returns 400, the request body does not match the current schema; copy the example from the official docs verbatim and diff. If polling never completes, confirm you are requesting the operation URL from the response rather than a URL you constructed. If the downloaded file will not open in the viewer, check that it is a .ply or .spz and not an HTML error page saved with the wrong extension.

Next steps

  • Read the full Marble World API guide for inputs, models and export details.
  • Track when the same flow will work against Atlas on the Atlas API page.
  • Import the world into an engine with the game development guide.
  • Join the waitlist below to hear when hosted generation opens on this site, so you can skip the polling code entirely.

Sources

  1. World Labs docs: API
  2. World Labs docs: API pricing
  3. Radiance Fields: Marble 1.1 and 1.1 Plus
  4. Radiance Fields: World API for Marble
  5. Spark renderer on GitHub

Frequently asked questions

Do I need an SDK?

No. The examples use the built-in fetch in Node 22 or later. Check the official docs for any published client library.

How long does a world take?

Minutes. The generate call returns immediately with an operation you poll.

What does this tutorial cost to run?

One Marble 1.1 world is 1,500 credits, about $1.20, and the minimum top-up is $5, as of September 3, 2026.

Where do I view the result?

Download the .spz or .ply export and drop it into the free splat viewer on this site, or render it with Spark in your own page.

Will this work with Atlas?

Atlas has no public API yet. The control flow here, generate then poll then export, is the pattern any future endpoint is likely to follow.

Are the field names exact?

The endpoint and model name come from the official docs; response field names are illustrative. Verify against docs.worldlabs.ai/api.