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

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

Event Types

Events are namespaced by sport (nba.*, mlb.*). A single endpoint can subscribe to events from multiple sports.

NBA

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

MLB

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

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:

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

Start Receiving Live Events

Set up your first webhook in minutes. Free tier includes 100 deliveries per month.