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:

  1. Our system detects the game status change
  2. We construct a signed JSON payload
  3. We POST it to every endpoint subscribed to nba.game.started
  4. 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

EventDescriptionPlan
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

MLB Events

EventDescription
mlb.game.startedGame begins
mlb.game.endedGame reaches final
mlb.game.inning_half_endedHalf-inning ends (top or bottom)
mlb.game.inning_endedFull inning ends
mlb.game.extra_inningsGame enters extra innings
mlb.batter.hitBatter records a hit
mlb.batter.home_runBatter hits a home run
mlb.batter.strikeoutBatter strikes out
mlb.batter.walkBatter walks
mlb.batter.hit_by_pitchBatter is hit by a pitch
mlb.team.scoredTeam scores a run

ATP Tennis Events

EventDescription
atp.match.startedMatch begins
atp.match.endedMatch concludes
atp.match.set_endedA set completes
atp.match.set_score_updatedGame score within a set changes
atp.match.game_score_updatedPoint score within a game changes

WTA Tennis Events

EventDescription
wta.match.startedMatch begins
wta.match.endedMatch concludes
wta.match.set_endedA set completes
wta.match.set_score_updatedGame score within a set changes
wta.match.game_score_updatedPoint 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 ID
  • X-BDL-Webhook-Timestamp - Unix timestamp
  • X-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:

  1. Verify the signature using the raw request body, timestamp header, and your signing secret
  2. Route by event type to handle each event appropriately
  3. 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:

  1. Sign up at app.balldontlie.io if you haven't already
  2. Navigate to the Webhooks tab in your dashboard
  3. Create an endpoint with your URL and select the event types you want
  4. Send a test event to verify your server receives and processes payloads correctly
  5. 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-Id header 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:

Need help? Join our Discord community or check the documentation.