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

FeatureFreeALL-ACCESS
Endpoints110
Deliveries per month100500,000
Available eventsFree events onlyAll events
Retry attempts35
Delivery log retention3 days30 days
Manual retryNoYes

NBA

Events are namespaced as nba.*. A single endpoint can subscribe to events from multiple sports.

Event Types

Event TypeDescriptionPlan
nba.game.startedGame beginsFree
nba.game.endedGame reaches finalFree
nba.game.period_endedQuarter endsALL-ACCESS
nba.game.overtimeGame enters overtimeALL-ACCESS
nba.player.scoredPlayer scoresALL-ACCESS
nba.player.reboundPlayer gets a reboundALL-ACCESS
nba.player.assistPlayer records an assistALL-ACCESS
nba.player.stealPlayer records a stealALL-ACCESS
nba.player.blockPlayer records a blockALL-ACCESS
nba.player.foulPlayer commits a foulALL-ACCESS
nba.player.turnoverPlayer commits a turnoverALL-ACCESS
nba.injury.createdPlayer added to injury reportALL-ACCESS
nba.injury.updatedInjury details changedALL-ACCESS
nba.injury.clearedPlayer removed from injury reportALL-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 TypeDescriptionPlan
mlb.game.startedGame beginsALL-ACCESS
mlb.game.endedGame reaches finalALL-ACCESS
mlb.game.inning_half_endedHalf-inning ends (top or bottom)ALL-ACCESS
mlb.game.inning_endedFull inning ends (after bottom half)ALL-ACCESS
mlb.game.extra_inningsGame enters extra inningsALL-ACCESS
mlb.batter.hitBatter records a hitALL-ACCESS
mlb.batter.home_runBatter hits a home runALL-ACCESS
mlb.batter.strikeoutBatter strikes outALL-ACCESS
mlb.batter.walkBatter walksALL-ACCESS
mlb.batter.hit_by_pitchBatter is hit by a pitchALL-ACCESS
mlb.team.scoredTeam scores a runALL-ACCESS
mlb.injury.createdPlayer added to injury reportALL-ACCESS
mlb.injury.updatedInjury details changedALL-ACCESS
mlb.injury.clearedPlayer removed from injury reportALL-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 TypeDescriptionPlan
nhl.game.startedGame beginsALL-ACCESS
nhl.game.endedGame reaches finalALL-ACCESS
nhl.game.period_endedPeriod endsALL-ACCESS
nhl.game.overtimeGame enters overtimeALL-ACCESS
nhl.player.goalPlayer scores a goalALL-ACCESS
nhl.player.assistPlayer records an assistALL-ACCESS
nhl.player.penaltyPlayer receives a penaltyALL-ACCESS
nhl.player.shotPlayer records a shot on goalALL-ACCESS
nhl.team.goalTeam scores a goalALL-ACCESS
nhl.injury.createdPlayer added to injury reportALL-ACCESS
nhl.injury.updatedInjury details changedALL-ACCESS
nhl.injury.clearedPlayer removed from injury reportALL-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 TypeDescriptionPlan
ncaab.game.startedGame beginsALL-ACCESS
ncaab.game.endedGame reaches finalALL-ACCESS
ncaab.game.period_endedHalf endsALL-ACCESS
ncaab.game.overtimeGame enters overtimeALL-ACCESS
ncaab.player.scoredPlayer scoresALL-ACCESS
ncaab.player.reboundPlayer gets a reboundALL-ACCESS
ncaab.player.assistPlayer records an assistALL-ACCESS
ncaab.player.stealPlayer records a stealALL-ACCESS
ncaab.player.blockPlayer records a blockALL-ACCESS
ncaab.player.foulPlayer commits a foulALL-ACCESS
ncaab.player.turnoverPlayer commits a turnoverALL-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 TypeDescriptionPlan
ncaaw.game.startedGame beginsALL-ACCESS
ncaaw.game.endedGame reaches finalALL-ACCESS
ncaaw.game.period_endedQuarter endsALL-ACCESS
ncaaw.game.overtimeGame enters overtimeALL-ACCESS
ncaaw.player.scoredPlayer scoresALL-ACCESS
ncaaw.player.reboundPlayer gets a reboundALL-ACCESS
ncaaw.player.assistPlayer records an assistALL-ACCESS
ncaaw.player.stealPlayer records a stealALL-ACCESS
ncaaw.player.blockPlayer records a blockALL-ACCESS
ncaaw.player.foulPlayer commits a foulALL-ACCESS
ncaaw.player.turnoverPlayer commits a turnoverALL-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 TypeDescriptionPlan
atp.match.startedMatch beginsALL-ACCESS
atp.match.endedMatch concludesALL-ACCESS
atp.match.set_endedA set completesALL-ACCESS
atp.match.set_score_updatedGame score within a set changesALL-ACCESS
atp.match.game_score_updatedPoint score within a game changesALL-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 TypeDescriptionPlan
wta.match.startedMatch beginsALL-ACCESS
wta.match.endedMatch concludesALL-ACCESS
wta.match.set_endedA set completesALL-ACCESS
wta.match.set_score_updatedGame score within a set changesALL-ACCESS
wta.match.game_score_updatedPoint score within a game changesALL-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.

LeaguePrefix
English Premier Leagueepl.*
La Ligalaliga.*
Serie Aseriea.*
Champions Leagueucl.*
Bundesligabundesliga.*
Ligue 1ligue1.*
MLSmls.*

EPL (English Premier League)

Event TypeDescriptionPlan
epl.game.startedGame beginsALL-ACCESS
epl.game.endedGame reaches finalALL-ACCESS
epl.game.halftimeFirst half endsALL-ACCESS
epl.game.second_half_startedSecond half beginsALL-ACCESS
epl.game.extra_timeExtra time beginsALL-ACCESS
epl.player.goalPlayer scores a goalALL-ACCESS
epl.player.yellow_cardPlayer receives a yellow cardALL-ACCESS
epl.player.red_cardPlayer receives a red cardALL-ACCESS
epl.player.substitutionPlayer substitutionALL-ACCESS

La Liga

Event TypeDescriptionPlan
laliga.game.startedGame beginsALL-ACCESS
laliga.game.endedGame reaches finalALL-ACCESS
laliga.game.halftimeFirst half endsALL-ACCESS
laliga.game.second_half_startedSecond half beginsALL-ACCESS
laliga.game.extra_timeExtra time beginsALL-ACCESS
laliga.player.goalPlayer scores a goalALL-ACCESS
laliga.player.yellow_cardPlayer receives a yellow cardALL-ACCESS
laliga.player.red_cardPlayer receives a red cardALL-ACCESS
laliga.player.substitutionPlayer substitutionALL-ACCESS

Serie A

Event TypeDescriptionPlan
seriea.game.startedGame beginsALL-ACCESS
seriea.game.endedGame reaches finalALL-ACCESS
seriea.game.halftimeFirst half endsALL-ACCESS
seriea.game.second_half_startedSecond half beginsALL-ACCESS
seriea.game.extra_timeExtra time beginsALL-ACCESS
seriea.player.goalPlayer scores a goalALL-ACCESS
seriea.player.yellow_cardPlayer receives a yellow cardALL-ACCESS
seriea.player.red_cardPlayer receives a red cardALL-ACCESS
seriea.player.substitutionPlayer substitutionALL-ACCESS

UCL (Champions League)

Event TypeDescriptionPlan
ucl.game.startedGame beginsALL-ACCESS
ucl.game.endedGame reaches finalALL-ACCESS
ucl.game.halftimeFirst half endsALL-ACCESS
ucl.game.second_half_startedSecond half beginsALL-ACCESS
ucl.game.extra_timeExtra time beginsALL-ACCESS
ucl.player.goalPlayer scores a goalALL-ACCESS
ucl.player.yellow_cardPlayer receives a yellow cardALL-ACCESS
ucl.player.red_cardPlayer receives a red cardALL-ACCESS
ucl.player.substitutionPlayer substitutionALL-ACCESS

Bundesliga

Event TypeDescriptionPlan
bundesliga.game.startedGame beginsALL-ACCESS
bundesliga.game.endedGame reaches finalALL-ACCESS
bundesliga.game.halftimeFirst half endsALL-ACCESS
bundesliga.game.second_half_startedSecond half beginsALL-ACCESS
bundesliga.game.extra_timeExtra time beginsALL-ACCESS
bundesliga.player.goalPlayer scores a goalALL-ACCESS
bundesliga.player.yellow_cardPlayer receives a yellow cardALL-ACCESS
bundesliga.player.red_cardPlayer receives a red cardALL-ACCESS
bundesliga.player.substitutionPlayer substitutionALL-ACCESS

Ligue 1

Event TypeDescriptionPlan
ligue1.game.startedGame beginsALL-ACCESS
ligue1.game.endedGame reaches finalALL-ACCESS
ligue1.game.halftimeFirst half endsALL-ACCESS
ligue1.game.second_half_startedSecond half beginsALL-ACCESS
ligue1.game.extra_timeExtra time beginsALL-ACCESS
ligue1.player.goalPlayer scores a goalALL-ACCESS
ligue1.player.yellow_cardPlayer receives a yellow cardALL-ACCESS
ligue1.player.red_cardPlayer receives a red cardALL-ACCESS
ligue1.player.substitutionPlayer substitutionALL-ACCESS

MLS

Event TypeDescriptionPlan
mls.game.startedGame beginsALL-ACCESS
mls.game.endedGame reaches finalALL-ACCESS
mls.game.halftimeFirst half endsALL-ACCESS
mls.game.second_half_startedSecond half beginsALL-ACCESS
mls.game.extra_timeExtra time beginsALL-ACCESS
mls.player.goalPlayer scores a goalALL-ACCESS
mls.player.yellow_cardPlayer receives a yellow cardALL-ACCESS
mls.player.red_cardPlayer receives a red cardALL-ACCESS
mls.player.substitutionPlayer substitutionALL-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 TypeDescriptionPlan
pga.tournament.startedTournament beginsALL-ACCESS
pga.tournament.endedTournament concludesALL-ACCESS
pga.tournament.round_startedA new round beginsALL-ACCESS
pga.player.hole_completedPlayer completes a hole (includes birdie, eagle, bogey, etc.)ALL-ACCESS
pga.player.round_completedPlayer finishes all 18 holes in a roundALL-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:

HeaderExampleDescription
Content-Typeapplication/jsonAlways JSON
User-AgentBDL-Webhook/1.0BallDontLie webhook user agent
X-BDL-Webhook-Idevt_550e8400...Unique event ID (for deduplication)
X-BDL-Webhook-Timestamp1706108400Unix timestamp of this delivery attempt
X-BDL-Webhook-Signaturev1=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.

  1. Extract the timestamp from X-BDL-Webhook-Timestamp
  2. Construct the signed message: {timestamp}.{raw_body}
  3. Compute HMAC-SHA256 using your endpoint secret
  4. 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:

AttemptDelay
1st retry30 seconds
2nd retry2 minutes
3rd retry10 minutes
4th retry30 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

StatusDescription
pendingQueued, waiting for first attempt or next retry
deliveringCurrently being sent
deliveredSuccessfully delivered (2xx response)
failedLast attempt failed, retries remaining
exhaustedAll retry attempts used, delivery abandoned

Getting Started

  1. Sign up at app.balldontlie.io
  2. Navigate to the Webhooks tab in your dashboard
  3. Create an endpoint with your URL and desired event types
  4. Send a test event to verify your endpoint is receiving payloads
  5. 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.

Download webhooks-openapi.yml

What an Agent Can Do

  1. Set up endpoints — create webhook endpoints pointed at your server URL and subscribe to the event types you need
  2. Scaffold a receiver — generate a server (Express, Flask, FastAPI, etc.) that accepts webhook POSTs, verifies signatures, and routes events by type
  3. Monitor health — check GET /webhooks/v1/usage for delivery counts and GET /webhooks/v1/endpoints/:id/deliveries?status=failed for failures
  4. 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 works

Tips for Agent Workflows

  • The OpenAPI spec includes all 130+ event types. Point your agent at GET /webhooks/v1/event-types to discover which events are available for your subscription tier.
  • Store the secret from the create endpoint response immediately — it is only returned on creation and on POST /rotate-secret.
  • Use POST /endpoints/:id/test to 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.