Anmelden API-Schlüssel holen
Tutorials

How to Get Real-Time Basketball Play-by-Play with Webhooks

Illustration of a real-time basketball play-by-play feed with scoring events

Learn how to register webhooks and receive real-time play-by-play events — baskets, fouls, substitutions, timeouts — using Live Basketball API.

Auf dieser Seite

Basketball generates far more in-game events than most sports — every basket, foul, timeout, and substitution is a moment worth capturing for a live app. Rather than polling constantly to catch each one, Live Basketball API pushes them to your server as they happen through a rich set of webhook event types. This guide covers registration, signature verification, and handling each event correctly.

The Full Event Catalogue

Unlike a simple "goal scored" notification, basketball webhooks break scoring and game flow into granular event types:

CategoryEvents
Match lifecyclematch.start, match.finish, match.postponed, match.cancelled
Periodsperiod.start, period.end
Scoringscore.update, basket.2pt, basket.3pt, basket.freethrow
In-game eventsfoul, timeout, substitution

Each delivered notification costs 1 credit — registering, listing, and deleting webhooks themselves is free.

Registering with Specific Events

Subscribe to only what your app actually needs — a simple score ticker doesn't need foul or substitution events, for example:

https://live-basketball-api.com/api/v1/webhook/register?api_key=YOUR_KEY&url=https://yourapp.com/hook&events=match.start,basket.3pt,match.finish&secret=your_secret_here

Passing a secret is optional but recommended — it lets you verify each incoming request actually came from the API rather than a spoofed source.

Verifying the Signature

When a secret is set, every delivery includes an X-Webhook-Signature: sha256=HMAC header. Always verify it before trusting the payload:

const crypto = require('crypto');

app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'] || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).send('Invalid signature');
  }

  const data = JSON.parse(req.body);
  handleEvent(data);
  res.sendStatus(200);
});

The Shared Payload Envelope

Every event type uses the same outer structure — event, timestamp, a match object reflecting game state at that moment, and an event-specific data object:

{
  "event": "basket.3pt",
  "timestamp": 1751235900,
  "match": {
    "id": 15395205,
    "competition": "NBA",
    "home": { "id": 3428, "name": "Boston Celtics" },
    "away": { "id": 3412, "name": "Golden State Warriors" },
    "status": "inprogress",
    "period": 3,
    "score": { "home": 87, "away": 79 }
  },
  "data": {
    "team": "home",
    "player": "Jayson Tatum",
    "game_time": "7'",
    "score": { "home": 87, "away": 79 }
  }
}

Building a Play-by-Play Feed

Since every scoring and in-game event follows the same shape, a single handler can build a readable live feed by branching only on event:

function describeEvent(payload) {
  const { event, data, match } = payload;
  const team = data.team === 'home' ? match.home.name : match.away.name;

  switch (event) {
    case 'basket.2pt':
      return `${data.player} (${team}) scores 2! ${data.game_time}`;
    case 'basket.3pt':
      return `${data.player} (${team}) drains a 3! ${data.game_time}`;
    case 'basket.freethrow':
      return `${data.player} (${team}) hits the free throw. ${data.game_time}`;
    case 'foul':
      return `Foul on ${data.player} (${team}). ${data.game_time}`;
    case 'timeout':
      return `${team} call a timeout. ${data.game_time}`;
    case 'substitution':
      return `${team}: ${data.player} in, ${data.player_out} out. ${data.game_time}`;
    case 'period.start':
      return `${data.period_label} begins.`;
    case 'period.end':
      return `${data.period_label} ends. Score: ${match.score.home}-${match.score.away}`;
    default:
      return null;
  }
}

Handling Substitutions Correctly

The substitution event is the one payload with two player fields instead of one — player is who's coming in, player_out is who's leaving. Getting this backwards is an easy mistake:

// data.player = player entering the game
// data.player_out = player leaving the game
function formatSub(data) {
  return `IN: ${data.player} — OUT: ${data.player_out}`;
}

Detecting Match End and Final Result

The match.finish payload includes a winner field directly in data, so you don't need to compare scores yourself:

if (event === 'match.finish') {
  const { score, winner } = data;
  console.log(`Final: ${match.home.name} ${score.home} - ${score.away} ${match.away.name}`);
  console.log(`Winner: ${winner}`); // "home", "away", or "draw"
}

Reducing Noise for High-Volume Games

A close, foul-heavy game can generate dozens of events. If your UI only needs the scoreboard and period changes, subscribe to a narrow event list rather than all:

&url=match.start,period.start,period.end,match.finish,score.update

This keeps credit usage predictable and avoids overwhelming a simple scoreboard UI with foul-by-foul updates it doesn't display.

Frequently Asked Questions

Does basket.2pt fire separately from score.update on the same basket?

Yes, both fire for the same scoring play — score.update is a general "the score changed" signal, while basket.2pt/basket.3pt/basket.freethrow give you the specific shot type. Subscribe to only the ones your UI actually needs to avoid double-handling the same moment.

Is the player field ever null?

Yes, for events like timeout where no specific player is associated with the action, player is null — always check before rendering a player name.

What does the period field represent for a game in overtime?

Regular quarters are periods 1-4; overtime periods continue counting upward (5, 6, etc.) — check period_label in period events for a human-readable "OT" label rather than inferring it from the number alone.

How many webhooks can I register at once?

Up to 10 active webhooks per account — useful for routing different event subsets to different endpoints (e.g. one for scoring events, one for match lifecycle events).

Artikel teilen:
← Zurück zum Blog