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:
| Category | Events |
|---|---|
| Match lifecycle | match.start, match.finish, match.postponed, match.cancelled |
| Periods | period.start, period.end |
| Scoring | score.update, basket.2pt, basket.3pt, basket.freethrow |
| In-game events | foul, 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).