Documentation
PulseScore API Docs
One normalized schema across every bookmaker. Same fields, same flat markets[] with canonicalMarket and selections[]. Your parser stays the same whether you call /v3/bet365, /fanduel, or any other bookmaker.
Bookmaker API References
Quick Start
Get live odds data in 3 steps:
- Create an account - Sign up at pulsescore.net (free BASIC plan: 500 requests/month)
- Generate an API key - Go to Dashboard → Generate API Key
- Make your first call - Use the key in the
X-Secretheader
curl -X GET "https://api.pulsescore.net/api/v3/bet365/live-events?sport=soccer" \
-H "X-Secret: YOUR_API_KEY"Authentication
All API requests require an API key passed in the request header:
| Header | Value |
|---|---|
| X-Secret | Your API key from the dashboard |
API keys can be generated and revoked from your dashboard. Keep your keys secret - do not expose them in client-side code.
REST API
The REST API returns JSON in the normalized schema. Every bookmaker exposes the same endpoint paths and response shape — only the base prefix differs:
https://api.pulsescore.net/api/v3/bet365 # Bet365 (normalized)
https://api.pulsescore.net/api/fanduel # Fanduel
https://api.pulsescore.net/api/bwin # Bwin
https://api.pulsescore.net/api/ps3838 # Pinnacle(PS3838)
https://api.pulsescore.net/api/paddypower # Paddy Power
https://api.pulsescore.net/api/{bookmaker} # …same pattern for every bookieThe endpoints below use /api/v3/bet365 as the example base URL. Swap the prefix for any other bookmaker — the response shape is identical. Standard HTTP status codes indicate success or failure.
Schema & Fallback
Every event payload exposes a normalized layer alongside the original bookmaker labels. Use the canonical fields by default — they're stable across bookmakers and safe to switch on in your code. If a canonical value looks wrong or is missing, fall back to the bookmaker's raw label, available on both markets and selections.
Markets
| Field | Use | When to fall back |
|---|---|---|
| canonicalMarket | Normalized market key (e.g. match_winner, total_goals). | Primary identifier — switch on this. |
| period | Normalized period (e.g. fulltime, first_half). | If period looks wrong or unmapped, read rawName. |
| rawName | Original market label from the bookmaker (e.g. "Full Time Result"). | Fallback when canonicalMarket or period are wrong or missing. |
Selections / Outcomes
The same rule applies inside selections[]: use the normalized name, and fall back to rawName if it doesn't match what you expect.
| Field | Use |
|---|---|
| name | Normalized outcome label (e.g. Over 2.5, team name). |
| rawName | Original outcome label from the bookmaker. Use as fallback when name looks wrong. |
Recommended pattern: read canonicalMarket / period / name first, fall back to rawName when the normalized value is unexpected. Report mis-mapped markets to support so the normalization layer can be improved.
Endpoints
Live Events
Fetch all currently live events with real-time odds and market groups.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| sport | string | No | Filter by sport: soccer (default), tennis, basketball, ice-hockey, volleyball, handball, table-tennis, esports (or e-sports — both accepted), american-football, baseball, rugby-league, rugby-union |
Response
[
{
"eventId": "98734521",
"sport": "soccer",
"league": "Premier League",
"home": "Liverpool",
"away": "Man City",
"live": true,
"startTime": "2026-05-20T19:00:00.000Z",
"markets": [
{
"canonicalMarket": "match_winner",
"period": "fulltime",
"selections": [
{ "name": "Liverpool", "decimal": "2.10" },
{ "name": "Draw", "decimal": "3.40" },
{ "name": "Man City", "decimal": "3.25" }
]
},
{
"canonicalMarket": "total_goals",
"period": "fulltime",
"line": "2.5",
"selections": [
{ "name": "Over 2.5", "decimal": "1.75" },
{ "name": "Under 2.5", "decimal": "2.05" }
]
}
]
}
]Get a list of all sports that currently have live events.
Response
[
{ "sport": "soccer", "count": 42 },
{ "sport": "tennis", "count": 18 },
{ "sport": "basketball", "count": 8 }
]Fetch a single live event by its eventId.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| eventId | string | Yes | Event ID from /live-events response |
Response
{
"eventId": "98734521",
"sport": "soccer",
"league": "Premier League",
"home": "Liverpool",
"away": "Man City",
"live": true,
"startTime": "2026-05-20T19:00:00.000Z",
"markets": [ ... ]
}Pre-Match (by Sport)
Pre-match data is organized by sport. Replace {sport} with: soccer (root), tennis, basketball, ice-hockey, american-football, horse-racing, volleyball, handball, table-tennis, esports (or e-sports — both accepted), baseball, greyhounds. Soccer uses the root path (no sport prefix).
Get all available leagues for a sport, paginated.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| page | number | No | Page number (default 1) |
| limit | number | No | Items per page (default 50) |
Response
[
{
"league": "Premier League",
"sport": "soccer",
"live": false,
"eventCount": 10
}
]Get all events for a specific league.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| league | string | Yes | League name from /leagues response |
Response
[
{
"eventId": "45921003",
"sport": "soccer",
"league": "Premier League",
"home": "Arsenal",
"away": "Chelsea",
"live": false,
"startTime": "2026-05-21T15:00:00.000Z",
"markets": [
{
"canonicalMarket": "match_winner",
"period": "fulltime",
"selections": [
{ "name": "Arsenal", "decimal": "2.30" },
{ "name": "Draw", "decimal": "3.20" },
{ "name": "Chelsea", "decimal": "3.10" }
]
}
]
}
]Paginated list of events for a sport.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| page | number | No | Page number (default 1) |
| limit | number | No | Items per page (default 50) |
Response
[
{
"eventId": "45921003",
"sport": "soccer",
"league": "Premier League",
"home": "Arsenal",
"away": "Chelsea",
"live": false,
"startTime": "2026-05-21T15:00:00.000Z",
"markets": [ ... ]
}
]Fetch a single event by its eventId.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| eventId | string | Yes | Event ID from /events or /leagues/:league/events |
Response
{
"eventId": "45921003",
"sport": "soccer",
"league": "Premier League",
"home": "Arsenal",
"away": "Chelsea",
"live": false,
"startTime": "2026-05-21T15:00:00.000Z",
"markets": [ ... ]
}WebSocket
The WebSocket feed provides real-time streaming of all in-play events for a chosen bookmaker and sport. Server pushes a frame every ~1 second containing every live event for the subscribed sport. Available on PRO and MAX plans.
Connection Pattern
Every bookmaker uses the same connection pattern — only the path segment changes. Pass your API key and the desired sport as query parameters:
wss://api.pulsescore.net/api/{bookmaker}/ws/live?key=YOUR_API_KEY&sport=SPORTBet365 uses the versioned path /api/v3/bet365/ws/live; every other bookmaker follows /api/{bookmaker}/ws/live. All frames share the same normalized event schema. Full URLs in the table below.
Endpoints by Bookmaker
| Bookmaker | URL |
|---|---|
| Bet365 | wss://api.pulsescore.net/api/v3/bet365/ws/live |
| Unibet AU | wss://api.pulsescore.net/api/unibetau/ws/live |
| Fanduel | wss://api.pulsescore.net/api/fanduel/ws/live |
| Bwin | wss://api.pulsescore.net/api/bwin/ws/live |
| DraftKings | wss://api.pulsescore.net/api/draftkings/ws/live |
| Ladbrokes | wss://api.pulsescore.net/api/ladbrokes/ws/live |
| Paddy Power | wss://api.pulsescore.net/api/paddypower/ws/live |
| Betfred | wss://api.pulsescore.net/api/betfred/ws/live |
| Betano DE | wss://api.pulsescore.net/api/betano-de/ws/live |
| BetMGM(CO.UK) | wss://api.pulsescore.net/api/betmgm-couk/ws/live |
| BetMGM(NL) | wss://api.pulsescore.net/api/betmgm-nl/ws/live |
All endpoints require ?key=YOUR_API_KEY&sport=SPORT as query parameters.
Valid Sports per Bookmaker
| Bookmaker | Sports |
|---|---|
| Bet365 | soccer, basketball, tennis, ice-hockey, rugby-league, rugby-union, volleyball, handball, table-tennis, e-sports, american-football, baseball, greyhounds, horse-racing, cricket |
| Unibet AU | american-football, australian-rules, baseball, basketball, boxing, cricket, cycling, darts, esports, formula-1, futsal, golf, greyhounds, handball, horse-racing, ice-hockey, lacrosse, mma, motorsports, rugby-league, rugby-union, snooker, soccer, table-tennis, tennis, volleyball |
| Fanduel | american-football, baseball, basketball, boxing, cricket, darts, handball, ice-hockey, mma, rugby-league, rugby-union, snooker, soccer, table-tennis, tennis |
| Bwin | american-football, baseball, basketball, cricket, darts, futsal, handball, ice-hockey, rugby-league, rugby-union, snooker, soccer, table-tennis, tennis, volleyball |
| DraftKings | american-football, australian-rules, baseball, basketball, boxing, cricket, cycling, darts, esports, golf, handball, ice-hockey, lacrosse, mma, motorsports, rugby-league, rugby-union, snooker, soccer, table-tennis, tennis |
| Ladbrokes | american-football, baseball, basketball, boxing, cricket, darts, esports, greyhounds, handball, horse-racing, ice-hockey, mma, rugby-league, rugby-union, soccer, table-tennis, tennis, volleyball |
| Paddy Power | american-football, australian-rules, baseball, basketball, boxing, cricket, darts, esports, golf, greyhounds, handball, horse-racing, ice-hockey, mma, motorsports, rugby-league, rugby-union, snooker, soccer, table-tennis, tennis, volleyball |
| Betfred | baseball, basketball, boxing, cricket, greyhounds, horse-racing, mma, rugby-league, soccer, tennis |
| Betano DE | american-football, baseball, basketball, boxing, cricket, cycling, darts, formula-1, golf, handball, ice-hockey, motorsports, rugby-league, rugby-union, soccer, tennis, volleyball |
| BetMGM(CO.UK) | american-football, baseball, basketball, boxing, cricket, darts, handball, ice-hockey, rugby-league, rugby-union, snooker, soccer, table-tennis, tennis, volleyball |
| BetMGM(NL) | american-football, baseball, basketball, boxing, cricket, darts, handball, ice-hockey, rugby-league, rugby-union, snooker, soccer, table-tennis, tennis, volleyball |
Subscribing to a sport with no live events right now is allowed — the server will simply send empty broadcast frames until events appear. Asking for a sport not listed above closes the connection with code 4004.
Query Parameters
| Param | Required | Description |
|---|---|---|
| key | Yes | Your API key |
| sport | Yes | Sport to stream (soccer, tennis, etc.) |
Message Format
On successful connection, the server sends a confirmation message:
{
"type": "connected",
"bookmaker": "fanduel",
"sport": "soccer",
"plan": "pro",
"validSports": ["soccer", "basketball", "tennis", "ice-hockey", "..."]
}Then every ~1 second, the server pushes a frame containing all live events for the subscribed sport:
{
"sport": "soccer",
"timestamp": 1747767600000,
"count": 12,
"data": [
{
"eventId": "98734521",
"sport": "soccer",
"league": "Premier League",
"home": "Liverpool",
"away": "Man City",
"score": "1-0",
"live": true,
"startTime": "2026-05-20T19:00:00.000Z",
"markets": [
{
"canonicalMarket": "match_winner",
"period": "fulltime",
"selections": [
{ "name": "Liverpool", "decimal": "2.10" },
{ "name": "Draw", "decimal": "3.40" },
{ "name": "Man City", "decimal": "3.25" }
]
}
]
}
]
}Connection Limits
| Plan | Max Connections |
|---|---|
| BASIC (Free) | 0 (REST only) |
| STARTER (€20/mo) | 0 (REST only) |
| PRO | 1 concurrent |
| MAX | 3 concurrent |
Code Examples
cURL
# Fetch live soccer events (normalized response)
curl -X GET "https://api.pulsescore.net/api/v3/bet365/live-events?sport=soccer" \
-H "X-Secret: YOUR_API_KEY"
# Swap the prefix for any other bookmaker — same shape, same paths
curl -X GET "https://api.pulsescore.net/api/fanduel/live-events?sport=soccer" \
-H "X-Secret: YOUR_API_KEY"
# Fetch soccer leagues (pre-match)
curl -X GET "https://api.pulsescore.net/api/v3/bet365/soccer/leagues" \
-H "X-Secret: YOUR_API_KEY"
# Fetch events for a league
curl -X GET "https://api.pulsescore.net/api/v3/bet365/soccer/leagues/Premier%20League/events" \
-H "X-Secret: YOUR_API_KEY"
# Fetch tennis leagues
curl -X GET "https://api.pulsescore.net/api/v3/bet365/tennis/leagues" \
-H "X-Secret: YOUR_API_KEY"
# Fetch a single event by eventId
curl -X GET "https://api.pulsescore.net/api/v3/bet365/soccer/events/98734521" \
-H "X-Secret: YOUR_API_KEY"Python
import requests
API_KEY = "YOUR_API_KEY"
# Swap this prefix for any bookmaker — the parser below stays the same
BASE_URL = "https://api.pulsescore.net/api/v3/bet365"
# BASE_URL = "https://api.pulsescore.net/api/fanduel"
# BASE_URL = "https://api.pulsescore.net/api/bwin"
# Fetch live events
response = requests.get(
f"{BASE_URL}/live-events",
headers={"X-Secret": API_KEY},
params={"sport": "soccer"}
)
events = response.json()
for event in events:
home, away = event["home"], event["away"]
for market in event["markets"]:
print(f"{home} vs {away} — {market['canonicalMarket']}")
for sel in market["selections"]:
print(f" {sel['name']}: {sel['decimal']}")Node.js
const API_KEY = "YOUR_API_KEY";
// Swap this prefix for any bookmaker — the parser below stays the same
const BASE_URL = "https://api.pulsescore.net/api/v3/bet365";
// const BASE_URL = "https://api.pulsescore.net/api/fanduel";
// const BASE_URL = "https://api.pulsescore.net/api/bwin";
// Fetch live events
const response = await fetch(`${BASE_URL}/live-events?sport=soccer`, {
headers: { "X-Secret": API_KEY },
});
const events = await response.json();
for (const event of events) {
console.log(`${event.home} vs ${event.away}`);
for (const market of event.markets) {
console.log(` ${market.canonicalMarket}:`);
for (const sel of market.selections) {
console.log(` ${sel.name}: ${sel.decimal}`);
}
}
}WebSocket (Node.js)
const WebSocket = require("ws");
const API_KEY = "YOUR_API_KEY";
// All bookmakers with live WebSocket support
const ENDPOINTS = {
bet365: "wss://api.pulsescore.net/api/v3/bet365/ws/live",
unibetau: "wss://api.pulsescore.net/api/unibetau/ws/live",
fanduel: "wss://api.pulsescore.net/api/fanduel/ws/live",
bwin: "wss://api.pulsescore.net/api/bwin/ws/live",
draftkings: "wss://api.pulsescore.net/api/draftkings/ws/live",
ladbrokes: "wss://api.pulsescore.net/api/ladbrokes/ws/live",
paddypower: "wss://api.pulsescore.net/api/paddypower/ws/live",
betfred: "wss://api.pulsescore.net/api/betfred/ws/live",
betano-de: "wss://api.pulsescore.net/api/betano-de/ws/live",
betmgm-couk: "wss://api.pulsescore.net/api/betmgm-couk/ws/live",
betmgm-nl: "wss://api.pulsescore.net/api/betmgm-nl/ws/live",
};
const bookmaker = "fanduel"; // bet365 | unibetau | fanduel | bwin | draftkings | ladbrokes | paddypower | betfred | betano-de | betmgm-couk | betmgm-nl
const sport = "soccer";
const ws = new WebSocket(
`${ENDPOINTS[bookmaker]}?key=${API_KEY}&sport=${sport}`
);
ws.on("open", () => {
console.log(`Connected to ${bookmaker} (${sport})`);
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.type === "connected") {
console.log(`Subscribed to ${msg.bookmaker} ${msg.sport} (plan: ${msg.plan})`);
console.log(`Valid sports: ${msg.validSports.join(", ")}`);
return;
}
// Broadcast frame
console.log(`${msg.count} live ${msg.sport} events`);
for (const event of msg.data) {
console.log(` ${event.home} vs ${event.away}`);
}
});
ws.on("close", (code, reason) => {
console.log(`Disconnected: ${code} ${reason}`);
});WebSocket (Python)
import asyncio
import json
import websockets
API_KEY = "YOUR_API_KEY"
# All bookmakers with live WebSocket support
ENDPOINTS = {
"bet365": "wss://api.pulsescore.net/api/v3/bet365/ws/live",
"unibetau": "wss://api.pulsescore.net/api/unibetau/ws/live",
"fanduel": "wss://api.pulsescore.net/api/fanduel/ws/live",
"bwin": "wss://api.pulsescore.net/api/bwin/ws/live",
"draftkings": "wss://api.pulsescore.net/api/draftkings/ws/live",
"ladbrokes": "wss://api.pulsescore.net/api/ladbrokes/ws/live",
"paddypower": "wss://api.pulsescore.net/api/paddypower/ws/live",
"betfred": "wss://api.pulsescore.net/api/betfred/ws/live",
"betano-de": "wss://api.pulsescore.net/api/betano-de/ws/live",
"betmgm-couk": "wss://api.pulsescore.net/api/betmgm-couk/ws/live",
"betmgm-nl": "wss://api.pulsescore.net/api/betmgm-nl/ws/live",
}
BOOKMAKER = "fanduel" # bet365 | unibetau | fanduel | bwin | draftkings | ladbrokes | paddypower | betfred | betano-de | betmgm-couk | betmgm-nl
SPORT = "soccer"
URL = f"{ENDPOINTS[BOOKMAKER]}?key={API_KEY}&sport={SPORT}"
async def stream():
async with websockets.connect(URL) as ws:
print(f"Connected to {BOOKMAKER} ({SPORT})")
async for message in ws:
msg = json.loads(message)
if msg.get("type") == "connected":
print(f"Subscribed to {msg['bookmaker']} {msg['sport']} (plan: {msg['plan']})")
print(f"Valid sports: {', '.join(msg['validSports'])}")
continue
# Broadcast frame
for event in msg["data"]:
print(f"{event['home']} vs {event['away']}")
asyncio.run(stream())Error Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 401 | Invalid or missing API key |
| 403 | Access denied (plan restriction) |
| 404 | Resource not found |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
WebSocket Close Codes
| Code | Meaning | Retry? |
|---|---|---|
| 4001 | Authentication failed | No |
| 4003 | Plan too low (PRO or MAX required) | No |
| 4004 | Invalid sport for this bookmaker | No |
| 4010 | Subscription expired | No |
| 4011 | Plan downgrade - reconnect required | Yes |
| 4012 | Session replaced by new connection | Yes |
| 4029 | Connection limit reached | No |
Rate Limits
| Plan | Requests | Rate (per bookmaker) | WebSocket | Price |
|---|---|---|---|---|
| BASIC | 500/month | 1 req/sec | None | Free |
| STARTER | 30,000/month | 1 req/min | None | €20/mo |
| PRO | Unlimited | 1 req/sec | 1 connection | €79/mo |
| MAX | Unlimited | 3 req/sec | 3 connections | €149/mo |
Rate-limited requests return HTTP 429. Implement exponential backoff for retries. WebSocket connections automatically receive updates - no polling needed.