Documentation menu

Test mode

The full API surface with fake robots, real math, and zero cost — a deterministic sandbox any key can use today.

Test mode is the entire documented API — every primitive, every lifecycle, every error code, every artifact type — served by a sandbox that fakes the robots and nothing else. Quotes follow the published rate card to the cent. Success rates come from actual per-episode draws with actual Wilson intervals. auto(ci=0.95, moe=0.03) really sizes n. A paired compare() really runs McNemar on the discordant pairs. What's simulated is the physics; the statistics are the same code path you'll be judged by.

Keys and base URL

Any key matching rbr_test_[a-z0-9]{16,} works immediately — no registration. If you don't want to invent one, the shared demo key is:

export ROBORAMA_API_KEY=rbr_test_demo

The base URL is the same as production, https://api.roborama.com; the key prefix selects the mode. A rbr_live_ key on the sandbox returns 403 live_key_on_sandbox. Both SDKs work unmodified — they already default to this host.

Graduation is a prefix swap: when you have a live key, change rbr_test_ to rbr_live_ and delete your sandbox overrides. Nothing else in your integration changes.

The 60-second tour

Quote, run at speed 100, poll, download a real artifact, force an error, poke a webhook:

the 60-second tour
import time

import roborama  # test mode: export ROBORAMA_API_KEY=rbr_test_demo

# 1. quote — the real rate card, decomposed, reconciling with /pricing
print(roborama.quote(robot="g1-edu-pro", environment="kitchen-std", episodes=600))

# 2. run at sandbox speed 100: 300 episodes land in about 13 seconds
run = roborama.run(
    robot="g1-edu-pro@fw2.3",
    environment="kitchen-std@v1.2",
    task="pick_place@v1",
    episodes=300,
    perturbation={"seed": 42},  # same seed, same result — forever
    sandbox={"speed": 100},     # pacing is 1 s/episode at speed 1
)

# 3. poll — state derives from elapsed time, identically on any instance
time.sleep(14)
print(run.result())             # n, rate, and a real Wilson 95% interval

# 4. artifacts — a genuine MCAP per episode (opens in Foxglove) + report
first = run.episodes[0]
print(first.mcap_url)           # presigned; expires after 24 h
print(run.report().url)         # the verification report PDF

# 5. force any documented error with the X-Roborama-Force header, and
# 6. fire due webhooks with POST /v1/runs/{id}/poke — see the cURL tab.

Determinism and seeds

Test mode has no database. Every id encodes its object (parameters, creation time, seed) under an HMAC signature; every GET recomputes current state from elapsed time plus a PRNG keyed on the seed. The consequences are useful:

  • Same id, same answers, forever, on any instance. A run created three seconds ago is queued; ten seconds in it's scheduling; then episodes tick at the pacing rate until it's completed.
  • Same seed, byte-identical results. Two runs with the same request body and the same perturbation.seed return byte-identical result JSON — including artifact links. Pin a seed in CI and your fixtures never move.
  • The canonical documented runs reproduce exactly. Submit the quickstart run with seed 42 and you get the published n=612, rate=0.874, ci95=(0.846, 0.898) — the fixtures on this site are live sandbox responses.

Pacing and sandbox.speed

Default pacing is one episode per second — a 300-episode run takes about five minutes, long enough to watch states move. For CI, pass "sandbox": {"speed": 100} on any create and the same run lands in about 13 seconds (queue and scheduling phases scale too). Speeds up to 10,000 are accepted; threshold() contracts cross their target within ~30 seconds at speed 100. Task pilots and kit lifecycles honor the same field.

Forced errors

Send X-Roborama-Force: <code> on any request and the sandbox returns that documented error with its real shape — every code on the error codes page, no exceptions. Three codes are in-band on POST /v1/runs: instead of an immediate error response, the created run demonstrates the documented behavior end to end.

TriggerEffect
X-Roborama-Force: <any documented code>that error, its real shape, its documented HTTP status
X-Roborama-Force: cell_fault on POST /v1/runsone episode suffers a facility fault: excluded from n, requeued, run.requeued webhook, trigger telemetry in /meta (facility incidents)
X-Roborama-Force: estop_triggered on POST /v1/runsthe run halts mid-flight as halted_facility: partial results with CI, remainder requeued, estop.triggered webhook
X-Roborama-Force: budget_exceeded on POST /v1/runsa budget is set that trips mid-flight: halted_budget with honest partial statistics
robot="unobtainium@fw0.0"robot_unknown
episodes=999999capacity_exceeded
policy with hf="anything-private" and no hf_tokencheckpoint_source_unauthorized
observation_contract="droid-3cam" against loft-stdcontract_validation_failed (loft-std has no wrist feed)
endpoint policy with latency_budget_ms below 50deterministic policy_deadline_missed episodes
publish="leaderboard"publish_forbidden (sandbox accounts have no opt-in)

Webhooks

POST /v1/webhooks returns a signed id encoding your URL — pass it as "webhook" on any create. Deliveries carry a Stripe-style Roborama-Signature header signed with the sandbox secret whsec_test, and fire when a GET or POST /v1/runs/{id}/poke touches the run (a stateless service has no cron; poke is the documented CI idiom). All six events are reachable: run.completed, episode.failed, threshold.crossed, regression.detected, estop.triggered, run.requeued.

Differences from live

SurfaceTest modeLive
Robots & physicsderived state, seeded drawsphysical hardware
Statisticsreal (same math, same code path)real
Quotes & billingreal rate card, $0 meteredreal rate card, metered
Artifactsgenuine formats, sample content (every episode's MCAP is real and Foxglove-openable; video is a sample clip; exports carry the first 3 episodes — LeRobot uses the v2.1 dataset layout, RLDS records carry JSON episode stubs in valid TFRecord framing)full per-episode capture
StreamsMJPEG loops of published cell footage; SSE events are reallive WebRTC + MJPEG
Capacityinfinite — no queues, no contentionscheduled, priced by priority
Persistencenothing persists: ids are self-contained, artifact links expire after 24 hper your data.retention
Mutationstask/kit mutations return the successor object under a new idmutate in place
Object idslong (they encode the object)short
Rate limit60 requests/min per keyaccount tiers

Everything else — request shapes, response shapes, error codes, webhook signatures, statistics — matches the OpenAPI spec exactly.