Test vs live keys
Keys come in two modes, chosen when you mint the key (mode on
POST /v1/apps/{id}/keys, default test). The prefix tells you which:
A key’s mode is fixed at creation. To go from sandbox to production, mint a
live key — you don’t “flip” a test key.
What a test key returns
The generative endpoints below short-circuit before any backend call and return a shape-faithful mock carrying a"mock": true marker. Responses are otherwise
deterministic — a 1×1 placeholder image, canned text — with one exception: a
newly created world/character still gets a real, freshly-generated id (and,
being private, no slug).
The
"mock": true field is the reliable way to detect at runtime that you’re
on a test key. Exception: POST /v1/characters/{id}/tts streams a tiny
silent audio/mpeg clip (real bytes, so it can flow through your audio path) —
being binary, it carries no mock marker in the body. Detect test mode
from the key prefix there.POST /v1/worlds with a compiled world body (import) is not mocked —
there’s no GPU to skip. The world is validated and stored verbatim, just
namespaced into your test data (see below).
Isolation
Test resources live in a separate namespace from live ones:- Test worlds and characters are stored
mode='test', forced private, and are never given a slug — so they can never appear in a catalog or at a public/?play=…URL. - Live reads never see test rows, and test reads never see live rows. A test key
calling
GET /v1/worlds/{id}on a live world’s id gets404, and vice versa. - Usage counters are tracked per mode, so test traffic never moves your live numbers.
Cost & quota
- Test usage is never billed, and that is permanent. Even after runtime
billing is enabled, test-mode
session_secondsare excluded. - The generative endpoints above consume no daily quota in test mode (they short-circuit before the quota reserve), so they’re effectively uncapped — go ahead and hammer them in CI.
- The one metered path is session-token minting (
POST /v1/sessions/token): it still counts against a separate test-namespacesessioncounter (same default caps), but is never billed.
Idempotency across modes
Idempotency-Key is scoped per (app, mode, key value) — test and live keep
separate idempotency caches, so reusing the same key value across a test call and
a live call on the same app is safe (each replays only within its own mode).
Before you ship
- Swap
sk_test_…→sk_live_…(andpk_test_…→pk_live_…). Nothing else in your request code changes. async: trueis ignored by test keys (they always return the sync mock, so there’s no job to poll and no webhook fires). Test the async/webhook path with a live key.- Re-run your happy path against live once to confirm real covers, real slugs, and real play URLs come back where the mocks had placeholders.
See Concepts → Keys & modes for the key model, and
Pricing for how live runtime billing works.

