DEVELOPER GUIDE
Subscribe to match updates.
Authenticate once, choose match IDs, then receive an initial snapshot followed by current match state.
1. Find a match
Use GET /v1/fixtures in the API explorer to find an observed match ID. Keep every ID as a string, including IDs inside markets. The catalogue is observed feed data, not a historical or full-season directory.
2. Open the connection
Connect to ws://localhost:8080/v1/ws locally. Use wss://your-host/v1/ws when the service is published over HTTPS. Send authentication within five seconds. Do not put credentials in the URL.
{"type":"auth","token":"YOUR_CLIENT_API_KEY"}Wait for {"type":"authenticated"}, then subscribe:
{"type":"subscribe","matchIds":["MATCH_ID_FROM_FIXTURES"]}You can subscribe to up to 16 matches on one connection. An open connection without subscriptions does not keep background ingestion active. Pending and active match subscriptions wake continuous Kafka consumption.
3. Handle the messages
| Type | Meaning |
|---|---|
snapshot | The first full match state for a successful subscription. |
state | The updated full match state. Replace your previously stored state for that match. |
stale | Mark your displayed data as no longer current. Do not continue presenting it as live. |
reset | Discard that match's state. The old subscription has been removed; subscribe again to obtain a new snapshot. |
error | Inspect the error code and optional matchId. Failed subscriptions are not active. |
unsubscribed | The match subscription was removed. |
Snapshots and state frames contain matchId, epoch, revision, fresh, lastCheckTime and event. The timestamp is null if unavailable. Treat epoch changes as a new feed generation. Revision numbers apply within that generation and cached match lifecycle; they are not global Kafka offsets.
Completed matches can arrive with fresh: false. Stale active-match WebSocket subscriptions are rejected. The attempt wakes Kafka; retry the subscription with backoff while the feed catches up. Ordinary keep-alives need not generate a state frame.
4. Unsubscribe or reconnect
{"type":"unsubscribe","matchIds":["MATCH_ID_FROM_FIXTURES"]}After a disconnect, reconnect with backoff, authenticate, and subscribe again. Do not assume updates were replayed during the outage. Slow clients may be disconnected and must obtain new snapshots.
JavaScript example
Run this from a page on the same origin as the bridge, or adjust the URL for your application. Replace the match ID before connecting. This example keeps the key in memory and does not save it in browser storage.
const base = new URL("/v1/ws", location.href);
base.protocol = location.protocol === "https:" ? "wss:" : "ws:";
const token = prompt("Bridge client API key"); // Not the admin password.
const wanted = new Set(["MATCH_ID_FROM_FIXTURES"]); // Keep IDs as strings.
const matches = new Map();
let socket, stopped = false, retryMs = 1000;
const retries = new Map(), retryTimers = new Map();
function retryMatch(id) {
if (!wanted.has(id) || retryTimers.has(id)) return;
const attempt = (retries.get(id) || 0) + 1;
if (attempt > 6) { console.warn("Match needs manual retry", id); return; }
retries.set(id, attempt);
retryTimers.set(id, setTimeout(() => {
retryTimers.delete(id);
if (wanted.has(id)) subscribe([id]);
}, Math.min(1000 * 2 ** attempt, 30000)));
}
function subscribe(ids) {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({type: "subscribe", matchIds: ids}));
}
}
function connect() {
socket = new WebSocket(base);
socket.onopen = () => socket.send(JSON.stringify({type: "auth", token}));
socket.onmessage = ({data}) => {
const frame = JSON.parse(data);
switch (frame.type) {
case "authenticated":
retryMs = 1000;
if (wanted.size) subscribe([...wanted]);
break;
case "snapshot":
case "state":
// Each frame contains the full maintained match state.
clearTimeout(retryTimers.get(frame.matchId));
retryTimers.delete(frame.matchId); retries.delete(frame.matchId);
matches.set(frame.matchId, frame);
console.log(frame.matchId, frame.fresh, frame.lastCheckTime, frame.event);
break;
case "stale":
if (matches.has(frame.matchId)) matches.get(frame.matchId).fresh = false;
console.warn("No longer current", frame.matchId);
break;
case "reset":
matches.delete(frame.matchId);
// The old subscription was removed. Obtain a new snapshot.
retryMatch(frame.matchId);
break;
case "error":
console.warn("Subscription error", frame.matchId, frame.error);
// Failed subscriptions are not active. Retry transient failures only.
if (["feed_unavailable", "feed_stale", "feed_reset", "not_observed",
"snapshot_timeout", "snapshot_failed", "snapshot_behind"].includes(frame.error)) {
retryMatch(frame.matchId);
}
break;
}
};
socket.onclose = event => {
matches.clear();
for (const timer of retryTimers.values()) clearTimeout(timer);
retryTimers.clear();
if (["unauthorized", "auth_timeout"].includes(event.reason)) stopped = true;
if (!stopped) {
setTimeout(connect, retryMs);
retryMs = Math.min(retryMs * 2, 30000);
}
};
}
function unsubscribe(matchId) {
wanted.delete(matchId); matches.delete(matchId);
clearTimeout(retryTimers.get(matchId)); retryTimers.delete(matchId); retries.delete(matchId);
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({type: "unsubscribe", matchIds: [matchId]}));
}
}
function disconnect() { stopped = true; socket?.close(); }
if (token) connect();
Traffic and idle refreshes
After initialization, no active match subscriptions means Kafka rests between the refresh cycles configured in Admin. Each refresh catches up on accumulated messages. A REST match request can fetch an available HTTPS snapshot without keeping Kafka connected. Check its fresh flag and timestamp: HTTP 200 does not promise live data. Fixture lists remain cached between cycles, and their freshness is shown explicitly.