Webhooks
Receive HTTP POST notifications in real time as game events happen
Overview
Webhooks deliver HTTP POST requests to your server when game events occur. Instead of polling the API, you register an endpoint URL and subscribe to the event types you care about. When an event fires, we send a signed JSON payload to your URL.
Manage webhooks through the dashboard.
Data Delay
Events may be delayed up to 1 minute from real time.
Plan Limits
| Feature | Free | ALL-ACCESS |
|---|---|---|
| Endpoints | 1 | 10 |
| Deliveries per month | 100 | 500,000 |
| Available events | Free events only | All events |
| Retry attempts | 3 | 5 |
| Delivery log retention | 3 days | 30 days |
| Manual retry | No | Yes |
Event Types
Events are namespaced by sport (nba.*, mlb.*). A single endpoint can subscribe to events from multiple sports.
NBA
| Event Type | 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 |
MLB
| Event Type | Description | Plan |
|---|---|---|
mlb.game.started | Game begins | ALL-ACCESS |
mlb.game.ended | Game reaches final | ALL-ACCESS |
mlb.game.inning_half_ended | Half-inning ends (top or bottom) | ALL-ACCESS |
mlb.game.inning_ended | Full inning ends (after bottom half) | ALL-ACCESS |
mlb.game.extra_innings | Game enters extra innings | ALL-ACCESS |
mlb.batter.hit | Batter records a hit | ALL-ACCESS |
mlb.batter.home_run | Batter hits a home run | ALL-ACCESS |
mlb.batter.strikeout | Batter strikes out | ALL-ACCESS |
mlb.batter.walk | Batter walks | ALL-ACCESS |
mlb.batter.hit_by_pitch | Batter is hit by a pitch | ALL-ACCESS |
mlb.team.scored | Team scores a run | ALL-ACCESS |
Event Payloads
Each delivery sends a JSON body directly as the HTTP POST body. The event_type field is included at the top level of every payload. Event metadata (ID, timestamp, signature) is sent via headers, not in the body.
Game-Only Events
Events: nba.game.started, nba.game.ended, nba.game.overtime, mlb.game.started, mlb.game.ended, mlb.game.extra_innings
{
"event_type": "nba.game.started",
"game": {
"id": 12345
}
}NBA Period Ended
Event: nba.game.period_ended
{
"event_type": "nba.game.period_ended",
"game": {
"id": 12345
},
"ended_period": 2
}MLB Inning Half Ended
Event: mlb.game.inning_half_ended
{
"event_type": "mlb.game.inning_half_ended",
"game": {
"id": 67890
},
"inning": 5,
"inning_half": "top"
}MLB Inning Ended
Event: mlb.game.inning_ended
{
"event_type": "mlb.game.inning_ended",
"game": {
"id": 67890
},
"inning": 5
}NBA Player Events
Events: nba.player.scored, nba.player.rebound, nba.player.assist, nba.player.steal, nba.player.block, nba.player.foul, nba.player.turnover
{
"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
Events: mlb.batter.hit, mlb.batter.home_run, mlb.batter.strikeout, mlb.batter.walk, mlb.batter.hit_by_pitch
{
"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
}
}MLB Team Scoring
Event: mlb.team.scored
{
"event_type": "mlb.team.scored",
"game": {
"id": 67890
},
"play": {
"type": "Scoring Play",
"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
},
"team_id": 10
}Webhook Headers
Every delivery includes the following headers:
| Header | Example | Description |
|---|---|---|
Content-Type | application/json | Always JSON |
User-Agent | BDL-Webhook/1.0 | BallDontLie webhook user agent |
X-BDL-Webhook-Id | evt_550e8400... | Unique event ID (for deduplication) |
X-BDL-Webhook-Timestamp | 1706108400 | Unix timestamp of this delivery attempt |
X-BDL-Webhook-Signature | v1=abc123... | HMAC-SHA256 signature |
Signature Verification
Verify that webhook deliveries are authentic by computing an HMAC-SHA256 signature and comparing it to the X-BDL-Webhook-Signature header.
- Extract the timestamp from
X-BDL-Webhook-Timestamp - Construct the signed message:
{timestamp}.{raw_body} - Compute HMAC-SHA256 using your endpoint secret
- Compare against the signature (after the
v1=prefix) using a constant-time comparison
JavaScript
const crypto = require('crypto');
function verifyWebhook(payload, headers, secret) {
const timestamp = headers['x-bdl-webhook-timestamp'];
const signature = headers['x-bdl-webhook-signature'];
const message = `${timestamp}.${payload}`;
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Python
import hmac
import hashlib
def verify_webhook(payload: bytes, headers: dict, secret: str) -> bool:
timestamp = headers['x-bdl-webhook-timestamp']
signature = headers['x-bdl-webhook-signature']
message = f"{timestamp}.{payload.decode()}"
expected = 'v1=' + hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Retries & Auto-disable
If your endpoint returns a non-2xx status code or the request times out (30s), the delivery is retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes |
| 3rd retry | 10 minutes |
| 4th retry | 30 minutes |
Free accounts get 3 total attempts (initial + 2 retries). ALL-ACCESS accounts get 5 total attempts (initial + 4 retries).
Auto-disable
Endpoints are automatically disabled after 2 consecutive deliveries where all retry attempts are exhausted. Re-enable them from the dashboard.
Delivery Statuses
| Status | Description |
|---|---|
pending | Queued, waiting for first attempt or next retry |
delivering | Currently being sent |
delivered | Successfully delivered (2xx response) |
failed | Last attempt failed, retries remaining |
exhausted | All retry attempts used, delivery abandoned |
Getting Started
- Sign up at app.balldontlie.io
- Navigate to the Webhooks tab in your dashboard
- Create an endpoint with your URL and desired event types
- Send a test event to verify your endpoint is receiving payloads
- Store your signing secret securely and implement signature verification
Start Receiving Live Events
Set up your first webhook in minutes. Free tier includes 100 deliveries per month.