In-play odds move constantly — a goal, a service break, a red card can shift a price within a second — so polling a REST endpoint for updates means either hammering it every few hundred milliseconds or missing changes in between. Server-Sent Events push each change down a single open HTTP connection as it happens, so the client just listens instead of asking.
Coming soon. LineSnap is in private testing and not serving traffic yet. This post describes the architecture; the docs page will flip to Available when it ships.
Apexon team··7 min read
That's the specific problem LineSnap's live feed is built to solve. A public in-play board updates score, clock, and price fields on hundreds of events at once, and on any given second almost none of those changes are ones a particular client cares about. A REST client polling /v1/live once a second to catch a score change spends most of its requests confirming that nothing changed. /v1/stream inverts that: open one connection, and the server only writes to it when something did.
Why SSE and not WebSockets?
Both push data over a persistent connection, but they solve different problems. WebSockets are full-duplex — built for a client that also needs to send frequent messages back, like a chat app or a trading terminal placing orders. A live-scores feed doesn't need that: the client subscribes and reads, and never talks back except to reconnect. Server-Sent Events fit a read-only stream more closely, and they carry a few practical advantages that matter more once a stream is actually running than they sound like they should:
SSE is plain HTTP. It passes through the same proxies, load balancers, and corporate firewalls that already carry your other API calls — no separate protocol upgrade to whitelist.
Reconnection is built into the client. Drop the connection and the browser's EventSource retries on its own; a WebSocket client has to implement that itself.
It's debuggable with curl -N. You can watch the raw event stream in a terminal without a WebSocket client, a browser console, or any tooling beyond curl.
The trade-off is real — SSE can't send anything from client to server on the same connection, and browsers cap how many SSE connections can stay open per origin — but for a feed nobody writes back to, that trade-off costs nothing.
The pipeline
The collector reads a public in-play board and is the only part of the system that talks to it directly. Everything downstream — the API, the archive, the stream — works off what the collector hands it, through two paths that exist for different reasons.
The hot path is a Unix domain socket bus: the collector pushes each update straight into the API process's in-memory store the moment it has one, and /v1/stream wakes up on that push instead of polling for it. The cold path is JSON files on disk — the same data, written on a slower cycle, that the API falls back to if the bus connection drops or the process restarts and needs to seed its store from something. Those files double as the collector's own watchdog signal: a board that stops updating them is a board the collector has stopped harvesting from, not just gone quiet for a moment.
Underneath both paths, a SQLite archive keeps a change-only record of every event — not a snapshot loop, but a row appended each time some field of that event actually changes, starting from the first time LineSnap saw it. It's forward-only by design: there's no history for a match that finished before the archive started watching it, and /v1/history/{event_id} only ever answers with what's accumulated since first sighting.
The API surface
Endpoint shapes are final even though the host they'll run on isn't serving traffic yet:
GET /v1/live and GET /v1/live/{sport} — the current board snapshot, across all sports or filtered to one.
GET /v1/odds — the board odds book, mainlines or full fixture depth.
GET /v1/event/{event_id} — a single event by id, with its odds joined in.
GET /v1/stream — the SSE feed: a full snapshot on connect, then only what changed.
GET /v1/history/{event_id} — the change-only archive for one event.
A raw connection with curl shows the stream as it actually arrives — a hello frame, then a full live snapshot, then live_delta frames carrying only the events that changed since the last one:
Endpoint shapes are final; the host goes live at launch.
Browsers can subscribe with the native EventSource API — which doesn't support setting custom headers, so a key passed as a query parameter is the practical option there instead of the X-API-Key header:
const stream = new EventSource(
"https://live.apexon.dev/v1/stream?api_key=ls_XXXX"
);
stream.addEventListener("live", (e) => {
const { count, events } = JSON.parse(e.data);
render(events); // first frame: full snapshot
});
stream.addEventListener("live_delta", (e) => {
const { upserts, removes } = JSON.parse(e.data);
applyDelta(upserts, removes); // later frames: only what changed
});
Endpoint shapes are final; the host goes live at launch.
Honest scope
This section matters more than the endpoint list, because it's what actually determines whether LineSnap fits what you're building. Three limits, stated plainly instead of buried in a docs footnote:
Board mainlines, not full coupon depth. The odds book reflects the mainline markets carried on the in-play board — not every market and every selection a trading desk would track. If your use case needs full coupon depth across every market type, this isn't that feed.
Not an official data-partner feed. LineSnap reads a public in-play board — the same information a browser would show someone watching the match. It isn't a licensed feed from a league, federation, or official data partner, and it doesn't claim to be; sourcing is labeled as exactly what it is.
Freshness is signaled, not promised. No public-board collector can publish a truthful latency SLA, because the honest number moves with conditions upstream of LineSnap's control. Instead, every response carries a real freshness signal — how long ago the underlying data last changed — so a caller can judge for itself whether what it just got is fresh enough to act on, instead of trusting an unverifiable number. /v1/meta/latency exists for the same reason: it's an operational readout for debugging, not a marketing figure.
FAQ
When does LineSnap launch?
No date is announced. Once testing clears and the collector is ready for real traffic, the coming-soon callout on this post and the LineSnap docs will flip to Available — watch docs.apexon.dev/linesnap/ or the status page for that change.
Will my FinMarkets key work?
No. API keys are scoped per product — a FinMarkets key is prefixed fm_… and a LineSnap key is prefixed ls_…; one doesn't authenticate against the other's endpoints. Each product's key only works against that product's routes.
Which sports are covered?
Whatever the board is carrying at the time — LineSnap doesn't maintain a fixed sports list. GET /v1/sports returns the current catalog with live counts, generated at runtime from what the collector is actually seeing, so it's the source of truth instead of a page that goes stale the next time the board's mix changes.
Is this official league data?
No. LineSnap reads a public in-play board — the same information a browser would show someone watching the match — not a licensed feed from a league, federation, or official data partner. That sourcing is labeled honestly rather than implied to be something it isn't.
Want to know when it ships?
There's no announced launch date. The architecture in this post is what's running in private testing today; when testing clears and the collector is ready for real traffic, this page and the LineSnap docs flip from Coming soon to Available.