TutorialGetting StartedREST API

Getting Started with the PulseScore API

Make your first PulseScore API call: get a free key, fetch live and pre-match odds as normalized JSON, page through boards, use the WebSocket and handle 429s.

By the PulseScore teamPublished Updated 7 min read

To get started with the PulseScore API, create a free account, generate an API key in the dashboard and send it in the X-Secret header with every request to https://api.pulsescore.net. This guide goes from that first call to paging through odds boards, pre-match leagues, single events, the live WebSocket and rate-limit handling.

What do you need before the first call?

  • A PulseScore account. The BASIC plan is free: 500 requests a month at 1 req/sec per bookmaker, with every bookmaker included.
  • An API key, generated in the dashboard.
  • curl, or any HTTP client that sends Accept-Encoding: gzip.

Paid plans raise the quota and the rate and add the live WebSocket and the Stream API; the pricing page compares them.

Step 1: Create an account and generate a key

Sign up at pulsescore.net, open the Dashboard and click Generate New Key. Copy the key right away: the dashboard shows the full key only once. You can revoke a key and generate a new one at any time.

Keep the key on your server. Anyone who holds it can spend your quota, so never ship it in browser or mobile code.

Step 2: Make your first request

Ask Bet365 for the soccer matches in play right now:

bash
curl --compressed "https://api.pulsescore.net/api/v3/bet365/live-events?sport=soccer" \
  -H "X-Secret: YOUR_API_KEY"

--compressed sends Accept-Encoding: gzip and inflates the reply, which makes a full board 6–14× smaller on the wire. Send that header from your own client too; browsers, Node's fetch, Python requests and Go's net/http already do.

What does the response look like?

Every list endpoint answers with the same envelope: your position in the list plus the items, here events.

json
{
  "total": 17,
  "page": 1,
  "limit": 5,
  "totalPages": 4,
  "hasNextPage": true,
  "hasPrevPage": false,
  "sport": "soccer",
  "events": [
    {
      "eventId": "196565074",
      "sport": "soccer",
      "league": "United Kingdom||England Premier League",
      "home": "Ipswich",
      "away": "Liverpool",
      "score": { "home": "0", "away": "1" },
      "markets": [
        {
          "canonicalMarket": "OVER_UNDER",
          "rawName": "Match Goals",
          "period": "FULL_TIME",
          "isActive": true,
          "marketId": "196565074:OVER_UNDER:FULL_TIME:match-goals",
          "selections": [
            { "canonicalOutcome": "OVER", "rawName": "Over", "odds": 1.5714, "line": 2.5, "isActive": true },
            { "canonicalOutcome": "UNDER", "rawName": "Under", "odds": 2.375, "line": 2.5, "isActive": true }
          ]
        }
      ]
    }
  ]
}

The fields you will use most:

FieldWhat it holds
eventId, league, home, awayWhich match it is; ids are per bookmaker
startTime, live, scoreKick-off and live state; score on in-play events
canonicalMarket, periodNormalized market and period, e.g. MATCH_RESULT, FULL_TIME
canonicalOutcome, odds, lineOutcome (HOME, DRAW, OVER), decimal price, handicap or total

Switch on canonicalMarket, period and canonicalOutcome, and fall back to rawName, the bookmaker's own label, when a normalized value is missing or looks wrong. Some bookmakers also send a slug (the event's path on their site) and, in play, matchClock and periodScores; treat those as optional. The schema section of the docs covers every field.

Step 3: Filter by sport and page through results

/live-events takes an optional sport filter in kebab-case or underscore form (soccer, tennis, ice-hockey, table_tennis). Without it you get every sport. To see which sports are in play right now, with an event count for each, call /live-events/sports.

Lists are paginated with page (default 1) and limit (default 5, maximum 30); keep asking for the next page until hasNextPage is false. Boards change while you page, so an event can slide onto the next page: key what you collect by eventId. In Node.js 18 or later, saved as live.mjs:

javascript
const BASE_URL = "https://api.pulsescore.net/api/v3/bet365";
const headers = {
  "X-Secret": process.env.PULSESCORE_API_KEY,
  "Accept-Encoding": "gzip",
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Every event of a paginated list, 30 per request, about one request a second.
async function fetchAll(path, params = {}) {
  const events = new Map(); // keyed by eventId: boards move while you page
  for (let page = 1; ; page++) {
    const query = new URLSearchParams({ ...params, page, limit: 30 });
    const res = await fetch(`${BASE_URL}${path}?${query}`, { headers });
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const body = await res.json();
    for (const event of body.events) events.set(event.eventId, event);
    if (!body.hasNextPage || body.events.length === 0) break;
    await sleep(1050);
  }
  return [...events.values()];
}

const live = await fetchAll("/live-events", { sport: "soccer" });
console.log(`${live.length} live soccer events`);

The one-second pause fits BASIC (1 req/sec per bookmaker) and PRO (1 req/sec); on STARTER (1 req/min) wait a minute between pages.

Step 4: Get pre-match leagues and events

Pre-match boards are organized by sport, with three list endpoints:

  • GET /{sport}/leagues returns leagues A-Z, each with its events embedded (markets included), paginated.
  • GET /{sport}/leagues/:league/events returns every pre-match event of one league with full markets, in one response.
  • GET /{sport}/events returns the upcoming pre-match events of a sport, soonest first, paginated.

Pass the league exactly as /leagues returns it, URL-encoded. On Bet365 that is the Country||League value:

bash
# Soccer leagues (Bet365 also answers at the root: /api/v3/bet365/leagues)
curl --compressed "https://api.pulsescore.net/api/v3/bet365/soccer/leagues?limit=30" \
  -H "X-Secret: YOUR_API_KEY"

# Every pre-match event of the Premier League
curl --compressed "https://api.pulsescore.net/api/v3/bet365/soccer/leagues/United%20Kingdom%7C%7CEngland%20Premier%20League/events" \
  -H "X-Secret: YOUR_API_KEY"

In code, encodeURIComponent(league) in JavaScript or urllib.parse.quote(league, safe="") in Python does the encoding.

Step 5: Fetch a single event

Two endpoints return one event with every market, wrapped in data:

  • GET /{sport}/events/:eventId for a pre-match event.
  • GET /live-events/events/:eventId for an in-play event.

An unknown id is not a 404: you get { "data": null }, and the live endpoint also answers null once the event is no longer in play. Check for null before you read the markets.

bash
curl --compressed "https://api.pulsescore.net/api/v3/bet365/soccer/events/196565074" \
  -H "X-Secret: YOUR_API_KEY"

Step 6: Switch bookmakers by changing the prefix

PulseScore covers 60+ bookmakers, every plan includes all of them, and each one exposes the same paths and the same normalized schema. Only the prefix changes: Bet365 lives at /api/v3/bet365 and every other bookmaker at /api/{bookmaker}.

text
https://api.pulsescore.net/api/v3/bet365
https://api.pulsescore.net/api/fanduel
https://api.pulsescore.net/api/ps3838
https://api.pulsescore.net/api/betfair-sb
https://api.pulsescore.net/api/{bookmaker}

One difference to remember: on other bookmakers soccer always has its own prefix, as in /api/fanduel/soccer/events, while Bet365 also answers at the root. Each bookmaker has a Swagger page listing its sports and routes at https://api.pulsescore.net/api/{bookmaker}/doc (Bet365: https://api.pulsescore.net/v3/bet365/doc), and the sport availability page shows what every bookmaker covers right now.

Horse racing and greyhounds come as race cards, not league boards: on Bet365 at /sports/horse-racing/races and /greyhounds/races, elsewhere at /horse-racing/races and /greyhounds/races, as in /api/betfair-sb/horse-racing/races. The horse racing odds guide walks through a race card.

Step 7: Stream live odds over WebSocket

On PRO, MAX and ULTRA you can keep a socket open instead of polling /live-events. Pass your key and one sport as query parameters:

text
wss://api.pulsescore.net/api/v3/bet365/ws/live?key=YOUR_API_KEY&sport=soccer

Other bookmakers use wss://api.pulsescore.net/api/{bookmaker}/ws/live. The server first sends a connected message with the bookmaker, the sport, your plan and validSports. Then, about once a second, it pushes a frame with every live event of that sport, with sport, timestamp, count and the events in data, in the same schema as the REST responses. Node.js 22 and later have WebSocket built in:

javascript
const url =
  "wss://api.pulsescore.net/api/v3/bet365/ws/live" +
  `?key=${process.env.PULSESCORE_API_KEY}&sport=soccer`;
const ws = new WebSocket(url);

ws.onmessage = ({ data }) => {
  const msg = JSON.parse(data);
  if (msg.type === "connected") {
    console.log(`Connected to ${msg.bookmaker} ${msg.sport} (${msg.plan})`);
  } else if (msg.type === "error") {
    console.error(msg.message);
  } else {
    console.log(`${msg.count} live ${msg.sport} events`);
  }
};
ws.onclose = ({ code, reason }) => console.log(`Closed: ${code} ${reason}`);

Sockets are counted per bookmaker: PRO allows 1, MAX 3 and ULTRA 6; on BASIC and STARTER the handshake is refused with HTTP 402. A sport the bookmaker doesn't stream closes the socket with 4004 and a bad key with 4001, and 4012 means a newer connection of your own account took the slot, so don't reconnect in a loop on it.

For many bookmakers and sports on one socket, or pre-match boards kept in sync, the Stream API sends each board once and then only what changed, once a second; the Stream API docs cover the protocol.

Step 8: Handle rate limits

Rate limits are counted per bookmaker, so you can poll several bookmakers side by side and pace each one on its own:

PlanRequests per bookmakerMonthly quota
BASIC1 req/sec500
STARTER1 req/min30,000
PRO1 req/secUnlimited
MAX3 req/secUnlimited
ULTRA6 req/secUnlimited

Successful responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; the reset value is the window length in seconds, not a timestamp. Over the limit you get HTTP 429, and its message tells you which of three things happened:

  • Over your plan's rate: the message says how many requests per second or minute your plan allows per bookmaker. Wait one window and carry on at that pace.
  • Key paused: ten 429s within ten seconds on one bookmaker pause the key for 60 seconds. Every request then answers 429 with a Retry-After header, so wait that many seconds.
  • Monthly quota exceeded: BASIC and STARTER have a monthly quota, and retrying won't help until the next month or an upgrade.

Back off on the first 429 and the pause never happens. The Python guide builds a client that paces itself and handles all three cases.

Next steps

Frequently asked questions

Is the PulseScore API free to use?

Yes. The BASIC plan is free and includes every bookmaker, with 500 requests a month at 1 req/sec per bookmaker. Paid plans raise the quota and the rate and add the live WebSocket and the Stream API.

How do I authenticate requests?

Send your API key in the X-Secret header on every REST request; the live WebSocket takes it as the key query parameter instead. Generate and revoke keys in the dashboard, and keep them on your server.

Do all bookmakers return the same JSON format?

Yes. Every bookmaker has the same paths and the same normalized schema, with canonicalMarket, period, canonicalOutcome and decimal odds. Only the URL prefix changes: Bet365 is at /api/v3/bet365 and every other bookmaker at /api/{bookmaker}.

Why am I getting HTTP 429?

Either you went over your plan's per-bookmaker rate, your key is paused for 60 seconds after ten 429s within ten seconds, or a BASIC or STARTER monthly quota is used up. The response message says which, and a paused key also gets a Retry-After header.

How do I get live odds without polling?

On PRO, MAX and ULTRA, open the live WebSocket for a bookmaker and sport: it pushes every live event about once a second. The Stream API goes further, sending each board once and then only the changes, for many bookmakers and sports on one socket.