Live scores over WebSocket: connect, subscribe, reconnect
Open one connection and receive goals as they happen. Token auth, match subscriptions, and the reconnect logic most teams forget until production breaks.
GOAL API· Engineering

A socket sends bytes only when something happens
Polling asks. A socket tells.
Open one connection, subscribe to the matches you care about, and receive updates as goals land. Your request count stops scaling with the number of matches you watch.
Use a socket when your screen shows live scores right now. Use webhooks when your server reacts to events without anyone watching.
Step 1: get a connection token
Your API key never touches the browser. Ask your own server for a short lived token first.
curl https://api.goal-api.com/v1/ws/token \
-H "Authorization: Bearer $GOAL_API_KEY"
The token expires in about a minute. Fetch a fresh one for every connection attempt. Caching a token and reusing the token an hour later gives you a rejected handshake.
Step 2: connect and authenticate
const ws = new WebSocket(`wss://api.goal-api.com/ws?wsToken=${token}`);
ws.onopen = () => {
ws.send(JSON.stringify({ type: "auth", token }));
};
Wait for auth_success before you subscribe. Subscribing early gets dropped.
Step 3: subscribe to matches
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "auth_success") {
for (const matchId of liveMatchIds) {
ws.send(JSON.stringify({ type: "subscribe", resource: "match", matchId }));
}
}
if (msg.type === "match_update") {
applyScore(msg.data);
}
if (msg.type === "error") {
console.warn("socket error", msg);
}
};
A match_update payload carries the fields you need for a score line:
match_idmatch_hometeam_scorematch_awayteam_scorematch_status
Step 4: reconnect, because sockets die
Networks drop. Laptops sleep. Load balancers close idle connections.
Treat onclose as temporary, never as final. A socket with no reconnect works perfectly in testing and silently dies in production the first time a user switches from wifi to mobile data.
let attempt = 0;
function connect() {
const ws = new WebSocket(url);
ws.onopen = () => { attempt = 0; };
ws.onclose = () => {
// 1s, 2s, 4s, 8s, 16s, then 30s, plus jitter so every open tab
// does not reconnect in lockstep after an outage.
const delays = [1000, 2000, 4000, 8000, 16000, 30000];
const base = delays[Math.min(attempt, delays.length - 1)];
attempt += 1;
setTimeout(connect, base + Math.floor(Math.random() * 1000));
};
}
Three rules keep a reconnect loop healthy:
- Back off exponentially. A tight retry loop turns one blip into a self inflicted outage.
- Add jitter. Without jitter, every client reconnects at the same instant.
- Reset the counter after a successful auth, not after a successful open.
Fetch a new token on every attempt. The old one has expired by then.
Step 5: resubscribe after reconnecting
A new connection knows nothing about your old subscriptions. Send them again after auth_success.
Refetch current scores over REST at the same time. Any goal scored while your socket was down never arrives as a push.
Sockets and webhooks solve different problems
Run both. A socket updates the screen in front of a user. Webhooks keep your database correct when nobody is watching.


