Webhooks

Receive HTTPS POSTs from sportapi when matching events occur. Sign-verified, retry-safe, and suitable for production push-notification systems.

Creating a subscription

curl https://api.sportapi.io/v1/webhooks \
  -X POST \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/sportapi",
    "events": ["news.breaking", "nba.scores.live"],
    "filter": { "team": "LAL" }
  }'

Webhook event format

POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-Sportapi-Signature: t=1700000000,v1=4f2a3b1c...
X-Sportapi-Event: news.breaking
X-Sportapi-Delivery: del_8f3a2b1c

{
  "id": "evt_8f3a2b1c",
  "event": "news.breaking",
  "created_at": "2025-11-14T03:42:18Z",
  "data": {
    "headline": "Lakers acquire All-Star in 3-team trade",
    "summary": "...",
    "url": "https://espn.com/...",
    "teams": ["LAL"]
  }
}

Signature verification

Required to prevent forged deliveries. The signature is HMAC-SHA256 of {timestamp}.${body}, using your webhook's signing secret.

import crypto from 'node:crypto';

function verify(req, secret) {
  const header = req.headers['x-sportapi-signature']; // 't=...,v1=...'
  const [tPart, sigPart] = header.split(',');
  const ts = tPart.split('=')[1];
  const sig = sigPart.split('=')[1];

  const payload = `${ts}.${req.rawBody}`;
  const expected = crypto.createHmac('sha256', secret)
                         .update(payload).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    throw new Error('Invalid signature');
  }

  // Reject if older than 5 minutes — prevents replay attacks
  if (Date.now()/1000 - Number(ts) > 300) {
    throw new Error('Stale request');
  }
}
🚫
Skipping signature verification is a security hole. Anyone can POST to your webhook URL and impersonate sportapi. Always verify before acting on the payload.

Retry logic

Failed deliveries (non-2xx response, timeout > 5s) retry with exponential backoff:

  • 1st retry: 1 minute
  • 2nd retry: 5 minutes
  • 3rd retry: 30 minutes
  • 4th retry: 2 hours
  • 5th retry: 12 hours
  • After 5 failures: subscription is marked failed and requires manual re-enable

Best practices

  • Respond with 2xx within 5 seconds — queue work for later if you need it
  • Process asynchronously; don't do downstream work inline
  • Verify signatures on every request
  • Handle duplicate deliveries idempotently — use the id field as a dedupe key
  • Log X-Sportapi-Delivery for traceability when contacting support
  • Use the filter field to narrow events server-side rather than discarding them on receipt