Breaking News Webhook
The Breaking News Webhook is the push-delivery mode of GET /posts: instead of polling on a tight loop, SportFeeds delivers breaking sports news to your server the moment it's classified — usually within seconds. When a post is identified as breaking news, SportFeeds sends an HTTP POST to the endpoint you registered.
Subscriptions are managed from your dashboard under Webhooks: register a target URL, filter by league, news type, status and origin, and reveal your signing secret. This page documents the event contract your endpoint will receive.
1. Register a target URL and select the leagues you want in the dashboard.
2. When a post is classified as news with origin breaking, SportFeeds POSTs a signed event to your URL.
3. Your endpoint verifies the signature, responds 2xx quickly, and processes the event asynchronously.
Each subscription supports optional filters. Leave a filter group empty to receive everything for that dimension; add values to narrow delivery. Filters combine with AND across groups and OR within a group.
- Leagues — e.g.
MLB,NBA,NFL. - News types —
injury,trade,signing,cut,suspension,contract_extension,roster_move,return_from_injury,coaching_change,draft,other. - News status —
verifiedorrumor. - News origin —
breaking(first to report) orrelaying.
Example: a subscription with leagues NFL, news types injury, and status verified receives only verified NFL injury news.
Each event is sent as a POST with Content-Type: application/json.
Requests time out after 10 seconds. Any non-2xx response (or timeout) is retried with exponential backoff (≈1m → 2m → 4m → 8m → 16m) for up to 6 attempts, after which the delivery is marked dead.
Retries reuse the same payload but carry a new X-SportFeeds-Delivery ID — de-duplicate on data.id if you need exactly-once processing.
| Header | Description |
|---|---|
| X-SportFeeds-Event | The event type, e.g. breaking_news. |
| X-SportFeeds-Delivery | Unique delivery ID for this attempt. Use it to de-duplicate retries. |
| X-SportFeeds-Signature | HMAC-SHA256 of the raw request body, formatted as sha256=<hex>. |
{
"event": "breaking_news",
"sent_at": "2026-06-18T14:32:10.482Z",
"data": {
"id": "1799000000000000000",
"post_url": "https://x.com/Source/status/1799000000000000000",
"text": "BREAKING: Star QB ruled out for the season with a knee injury.",
"league": "NFL",
"media_type": "text",
"created_at": "2026-06-18T14:32:08.000Z",
"news_type": "injury",
"news_status": "confirmed",
"news_origin": "breaking",
"news_attribution": "Beat Reporter",
"players": [
{ "sportfeeds": 12345, "name": "Player Name" }
],
"teams": [
{ "sportfeeds": 21, "name": "Team Name" }
],
"event_id": "b1e7c0a2-...-9f",
"alt_sources": [
{
"post_url": "https://x.com/OtherOutlet/status/1799000000000000123",
"source": "@OtherOutlet",
"news_attribution": "per @BeatReporter",
"role": "alt",
"text": "QB out for the year, per @BeatReporter.",
"created_at": "2026-06-18T14:39:02.000Z"
}
]
}
}When a reporter breaks news, other outlets quickly post their own wording of the same story (often "per @reporter"). We deliver a single breaking_news event for the first report and attach the follow-ons as alt_sources. Every event carries an event_id so you can thread related deliveries.
If a later post adds key new information (e.g. the specific injury, trade compensation, or a status change), we send a breaking_news.updateevent that references the original via update_of and includes a short whats_new summary. Active breaking_news subscriptions receive these updates automatically.
{
"event": "breaking_news.update",
"sent_at": "2026-06-18T15:10:44.118Z",
"data": {
"id": "1799000000000000999",
"post_url": "https://x.com/Insider/status/1799000000000000999",
"text": "It's a torn ACL — QB will miss the full season.",
"league": "NFL",
"news_type": "injury",
"news_status": "confirmed",
"event_id": "b1e7c0a2-...-9f",
"update_of": "b1e7c0a2-...-9f",
"whats_new": "It's a torn ACL — QB will miss the full season.",
"players": [ { "sportfeeds": 12345, "name": "Player Name" } ],
"teams": [ { "sportfeeds": 21, "name": "Team Name" } ],
"alt_sources": []
}
}Compute an HMAC-SHA256 of the raw request body using your subscription's signing secret, prefix it with sha256=, and compare it (in constant time) to the X-SportFeeds-Signature header. Reject any request that doesn't match.
import crypto from "crypto";
// Express example — make sure you have the RAW request body
function verifySportFeeds(rawBody, headerSignature, signingSecret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", signingSecret).update(rawBody).digest("hex");
// constant-time compare
const a = Buffer.from(headerSignature || "");
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/sportfeeds/webhook", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.header("X-SportFeeds-Signature");
if (!verifySportFeeds(req.body, sig, process.env.SPORTFEEDS_SIGNING_SECRET)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
// Acknowledge fast, process async
res.status(200).send("ok");
});Return any 2xx status as soon as you've stored the event. Do heavy processing asynchronously — slow handlers risk the 10s timeout and trigger retries.
Any other status (or no response) is treated as a failure and retried.
Testing: use the Webhooks dashboard to send a synthetic test event to your endpoint and confirm your signature verification works before going live.