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
undoevents (withretractsSeq) andscore_setcorrections 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
POST /api/sandbox/sessionswith your normal credential.- Poll
GET /api/sessions?status=open&sandbox=trueuntil thesessionIdappears (catalog — note thesandboxparameter). - Connect
WS /api/subscribe/{sessionId}— the hello frame carries"sandbox": true. - Fold frames through your reducer; exercise your gap detection against
outSeq. - Observe
{"type": "session_closed"}+ close code4000when the tape ends. 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/sessionsnever returns sandbox sessions unless you pass?sandbox=true— and that mode returns only your own mocks, no real games. - The hello frame and
/historyresponse carry"sandbox": trueon mock sessions. The key is absent on real games (neverfalse), 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 refused4403). Accounts with zero entitlements can
use the sandbox, which makes it a way to build your integration before going live.