Live feed & WebSocket
Real-time in-play events delivered over WebSocket. Requires the Live plan.
WebSocket connection
/ws/live?apiKey=YOUR_KEYconst ws = new WebSocket(
"wss://api.tickodds.com/ws/live?apiKey=YOUR_KEY"
);
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(msg);
};Subscription protocol
After connecting, send a subscribe message to filter by sport:
ws.send(JSON.stringify({ subscribe: ["all"] }));
// or filter:
ws.send(JSON.stringify({ subscribe: ["Soccer", "Tennis"] }));
// unsubscribe from a sport:
ws.send(JSON.stringify({ unsubscribe: ["Tennis"] }));Event shape
Live events emit the same marketGroups[] + flat odds[]shape as the prematch match detail endpoint — so one code path handles both pre-match and in-play. See the Prematch docs for the full typed shape.
{
"fixtureId": "191691460",
"sportName": "Soccer",
"home": "Crystal Palace",
"away": "West Ham",
"score": "2-1",
"clock": "67:23",
"period": "2nd Half",
"status": "in_progress", // in_progress | half_time | ended | suspended
"marketGroups": [
{
"name": "Fulltime Result",
"key": "full_time_result",
"industryKey": "h2h_3_way",
"period": "full_game",
"type": "simple",
"selections": [
{ "name": "Crystal Palace", "odds": 1.45, "side": "home", "team": "home", "isMain": true },
{ "name": "Draw", "odds": 4.50, "side": "draw", "team": null, "isMain": true },
{ "name": "West Ham", "odds": 7.00, "side": "away", "team": "away", "isMain": true }
]
}
],
"odds": [
{
"id": "191691460:full_time_result:full_game:home",
"marketKey": "full_time_result",
"industryKey": "h2h_3_way",
"period": "full_game",
"side": "home",
"price": 1.45,
"groupingKey": "full_time_result:full_game"
}
]
}A deprecated legacyOdds field (flat { marketName: { selection: odds } } map) is included for back-compat. New integrations should use odds[] / marketGroups[].
Server message types
snapshot
Sent once on connect — current state of all live events.
{
"type": "snapshot",
"count": 87,
"events": [ { fixtureId, home, away, score, marketGroups, odds, ... } ]
}match_start
A new match has gone live.
{ "type": "match_start", "event": { ... } }update
A score, period, incident, or odds change. The full refreshed event (with updated marketGroups and odds) is included.
{
"type": "update",
"fixtureId": "191691460",
"change": "odds" | "score" | "period" | "incident" | "suspended",
"event": { ... },
// change === "score" → also: "from", "to" (old/new score strings)
// change === "period" → also: "from", "to" (old/new period labels)
// change === "incident" → also: "incident" (e.g. "Goal", "Red Card")
// change === "odds" → also: "selection": { id, name, odds, handicap, suspended }, "marketName"
}match_end
A match has finished.
{ "type": "match_end", "fixtureId": "191691460", "event": { ... } }heartbeat
Sent every 15 seconds to keep the connection alive.
{ "type": "heartbeat", "timestamp": "2026-04-17T12:34:56Z" }REST fallback
/api/v1/live/eventsSnapshot of all live events/api/v1/live/events/{fixtureId}Single live event with full odds/api/v1/live/sportsSports with active live events/api/v1/live/statusLive scraper healthREST is useful for periodic polling, but for real-time updates (sub-second) use WebSocket.
Connection limits
- Max 50 concurrent WebSocket connections per API key
- Server pings clients every 15s; unresponsive clients are disconnected
- WebSocket traffic does not count toward HTTP rate limits