tkTickOdds

Live feed & WebSocket

Real-time in-play events delivered over WebSocket. Requires the Live plan.

WebSocket connection

WS/ws/live?apiKey=YOUR_KEY
const 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

GET/api/v1/live/eventsSnapshot of all live events
GET/api/v1/live/events/{fixtureId}Single live event with full odds
GET/api/v1/live/sportsSports with active live events
GET/api/v1/live/statusLive scraper health

REST 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