If you've built anything with live sports data, you know the pattern: poll the API every few seconds, compare the response to what you had before, figure out what changed, and react accordingly. It works, but it's wasteful. Most of those requests return the same data. You burn through rate limits, add latency, and write diffing logic that shouldn't be your problem.
Today we're launching webhooks for the BALLDONTLIE API. Instead of asking us "did anything change?" thousands of times per game, you tell us what you care about and we push it to your server the moment it happens.
New to the API? Start with our Getting Started guide first to set up your account and API key.
The Problem with Polling
Consider a typical use case: you want to track NBA scores and notify your users when a game starts. With polling, your code looks something like this:
import requests
import time
API_KEY = "your-api-key"
headers = {"Authorization": API_KEY}
known_started = set()
while True:
response = requests.get(
"https://api.balldontlie.io/nba/v1/games",
headers=headers,
params={"dates[]": "2026-02-22"}
)
for game in response.json()["data"]:
if game["status"] != "Final" and game["id"] not in known_started:
if "Qtr" in game["status"] or "Half" in game["status"]:
print(f"Game started: {game['visitor_team']['abbreviation']} @ {game['home_team']['abbreviation']}")
known_started.add(game["id"])
time.sleep(30)
This approach has real costs:
- Wasted API calls - On a night with 10 NBA games, you might poll hundreds of times to catch 10 game-start events
- Added latency - If you poll every 30 seconds, you could miss the start by up to 29 seconds
- Diffing logic - You need to track previous state and figure out what changed
- Rate limit pressure - Every poll counts against your quota, even when nothing changed
How Webhooks Work
With webhooks, the flow is reversed. You register a URL, subscribe to event types, and we send you an HTTP POST when something happens. No polling. No diffing. No wasted requests.
Here's what happens when an NBA game starts:
- Our system detects the game status change
- We construct a signed JSON payload
- We POST it to every endpoint subscribed to
nba.game.started - Your server processes the event and responds with a 200
The equivalent of the polling loop above is: register an endpoint, subscribe to nba.game.started, and handle incoming requests. That's it.
Supported Events
Webhooks currently support NBA, MLB, NHL, NCAAB, NCAAW, ATP Tennis, and WTA Tennis, with 65+ event types across all sports.
NBA Events
| Event | Description | Plan |
|---|---|---|
nba.game.started | Game begins | Free |
nba.game.ended | Game reaches final | Free |
nba.game.period_ended | Quarter ends | ALL-ACCESS |
nba.game.overtime | Game enters overtime | ALL-ACCESS |
nba.player.scored | Player scores | ALL-ACCESS |
nba.player.rebound | Player gets a rebound | ALL-ACCESS |
nba.player.assist | Player records an assist | ALL-ACCESS |
nba.player.steal | Player records a steal | ALL-ACCESS |
nba.player.block | Player records a block | ALL-ACCESS |
nba.player.foul | Player commits a foul | ALL-ACCESS |
nba.player.turnover | Player commits a turnover | ALL-ACCESS |
nba.injury.created | Player added to injury report | ALL-ACCESS |
nba.injury.updated | Injury details changed | ALL-ACCESS |
nba.injury.cleared | Player removed from injury report | ALL-ACCESS |
MLB Events
| Event | Description |
|---|---|
mlb.game.started | Game begins |
mlb.game.ended | Game reaches final |
mlb.game.inning_half_ended | Half-inning ends (top or bottom) |
mlb.game.inning_ended | Full inning ends |
mlb.game.extra_innings | Game enters extra innings |
mlb.batter.hit | Batter records a hit |
mlb.batter.home_run | Batter hits a home run |
mlb.batter.strikeout | Batter strikes out |
mlb.batter.walk | Batter walks |
mlb.batter.hit_by_pitch | Batter is hit by a pitch |
mlb.team.scored | Team scores a run |
ATP Tennis Events
| Event | Description |
|---|---|
atp.match.started | Match begins |
atp.match.ended | Match concludes |
atp.match.set_ended | A set completes |
atp.match.set_score_updated | Game score within a set changes |
atp.match.game_score_updated | Point score within a game changes |
WTA Tennis Events
| Event | Description |
|---|---|
wta.match.started | Match begins |
wta.match.ended | Match concludes |
wta.match.set_ended | A set completes |
wta.match.set_score_updated | Game score within a set changes |
wta.match.game_score_updated | Point score within a game changes |
What You Receive
Every webhook delivery is an HTTP POST to your endpoint with a JSON body. The payload structure depends on the event type, but every payload includes event_type at the top level.
Game events (starts, ends, overtime):
{
"event_type": "nba.game.started",
"game": {
"id": 12345
}
}
Player events include play-by-play context:
{
"event_type": "nba.player.scored",
"game": {
"id": 12345
},
"play": {
"type": "Made Shot",
"text": "LeBron James makes driving layup",
"score_value": 2,
"period": 3,
"clock": "4:32",
"home_score": 78,
"away_score": 72
},
"player": {
"id": 678,
"first_name": "LeBron",
"last_name": "James",
"position": "F",
"team_id": 14
}
}
MLB batter events include both batter and pitcher:
{
"event_type": "mlb.batter.home_run",
"game": {
"id": 67890
},
"play": {
"type": "Home Run",
"text": "Aaron Judge homers (25) on a fly ball to left center field.",
"score_value": 1,
"inning": 5,
"inning_half": "top",
"home_score": 3,
"away_score": 2
},
"batter": {
"id": 456,
"first_name": "Aaron",
"last_name": "Judge",
"team_id": 10
},
"pitcher": {
"id": 789,
"first_name": "Brayan",
"last_name": "Bello",
"team_id": 15
}
}
Each delivery also includes headers for signature verification and deduplication:
X-BDL-Webhook-Id- Unique event IDX-BDL-Webhook-Timestamp- Unix timestampX-BDL-Webhook-Signature- HMAC-SHA256 signature
Building a Webhook Receiver
Here's a complete webhook receiver in Node.js. It verifies the signature, parses the event, and logs what happened.
Node.js (Express)
import express from "express";
import crypto from "crypto";
const app = express();
const PORT = 3000;
// Store the raw body for signature verification
app.use(
express.json({
verify: (req: express.Request, _res, buf) => {
(req as any).rawBody = buf.toString();
},
})
);
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || "your-signing-secret";
function verifySignature(
rawBody: string,
timestamp: string,
signature: string
): boolean {
const message = `${timestamp}.${rawBody}`;
const expected =
"v1=" +
crypto.createHmac("sha256", WEBHOOK_SECRET).update(message).digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
app.post("/webhook", (req, res) => {
const timestamp = req.headers["x-bdl-webhook-timestamp"] as string;
const signature = req.headers["x-bdl-webhook-signature"] as string;
const eventId = req.headers["x-bdl-webhook-id"] as string;
const rawBody = (req as any).rawBody as string;
// Verify the signature
if (!timestamp || !signature) {
res.status(401).json({ error: "Missing signature headers" });
return;
}
if (!verifySignature(rawBody, timestamp, signature)) {
res.status(401).json({ error: "Invalid signature" });
return;
}
// Process the event
const event = req.body;
console.log(`[${eventId}] ${event.event_type}`);
switch (event.event_type) {
case "nba.game.started":
console.log(` Game ${event.game.id} has started`);
break;
case "nba.player.scored":
console.log(
` ${event.player.first_name} ${event.player.last_name} scored`
);
console.log(
` ${event.play.text} (${event.play.home_score}-${event.play.away_score})`
);
break;
case "mlb.batter.home_run":
console.log(
` ${event.batter.first_name} ${event.batter.last_name} hit a home run`
);
break;
case "nba.injury.created":
console.log(
` ${event.player.first_name} ${event.player.last_name} added to injury report: ${event.injury.status}`
);
break;
case "nba.injury.cleared":
console.log(
` ${event.player.first_name} ${event.player.last_name} cleared from injury report`
);
break;
default:
console.log(` Payload:`, JSON.stringify(event, null, 2));
}
// Always respond 200 to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(PORT, () => {
console.log(`Webhook receiver listening on port ${PORT}`);
});
Python (Flask)
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "your-signing-secret")
def verify_signature(payload: bytes, headers: dict) -> bool:
timestamp = headers.get("x-bdl-webhook-timestamp", "")
signature = headers.get("x-bdl-webhook-signature", "")
message = f"{timestamp}.{payload.decode()}"
expected = "v1=" + hmac.new(
WEBHOOK_SECRET.encode(), message.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route("/webhook", methods=["POST"])
def handle_webhook():
if not verify_signature(request.data, request.headers):
return jsonify({"error": "Invalid signature"}), 401
event = request.json
event_type = event.get("event_type", "")
event_id = request.headers.get("x-bdl-webhook-id", "unknown")
print(f"[{event_id}] Received: {event_type}")
if event_type == "nba.game.started":
print(f" Game {event['game']['id']} has started")
elif event_type == "nba.player.scored":
player = event["player"]
play = event["play"]
print(f" {player['first_name']} {player['last_name']} scored")
print(f" {play['text']} ({play['home_score']}-{play['away_score']})")
elif event_type == "mlb.batter.home_run":
batter = event["batter"]
print(f" {batter['first_name']} {batter['last_name']} hit a home run")
elif event_type == "nba.injury.created":
player = event["player"]
injury = event["injury"]
print(f" {player['first_name']} {player['last_name']} added to injury report: {injury['status']}")
elif event_type == "nba.injury.cleared":
player = event["player"]
print(f" {player['first_name']} {player['last_name']} cleared from injury report")
return jsonify({"received": True}), 200
if __name__ == "__main__":
app.run(port=3000)
Both examples follow the same pattern:
- Verify the signature using the raw request body, timestamp header, and your signing secret
- Route by event type to handle each event appropriately
- Return 200 immediately to acknowledge receipt
Return 200 as quickly as possible. If you need to do heavy processing, queue the work and respond immediately. We'll retry on non-2xx responses, but your endpoint has a 30-second timeout per delivery.
Use Cases
Webhooks open up workflows that were impractical or impossible with polling:
- Live score notifications - Push game updates to your users via Slack, Discord, SMS, or push notifications the moment they happen
- Fantasy sports alerts - Get notified when a player you're tracking scores, gets an assist, or hits a home run
- Injury monitoring - React immediately when a player is added to or cleared from the injury report
- Betting triggers - React to game events (overtime, period end) to adjust live betting strategies
- Data pipelines - Stream play-by-play events into your database without maintaining a polling loop
- Media and content - Auto-generate social media posts or highlight clips when key events occur
Let Your AI Agent Handle It
The Webhooks API is fully programmable via API key, and we publish an OpenAPI spec covering every endpoint. That means AI coding agents — Claude Code, Cursor, Windsurf, Copilot — can manage your entire webhook lifecycle without you writing a line of code.
Here's an example prompt you can paste into your agent:
I want to receive real-time NBA scoring alerts via BALLDONTLIE webhooks.
Here is the API spec: https://www.balldontlie.io/webhooks-openapi.yml
My API key is: $BALLDONTLIE_API_KEY
Please:
1. Create a webhook endpoint at https://my-server.com/webhooks/bdl
subscribed to nba.player.scored and nba.game.ended
2. Write an Express.js server that verifies the HMAC-SHA256 signature
and logs each scoring play
3. Send a test event to verify it works
The agent reads the spec, creates the endpoint via the API, scaffolds a server with signature verification, and tests the connection. You can extend this to build Discord bots, Slack alerts, database pipelines, or anything else that reacts to live sports events.
For more details on agent workflows and tips, see the AI & Agents section of the webhooks documentation.
Getting Started
Setting up webhooks takes a few minutes:
- Sign up at app.balldontlie.io if you haven't already
- Navigate to the Webhooks tab in your dashboard
- Create an endpoint with your URL and select the event types you want
- Send a test event to verify your server receives and processes payloads correctly
- Store your signing secret securely and implement signature verification
The free tier includes 1 endpoint and 100 deliveries per month with access to nba.game.started and nba.game.ended. ALL-ACCESS subscribers get 10 endpoints, 500,000 deliveries per month, and access to all 65+ event types across NBA, MLB, NHL, NCAAB, NCAAW, ATP Tennis, and WTA Tennis.
For full technical details including retry behavior, delivery statuses, and the complete event type reference, see the webhooks documentation.
Reliability Built In
We know webhooks are only useful if they're reliable. Here's what we've built to ensure your events arrive:
- Automatic retries with exponential backoff (up to 5 attempts for ALL-ACCESS)
- Signed payloads with HMAC-SHA256 so you can verify every delivery is authentic
- Deduplication IDs via the
X-BDL-Webhook-Idheader so you can handle duplicate deliveries safely - Delivery logs in the dashboard so you can inspect payloads, see response codes, and manually retry failed deliveries
- Auto-disable for consistently failing endpoints, preventing wasted resources on both sides
Beyond Game Events
The BALLDONTLIE API covers data across NBA, NFL, MLB, NHL, EPL, WNBA, NCAAF, NCAAB, and Tennis. Webhooks now support NBA, MLB, NHL, NCAAB, NCAAW, ATP Tennis, and WTA Tennis with 65+ event types — including real-time injury report updates for NBA and point-by-point scoring for tennis. The event system is built to be sport-agnostic, so adding new sports and event categories means adding new event types to the same infrastructure.
Next Steps
Ready to stop polling? Here are a few ways to get started:
- Sign up for a free API key and set up your first webhook endpoint
- Read the full webhooks documentation for the complete technical reference
- Download the Webhooks OpenAPI spec and give it to your AI agent
- Check out our NFL scoreboard tutorial or odds comparison guide for more ways to use the API
Need help? Join our Discord community or check the documentation.