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 or programmatically via API key. The full API is covered by an OpenAPI spec — hand it to your AI coding agent and let it handle setup, monitoring, and receiver scaffolding.
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 |
NBA
Events are namespaced as nba.*. A single endpoint can subscribe to events from multiple sports.
Event Types
| 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 |
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 |
Game Events
Events: nba.game.started, nba.game.ended, nba.game.overtime. The same {"game": {"id": ...}} structure applies to game.started and game.ended across all sports.
{
"event_type": "nba.game.started",
"game": {
"id": 12345
}
}Period Ended
Event: nba.game.period_ended
{
"event_type": "nba.game.period_ended",
"game": {
"id": 12345
},
"ended_period": 2
}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",
"wallclock": "2026-03-15T02:32:15Z",
"home_score": 78,
"away_score": 72
},
"player": {
"id": 678,
"first_name": "LeBron",
"last_name": "James",
"position": "F",
"team_id": 14
}
}Injury Events
Events: nba.injury.created, nba.injury.updated, nba.injury.cleared
Injury events are not tied to a specific game. They fire whenever the NBA injury report changes — a player is added, their status changes, or they are cleared. The created and updated events include current injury details, while updated and cleared include the previous injury for diffing.
{
"event_type": "nba.injury.created",
"player": {
"id": 678,
"first_name": "LeBron",
"last_name": "James",
"position": "F",
"team_id": 14
},
"injury": {
"status": "Out",
"description": "Left ankle sprain",
"return_date": "2026-03-15"
}
}{
"event_type": "nba.injury.updated",
"player": {
"id": 678,
"first_name": "LeBron",
"last_name": "James",
"position": "F",
"team_id": 14
},
"injury": {
"status": "Doubtful",
"description": "Left ankle sprain",
"return_date": "2026-03-20"
},
"previous_injury": {
"status": "Out",
"description": "Left ankle sprain",
"return_date": "2026-03-15"
}
}{
"event_type": "nba.injury.cleared",
"player": {
"id": 678,
"first_name": "LeBron",
"last_name": "James",
"position": "F",
"team_id": 14
},
"previous_injury": {
"status": "Doubtful",
"description": "Left ankle sprain",
"return_date": "2026-03-20"
}
}MLB
Events are namespaced as mlb.*. A single endpoint can subscribe to events from multiple sports.
Event Types
| 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 |
mlb.injury.created | Player added to injury report | ALL-ACCESS |
mlb.injury.updated | Injury details changed | ALL-ACCESS |
mlb.injury.cleared | Player removed from injury report | ALL-ACCESS |
Game Events
Events: mlb.game.started, mlb.game.ended, mlb.game.extra_innings
{
"event_type": "mlb.game.started",
"game": {
"id": 67890
}
}Inning Half Ended
Event: mlb.game.inning_half_ended
{
"event_type": "mlb.game.inning_half_ended",
"game": {
"id": 67890
},
"inning": 5,
"inning_half": "top"
}Inning Ended
Event: mlb.game.inning_ended
{
"event_type": "mlb.game.inning_ended",
"game": {
"id": 67890
},
"inning": 5
}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",
"wallclock": "2026-07-15T23:45:12Z",
"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
}
}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",
"wallclock": "2026-07-15T23:45:12Z",
"home_score": 3,
"away_score": 2
},
"team_id": 10
}Injury Events
Events: mlb.injury.created, mlb.injury.updated, mlb.injury.cleared
Injury events are not tied to a specific game. They fire whenever the MLB injury report changes. The injury payload includes sport-specific fields like type, detail, and side.
{
"event_type": "mlb.injury.created",
"player": {
"id": 456,
"first_name": "Mike",
"last_name": "Trout",
"team_id": 1
},
"injury": {
"status": "60-Day IL",
"type": "Knee",
"detail": "Meniscus",
"side": "Left",
"return_date": "2026-08-15T00:00:00Z",
"long_comment": "Underwent surgery, expected back in August",
"short_comment": "Knee surgery"
}
}{
"event_type": "mlb.injury.updated",
"player": {
"id": 456,
"first_name": "Mike",
"last_name": "Trout",
"team_id": 1
},
"injury": {
"status": "15-Day IL",
"type": "Knee",
"detail": "Meniscus",
"side": "Left",
"return_date": "2026-07-20T00:00:00Z",
"long_comment": "Progressing ahead of schedule",
"short_comment": "Knee"
},
"previous_injury": {
"status": "60-Day IL",
"type": "Knee",
"detail": "Meniscus",
"side": "Left",
"return_date": "2026-08-15T00:00:00Z",
"long_comment": "Underwent surgery, expected back in August",
"short_comment": "Knee surgery"
}
}{
"event_type": "mlb.injury.cleared",
"player": {
"id": 456,
"first_name": "Mike",
"last_name": "Trout",
"team_id": 1
},
"previous_injury": {
"status": "15-Day IL",
"type": "Knee",
"detail": "Meniscus",
"side": "Left",
"return_date": "2026-07-20T00:00:00Z",
"long_comment": "Progressing ahead of schedule",
"short_comment": "Knee"
}
}NHL
Events are namespaced as nhl.*. A single endpoint can subscribe to events from multiple sports.
Event Types
| Event Type | Description | Plan |
|---|---|---|
nhl.game.started | Game begins | ALL-ACCESS |
nhl.game.ended | Game reaches final | ALL-ACCESS |
nhl.game.period_ended | Period ends | ALL-ACCESS |
nhl.game.overtime | Game enters overtime | ALL-ACCESS |
nhl.player.goal | Player scores a goal | ALL-ACCESS |
nhl.player.assist | Player records an assist | ALL-ACCESS |
nhl.player.penalty | Player receives a penalty | ALL-ACCESS |
nhl.player.shot | Player records a shot on goal | ALL-ACCESS |
nhl.team.goal | Team scores a goal | ALL-ACCESS |
nhl.injury.created | Player added to injury report | ALL-ACCESS |
nhl.injury.updated | Injury details changed | ALL-ACCESS |
nhl.injury.cleared | Player removed from injury report | ALL-ACCESS |
Game Events
Events: nhl.game.started, nhl.game.ended, nhl.game.overtime
{
"event_type": "nhl.game.started",
"game": {
"id": 178532
}
}Period Ended
Event: nhl.game.period_ended
{
"event_type": "nhl.game.period_ended",
"game": {
"id": 178532
},
"ended_period": 2
}Player Events
Events: nhl.player.goal, nhl.player.assist, nhl.player.shot
{
"event_type": "nhl.player.goal",
"game": {
"id": 178532
},
"play": {
"type": "goal",
"period": 2,
"period_type": "REG",
"time_in_period": "14:22",
"time_remaining": "05:38",
"home_score": 3,
"away_score": 1
},
"player": {
"id": 1234,
"first_name": "Connor",
"last_name": "McDavid",
"position": "C",
"team_id": 56
}
}Penalty
Event: nhl.player.penalty
{
"event_type": "nhl.player.penalty",
"game": {
"id": 178532
},
"play": {
"type": "penalty",
"period": 1,
"period_type": "REG",
"time_in_period": "08:15",
"time_remaining": "11:45",
"home_score": 1,
"away_score": 0,
"penalty_type": "tripping",
"penalty_minutes": 2
},
"player": {
"id": 9012,
"first_name": "Tom",
"last_name": "Wilson",
"position": "RW",
"team_id": 23
}
}Team Goal
Event: nhl.team.goal
{
"event_type": "nhl.team.goal",
"game": {
"id": 178532
},
"play": {
"type": "goal",
"period": 2,
"period_type": "REG",
"time_in_period": "14:22",
"time_remaining": "05:38",
"home_score": 3,
"away_score": 1
},
"team_id": 56,
"scorer": {
"id": 1234,
"first_name": "Connor",
"last_name": "McDavid",
"position": "C",
"team_id": 56
}
}Injury Events
Events: nhl.injury.created, nhl.injury.updated, nhl.injury.cleared
Injury events are not tied to a specific game. They fire whenever the NHL injury report changes. The injury payload includes fields like status_abbreviation, injury_type, and comment.
{
"event_type": "nhl.injury.created",
"player": {
"id": 1234,
"first_name": "Connor",
"last_name": "McDavid",
"position": "C",
"team_id": 56
},
"injury": {
"status": "Out",
"status_abbreviation": "O",
"injury_type": "Upper Body",
"return_date": "2026-04-10",
"comment": "Day-to-day with upper body injury"
}
}{
"event_type": "nhl.injury.updated",
"player": {
"id": 1234,
"first_name": "Connor",
"last_name": "McDavid",
"position": "C",
"team_id": 56
},
"injury": {
"status": "Day-To-Day",
"status_abbreviation": "DTD",
"injury_type": "Upper Body",
"return_date": null,
"comment": "Participated in practice"
},
"previous_injury": {
"status": "Out",
"status_abbreviation": "O",
"injury_type": "Upper Body",
"return_date": "2026-04-10",
"comment": "Day-to-day with upper body injury"
}
}{
"event_type": "nhl.injury.cleared",
"player": {
"id": 1234,
"first_name": "Connor",
"last_name": "McDavid",
"position": "C",
"team_id": 56
},
"previous_injury": {
"status": "Day-To-Day",
"status_abbreviation": "DTD",
"injury_type": "Upper Body",
"return_date": null,
"comment": "Participated in practice"
}
}NCAAB
Events are namespaced as ncaab.*. A single endpoint can subscribe to events from multiple sports.
Event Types
| Event Type | Description | Plan |
|---|---|---|
ncaab.game.started | Game begins | ALL-ACCESS |
ncaab.game.ended | Game reaches final | ALL-ACCESS |
ncaab.game.period_ended | Half ends | ALL-ACCESS |
ncaab.game.overtime | Game enters overtime | ALL-ACCESS |
ncaab.player.scored | Player scores | ALL-ACCESS |
ncaab.player.rebound | Player gets a rebound | ALL-ACCESS |
ncaab.player.assist | Player records an assist | ALL-ACCESS |
ncaab.player.steal | Player records a steal | ALL-ACCESS |
ncaab.player.block | Player records a block | ALL-ACCESS |
ncaab.player.foul | Player commits a foul | ALL-ACCESS |
ncaab.player.turnover | Player commits a turnover | ALL-ACCESS |
Game Events
Events: ncaab.game.started, ncaab.game.ended, ncaab.game.overtime
{
"event_type": "ncaab.game.started",
"game": {
"id": 401638901
}
}Period Ended
Event: ncaab.game.period_ended
{
"event_type": "ncaab.game.period_ended",
"game": {
"id": 401638901
},
"ended_period": 1
}Player Events
Events: ncaab.player.scored, ncaab.player.rebound, ncaab.player.assist, ncaab.player.steal, ncaab.player.block, ncaab.player.foul, ncaab.player.turnover
{
"event_type": "ncaab.player.scored",
"game": {
"id": 401638901
},
"play": {
"type": "Made Shot",
"text": "Cooper Flagg makes driving layup",
"score_value": 2,
"period": 1,
"clock": "12:45",
"home_score": 14,
"away_score": 10
},
"player": {
"id": 5678,
"name": "Cooper Flagg",
"position": "F",
"team_id": 150
}
}NCAAW
Events are namespaced as ncaaw.*. A single endpoint can subscribe to events from multiple sports.
Event Types
| Event Type | Description | Plan |
|---|---|---|
ncaaw.game.started | Game begins | ALL-ACCESS |
ncaaw.game.ended | Game reaches final | ALL-ACCESS |
ncaaw.game.period_ended | Quarter ends | ALL-ACCESS |
ncaaw.game.overtime | Game enters overtime | ALL-ACCESS |
ncaaw.player.scored | Player scores | ALL-ACCESS |
ncaaw.player.rebound | Player gets a rebound | ALL-ACCESS |
ncaaw.player.assist | Player records an assist | ALL-ACCESS |
ncaaw.player.steal | Player records a steal | ALL-ACCESS |
ncaaw.player.block | Player records a block | ALL-ACCESS |
ncaaw.player.foul | Player commits a foul | ALL-ACCESS |
ncaaw.player.turnover | Player commits a turnover | ALL-ACCESS |
NCAAW uses the same payload structure as NCAAB.
ATP Tennis
Events are namespaced as atp.*. A single endpoint can subscribe to events from multiple sports.
Event Types
| Event Type | Description | Plan |
|---|---|---|
atp.match.started | Match begins | ALL-ACCESS |
atp.match.ended | Match concludes | ALL-ACCESS |
atp.match.set_ended | A set completes | ALL-ACCESS |
atp.match.set_score_updated | Game score within a set changes | ALL-ACCESS |
atp.match.game_score_updated | Point score within a game changes | ALL-ACCESS |
Match Events
ATP tennis uses match-level events instead of play-by-play. Events track match state changes including set completions, game scores within sets, and point scores within games.
Events: atp.match.started, atp.match.ended
{
"event_type": "atp.match.started",
"match": {
"id": 2101712,
"tournament": {
"id": 5001,
"name": "Australian Open",
"surface": "Hard",
"category": "Grand Slam"
},
"season": 2026,
"scheduled_time": "2026-01-20T09:00:00Z",
"round": "Round of 128",
"court_name": "Rod Laver Arena",
"score": "0-0",
"number_of_sets": 3,
"duration": null,
"player1_game_score": "0",
"player2_game_score": "0",
"match_status": "in_progress"
},
"player1": {
"id": 2015927,
"first_name": "Novak",
"last_name": "Djokovic",
"country": "Serbia"
},
"player2": {
"id": 2234167,
"first_name": "Carlos",
"last_name": "Alcaraz",
"country": "Spain"
}
}Set Ended
Event: atp.match.set_ended
{
"event_type": "atp.match.set_ended",
"match": { "...base match object..." },
"player1": { "..." },
"player2": { "..." },
"set_number": 2,
"set_score": "3-6"
}Set Score Updated
Event: atp.match.set_score_updated. Fires when a game is won within a set.
{
"event_type": "atp.match.set_score_updated",
"match": {
"...base match...",
"score": "6-4 3-1"
},
"player1": { "..." },
"player2": { "..." },
"current_set": 2,
"current_set_score": "3-1"
}Game Score Updated
Event: atp.match.game_score_updated. Fires on every point within a game.
{
"event_type": "atp.match.game_score_updated",
"match": {
"...base match...",
"player1_game_score": "30",
"player2_game_score": "15"
},
"player1": { "..." },
"player2": { "..." },
"player1_game_score": "30",
"player2_game_score": "15"
}WTA Tennis
Events are namespaced as wta.*. A single endpoint can subscribe to events from multiple sports.
Event Types
| Event Type | Description | Plan |
|---|---|---|
wta.match.started | Match begins | ALL-ACCESS |
wta.match.ended | Match concludes | ALL-ACCESS |
wta.match.set_ended | A set completes | ALL-ACCESS |
wta.match.set_score_updated | Game score within a set changes | ALL-ACCESS |
wta.match.game_score_updated | Point score within a game changes | ALL-ACCESS |
Match Events
WTA tennis uses the same event structure as ATP. Events track match state changes (started, ended) and score updates at the set, game, and point level.
Events: wta.match.started, wta.match.ended
{
"event_type": "wta.match.started",
"match": {
"id": 3066199,
"tournament": {
"id": 1234,
"name": "Australian Open",
"surface": "Hard",
"category": "Grand Slam"
},
"season": 2026,
"scheduled_time": "2026-01-25T08:30:00Z",
"round": "F",
"court_name": "Rod Laver Arena",
"score": "0-0",
"number_of_sets": 3,
"duration": null,
"player1_game_score": "0",
"player2_game_score": "0",
"match_status": "m"
},
"player1": {
"id": 1001,
"first_name": "Iga",
"last_name": "Swiatek",
"country": "Poland"
},
"player2": {
"id": 1002,
"first_name": "Aryna",
"last_name": "Sabalenka",
"country": "Belarus"
}
}Set Ended
Event: wta.match.set_ended
{
"event_type": "wta.match.set_ended",
"match": { "...base match object..." },
"player1": { "..." },
"player2": { "..." },
"set_number": 1,
"set_score": "6-4"
}Set Score Updated
Event: wta.match.set_score_updated. Fires when a game is won within a set.
{
"event_type": "wta.match.set_score_updated",
"match": {
"...base match...",
"score": "6-4 3-1"
},
"player1": { "..." },
"player2": { "..." },
"current_set": 2,
"current_set_score": "3-1"
}Game Score Updated
Event: wta.match.game_score_updated. Fires on every point within a game.
{
"event_type": "wta.match.game_score_updated",
"match": {
"...base match...",
"player1_game_score": "30",
"player2_game_score": "15"
},
"player1": { "..." },
"player2": { "..." },
"player1_game_score": "30",
"player2_game_score": "15"
}Soccer
Seven soccer leagues share the same event types. Each league uses its own namespace prefix.
| League | Prefix |
|---|---|
| English Premier League | epl.* |
| La Liga | laliga.* |
| Serie A | seriea.* |
| Champions League | ucl.* |
| Bundesliga | bundesliga.* |
| Ligue 1 | ligue1.* |
| MLS | mls.* |
EPL (English Premier League)
| Event Type | Description | Plan |
|---|---|---|
epl.game.started | Game begins | ALL-ACCESS |
epl.game.ended | Game reaches final | ALL-ACCESS |
epl.game.halftime | First half ends | ALL-ACCESS |
epl.game.second_half_started | Second half begins | ALL-ACCESS |
epl.game.extra_time | Extra time begins | ALL-ACCESS |
epl.player.goal | Player scores a goal | ALL-ACCESS |
epl.player.yellow_card | Player receives a yellow card | ALL-ACCESS |
epl.player.red_card | Player receives a red card | ALL-ACCESS |
epl.player.substitution | Player substitution | ALL-ACCESS |
La Liga
| Event Type | Description | Plan |
|---|---|---|
laliga.game.started | Game begins | ALL-ACCESS |
laliga.game.ended | Game reaches final | ALL-ACCESS |
laliga.game.halftime | First half ends | ALL-ACCESS |
laliga.game.second_half_started | Second half begins | ALL-ACCESS |
laliga.game.extra_time | Extra time begins | ALL-ACCESS |
laliga.player.goal | Player scores a goal | ALL-ACCESS |
laliga.player.yellow_card | Player receives a yellow card | ALL-ACCESS |
laliga.player.red_card | Player receives a red card | ALL-ACCESS |
laliga.player.substitution | Player substitution | ALL-ACCESS |
Serie A
| Event Type | Description | Plan |
|---|---|---|
seriea.game.started | Game begins | ALL-ACCESS |
seriea.game.ended | Game reaches final | ALL-ACCESS |
seriea.game.halftime | First half ends | ALL-ACCESS |
seriea.game.second_half_started | Second half begins | ALL-ACCESS |
seriea.game.extra_time | Extra time begins | ALL-ACCESS |
seriea.player.goal | Player scores a goal | ALL-ACCESS |
seriea.player.yellow_card | Player receives a yellow card | ALL-ACCESS |
seriea.player.red_card | Player receives a red card | ALL-ACCESS |
seriea.player.substitution | Player substitution | ALL-ACCESS |
UCL (Champions League)
| Event Type | Description | Plan |
|---|---|---|
ucl.game.started | Game begins | ALL-ACCESS |
ucl.game.ended | Game reaches final | ALL-ACCESS |
ucl.game.halftime | First half ends | ALL-ACCESS |
ucl.game.second_half_started | Second half begins | ALL-ACCESS |
ucl.game.extra_time | Extra time begins | ALL-ACCESS |
ucl.player.goal | Player scores a goal | ALL-ACCESS |
ucl.player.yellow_card | Player receives a yellow card | ALL-ACCESS |
ucl.player.red_card | Player receives a red card | ALL-ACCESS |
ucl.player.substitution | Player substitution | ALL-ACCESS |
Bundesliga
| Event Type | Description | Plan |
|---|---|---|
bundesliga.game.started | Game begins | ALL-ACCESS |
bundesliga.game.ended | Game reaches final | ALL-ACCESS |
bundesliga.game.halftime | First half ends | ALL-ACCESS |
bundesliga.game.second_half_started | Second half begins | ALL-ACCESS |
bundesliga.game.extra_time | Extra time begins | ALL-ACCESS |
bundesliga.player.goal | Player scores a goal | ALL-ACCESS |
bundesliga.player.yellow_card | Player receives a yellow card | ALL-ACCESS |
bundesliga.player.red_card | Player receives a red card | ALL-ACCESS |
bundesliga.player.substitution | Player substitution | ALL-ACCESS |
Ligue 1
| Event Type | Description | Plan |
|---|---|---|
ligue1.game.started | Game begins | ALL-ACCESS |
ligue1.game.ended | Game reaches final | ALL-ACCESS |
ligue1.game.halftime | First half ends | ALL-ACCESS |
ligue1.game.second_half_started | Second half begins | ALL-ACCESS |
ligue1.game.extra_time | Extra time begins | ALL-ACCESS |
ligue1.player.goal | Player scores a goal | ALL-ACCESS |
ligue1.player.yellow_card | Player receives a yellow card | ALL-ACCESS |
ligue1.player.red_card | Player receives a red card | ALL-ACCESS |
ligue1.player.substitution | Player substitution | ALL-ACCESS |
MLS
| Event Type | Description | Plan |
|---|---|---|
mls.game.started | Game begins | ALL-ACCESS |
mls.game.ended | Game reaches final | ALL-ACCESS |
mls.game.halftime | First half ends | ALL-ACCESS |
mls.game.second_half_started | Second half begins | ALL-ACCESS |
mls.game.extra_time | Extra time begins | ALL-ACCESS |
mls.player.goal | Player scores a goal | ALL-ACCESS |
mls.player.yellow_card | Player receives a yellow card | ALL-ACCESS |
mls.player.red_card | Player receives a red card | ALL-ACCESS |
mls.player.substitution | Player substitution | ALL-ACCESS |
Soccer Player Events
Events: epl.player.goal, epl.player.yellow_card, epl.player.red_card, epl.player.substitution (same structure for all soccer leagues: laliga, seriea, ucl, bundesliga, ligue1, mls)
{
"event_type": "epl.player.goal",
"game": {
"id": 54321,
"home_team": {
"id": 100,
"name": "Arsenal"
},
"away_team": {
"id": 200,
"name": "Chelsea"
},
"date": "2026-03-15T15:00:00Z",
"season": 2025,
"home_score": 2,
"away_score": 1,
"status": "STATUS_SECOND_HALF"
},
"player": {
"id": 300,
"name": "Bukayo Saka",
"team_id": 100
},
"event_time": 67,
"period": 2,
"goal_type": "regular",
"is_own_goal": false
}PGA Golf
Events are namespaced as pga.*. PGA webhooks track tournament lifecycle and individual player scoring on a hole-by-hole basis.
Event Types
| Event Type | Description | Plan |
|---|---|---|
pga.tournament.started | Tournament begins | ALL-ACCESS |
pga.tournament.ended | Tournament concludes | ALL-ACCESS |
pga.tournament.round_started | A new round begins | ALL-ACCESS |
pga.player.hole_completed | Player completes a hole (includes birdie, eagle, bogey, etc.) | ALL-ACCESS |
pga.player.round_completed | Player finishes all 18 holes in a round | ALL-ACCESS |
Tournament Events
Tournament lifecycle events fire when the tournament status transitions.
Events: pga.tournament.started, pga.tournament.ended
{
"event_type": "pga.tournament.started",
"tournament": {
"id": 16,
"name": "THE PLAYERS Championship",
"season": 2026,
"status": "IN_PROGRESS"
}
}Round Started
Event: pga.tournament.round_started. Fires when the first scorecard of a new round is recorded.
{
"event_type": "pga.tournament.round_started",
"tournament": {
"id": 16,
"name": "THE PLAYERS Championship",
"season": 2026,
"status": "IN_PROGRESS"
},
"round": 2
}Hole Completed
Event: pga.player.hole_completed. Fires for every completed hole. The result field classifies the score (hole_in_one, eagle, birdie, par, bogey, double_bogey_plus).
{
"event_type": "pga.player.hole_completed",
"tournament": {
"id": 16,
"name": "THE PLAYERS Championship",
"season": 2026,
"status": "IN_PROGRESS"
},
"player": {
"id": 185,
"first_name": "Scottie",
"last_name": "Scheffler",
"country": "United States"
},
"scorecard": {
"round": 1,
"hole": 17,
"par": 3,
"score": 2,
"score_to_par": -1,
"result": "birdie",
"tee_time": "2026-03-13T12:45:00Z"
}
}Round Completed
Event: pga.player.round_completed. Fires when a player finishes all 18 holes in a round.
{
"event_type": "pga.player.round_completed",
"tournament": {
"id": 16,
"name": "THE PLAYERS Championship",
"season": 2026,
"status": "IN_PROGRESS"
},
"player": {
"id": 185,
"first_name": "Scottie",
"last_name": "Scheffler",
"country": "United States"
},
"round": 1,
"score": 68,
"par_relative_score": -4
}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
AI & Agents
The Webhooks API is fully programmable via API key, which means AI coding agents can manage your entire webhook lifecycle — creating endpoints, subscribing to events, building receivers, and monitoring delivery health — without you touching the dashboard.
OpenAPI Specification
Give your agent the Webhooks OpenAPI spec and it has everything it needs to work with the API: endpoint schemas, authentication, request/response formats, and event type definitions.
What an Agent Can Do
- Set up endpoints — create webhook endpoints pointed at your server URL and subscribe to the event types you need
- Scaffold a receiver — generate a server (Express, Flask, FastAPI, etc.) that accepts webhook POSTs, verifies signatures, and routes events by type
- Monitor health — check
GET /webhooks/v1/usagefor delivery counts andGET /webhooks/v1/endpoints/:id/deliveries?status=failedfor failures - Self-heal — retry failed deliveries, rotate secrets, re-enable disabled endpoints, and adjust event subscriptions
Example Prompt
Paste this into your AI coding tool (Claude Code, Cursor, Windsurf, Copilot, etc.) to get started:
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:
- Accepts POST /webhooks/bdl
- Verifies the HMAC-SHA256 signature using the endpoint secret
- Logs a message for each scoring play with the player name and score
- Returns 200 on success
3. Send a test event to verify it worksTips for Agent Workflows
- The OpenAPI spec includes all 130+ event types. Point your agent at
GET /webhooks/v1/event-typesto discover which events are available for your subscription tier. - Store the
secretfrom the create endpoint response immediately — it is only returned on creation and onPOST /rotate-secret. - Use
POST /endpoints/:id/testto send a test event and verify your receiver before live games start. - Agents can build event-driven automations on top of webhooks: Discord bots, Slack alerts, SMS notifications, database pipelines, betting triggers, and more.
Start Receiving Live Events
Set up your first webhook in minutes. Free tier includes 100 deliveries per month.