Skip to main content
The sandbox lets you drive the entire feed surface on demand, without waiting for a real live game: one POST mints a mock session that replays a recorded real game through the production pipeline. It is a real session in every observable way — it appears in your catalog, streams real frames over the real WebSocket, ends with a real session_closed, and stays readable via /history — so what you test is exactly what production sends. Wire it into your CI: trigger, discover, subscribe, assert your state folds, observe the close.
Mock sessions are private to your account and never appear in default catalog listings — your production pipeline cannot confuse one for a real game (see Telling mocks apart). Any authenticated account can trigger one; no league entitlement is required.

Trigger a mock session

Request body

Response

endsAt is the projected end of the replay. The session then ends exactly like a real game: subscribers receive {"type": "session_closed"} and a close with code 4000, and the session moves to the catalog’s closed slice.

Semantics

  • One active mock per account. Triggering while you already have one running replaces it: the old mock is force-ended (it moves to status=closed) and the response names the new one. Your CI can always trigger unconditionally — a leaked session from a crashed run can’t wedge it.
  • The tape is a real recorded game — one per sport, fixed — including scout corrections: expect undo events (with retractsSeq) and score_set corrections mid-stream. That is the point; those are the frames integrations get wrong.
  • Determinism: every run of a league’s tape replays the same events in the same order with the same state transitions. Only timestamps differ — they are stamped fresh at replay time, so your staleness/latency logic sees live-shaped data.
  • Game-clock values inside event payloads (period, soccer minute) come from the recording and will not match wall clock at speed ≠ 1 — inherent to accelerated replay.
  • A server restart mid-replay abandons the mock; it is force-ended shortly after (lazily). Just trigger again.

The full test flow

  1. POST /api/sandbox/sessions with your normal credential.
  2. Poll GET /api/sessions?status=open&sandbox=true until the sessionId appears (catalog — note the sandbox parameter).
  3. Connect WS /api/subscribe/{sessionId} — the hello frame carries "sandbox": true.
  4. Fold frames through your reducer; exercise your gap detection against outSeq.
  5. Observe {"type": "session_closed"} + close code 4000 when the tape ends.
  6. GET /api/history/{sessionId} — the ended mock replays in full (also stamped "sandbox": true); assert your cold-load backfill reproduces the same terminal state.

Telling mocks apart

Three guarantees keep a mock out of your production data path:
  • Default catalog listings exclude mocks. GET /api/sessions never returns sandbox sessions unless you pass ?sandbox=true — and that mode returns only your own mocks, no real games.
  • The hello frame and /history response carry "sandbox": true on mock sessions. The key is absent on real games (never false), so existing parsers are unaffected. Check it wherever a session id arrives outside catalog context.
  • The matchup is unmistakable: every mock plays "Sandbox A" vs "Sandbox B".

Access model

The trigger grants your account an entitlement on the minted session. No other consumer can see or subscribe to it (their subscribe attempt is refused 4403). Accounts with zero entitlements can use the sandbox, which makes it a way to build your integration before going live.