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:
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'sscheduling; then episodes tick at the pacing rate until it'scompleted. - Same seed, byte-identical results. Two runs with the same request body
and the same
perturbation.seedreturn 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.
| Trigger | Effect |
|---|---|
X-Roborama-Force: <any documented code> | that error, its real shape, its documented HTTP status |
X-Roborama-Force: cell_fault on POST /v1/runs | one 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/runs | the run halts mid-flight as halted_facility: partial results with CI, remainder requeued, estop.triggered webhook |
X-Roborama-Force: budget_exceeded on POST /v1/runs | a budget is set that trips mid-flight: halted_budget with honest partial statistics |
robot="unobtainium@fw0.0" | robot_unknown |
episodes=999999 | capacity_exceeded |
policy with hf="anything-private" and no hf_token | checkpoint_source_unauthorized |
observation_contract="droid-3cam" against loft-std | contract_validation_failed (loft-std has no wrist feed) |
endpoint policy with latency_budget_ms below 50 | deterministic 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
| Surface | Test mode | Live |
|---|---|---|
| Robots & physics | derived state, seeded draws | physical hardware |
| Statistics | real (same math, same code path) | real |
| Quotes & billing | real rate card, $0 metered | real rate card, metered |
| Artifacts | genuine 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 |
| Streams | MJPEG loops of published cell footage; SSE events are real | live WebRTC + MJPEG |
| Capacity | infinite — no queues, no contention | scheduled, priced by priority |
| Persistence | nothing persists: ids are self-contained, artifact links expire after 24 h | per your data.retention |
| Mutations | task/kit mutations return the successor object under a new id | mutate in place |
| Object ids | long (they encode the object) | short |
| Rate limit | 60 requests/min per key | account tiers |
Everything else — request shapes, response shapes, error codes, webhook signatures, statistics — matches the OpenAPI spec exactly.