> ## Documentation Index
> Fetch the complete documentation index at: https://docs.akaramarkets.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Core Concepts

> How the feed works: snapshot stream, sessions, the A/B model, and event patterns

## Snapshot stream

Every frame the server sends carries two things: the event that just happened, and the full match snapshot after that event is applied. You never have to rebuild state from the events yourself — each snapshot is complete and already computed.

```text theme={null}
 frame = { outSeq, event, state }
                   ↑      		↑
          what just happened   the world now
```

This has a useful consequence: if you miss a frame, you are only ever one event behind, and the next frame's `state` brings you fully back in sync. There is no resend mechanism, and none is needed.

## `outSeq` is for gap detection only

`outSeq` is a counter that increases by one on every frame. Use it to spot missed frames: a gap means you missed something. It is not a way to request a resend — if you need the missing events, re-backfill from `/history` and re-subscribe.

## Sessions

Every endpoint includes a `{session}` segment in its path. Each session is a separate match: a press on one never affects another, and your consumer token works across every session you are allowed to read.

Session ids are short alphanumeric slugs, one per match. An id that doesn't match the expected format returns `404` before the token is even checked.

## The A/B side model

On the wire, teams are always `A` or `B` — never named. The display names are set when the session is created and travel in the top-level `matchup` `{a, b}` on every frame and on the `/api/history` and `/api/health` responses, not in the game state. Every team-attributed field — the score, basketball's `possession`, baseball's run credit, soccer's card counts — refers to the abstract `A` and `B` sides. The sides are stable for the whole game: `A` and `B` keep pointing at the same teams even when the teams switch ends at half-time (soccer) or the batting side alternates each half-inning (baseball).

Some sports extend the side model into space. Soccer's `zone` field marks which half the ball is in: `a_half` is the half where Team A's goal is (Team B attacks into it), and `inBox` adds depth — `zone: "a_half", inBox: true` means Team B is in a dangerous spot inside A's box. See each sport's page for its own state fields.

## Pulses vs. open/resolve

The event vocabulary uses two patterns, in every sport.

**Pulses.** Most events are self-contained facts: basketball's `possession` says who has the ball, baseball's `run` credits a run, soccer's `goal` scores one. Apply the frame and move on — nothing is left pending.

**Open then resolve.** Some events open a state that stays open until a later event closes it. For example:

* Basketball: `2pt_attempt` / `3pt_attempt` / `ft_attempt` (the ball is in the air) → `made` or `miss`
* Soccer: `penalty_awarded` → `pen_kick` → `pen_scored` or `pen_missed`
* Soccer: `var_review` → any corrections (each one leaves the review open) → `var_resolve` (the one event that closes it)
* Baseball: `pitch` (a play opens) → `end` (the play closes)

The snapshot always shows what is currently open (basketball `openAttempt`, soccer `pendingSetPiece` / `openVar`, baseball `live`), so a consumer that reconnects sees the current state without replaying the event log. At most one of a given kind is ever open at a time, so there's never an ambiguity to resolve: the next resolving event always closes the one thing that's currently open.

Because of that, there's no separate id to thread across frames — each sport's resolving event already carries what it needs:

* **Basketball** — the resolving `made` / `miss` self-describes the shot it resolved on its own payload (`team`, `shot_type`); no lookup needed.
* **Soccer** — a VAR review's identity, if you need it (e.g. `"v2"`), is on `state.openVar.varId` for every frame the review is open; `var_resolve` clears it.
* **Baseball / football** — the open thing is a plain flag (`live`, `flagOpen`), not an id'd pair.

## Advisories

Some frames come from the server itself rather than from a scout. The liveness monitor sends `scout_status` frames to tell you whether the scout is still active. They look like normal frames, but with `event.type === "scout_status"` and `event.seq === null` (no `seq`, because no scout pressed anything). The payload is `{ "status": "dark" | "live", "lastHeartbeat": number | null }`.

* `"dark"` — the scout's heartbeat hasn't been seen recently. Expect no new events until they reconnect.
* `"live"` — the scout is active again.

`lastHeartbeat` is the Unix timestamp (seconds) of the last heartbeat from the scout, or `null` if none has been seen yet for this session.

Advisory frames still include the full match snapshot in `state`, so there is nothing to re-sync.

## Reconnection

The close code decides whether to come back — some closes are deliberate refusals that will never change, and looping on them is a bug:

* **`4000` — the game is over; stop.** The normal end of **every** session, preceded by a `{"type": "session_closed"}` control message. Not a failure — do not reconnect; the full tape stays readable via `GET /api/history/{session}`.
* **`4404` / `4403` — stop.** These arrive after a `{"type": "error", "code": ...}` frame ([close codes](/api-reference/websocket#close-codes)): the game has ended, was never published, or your account is not entitled to it. Reconnecting gets the same refusal forever. Fetch the tape from `GET /api/history/{session}` instead (finished games stay replayable), or re-check your entitlements.
* **A failed handshake — fix the credential, then retry.** A missing or invalid token rejects the upgrade request itself with HTTP `403`, before any WebSocket exists (a browser observes a failed connection / `1006` with no server frames; a non-browser client sees the `403` response). Retrying with the same token loops.
* **`1011` — the server's fault; retry with backoff.** It arrives after a `{"type": "error", "code": "unavailable"}` frame: the server could not verify any credential (its auth infrastructure is down or misconfigured). Your token was never examined — do not rotate it; reconnect with backoff until the server recovers.
* **Anything else — transient; reconnect with backfill.** A network drop (`1006` mid-stream), a server restart, a proxy timeout:
  1. Re-fetch the event log from `GET /api/history/{session}` to fill any tape rows you missed (history returns events only — your state re-syncs from the snapshot on the first frame after the socket reopens).
  2. Re-open the WebSocket to start receiving live frames again.
  3. Before appending, remove duplicates against your existing tape by `seq` — events you already have will appear again in the backfill.
