Skip to content
datasportsAPI

DeliveryWebhooks

Documentation

/

Type to filter the docs by title or path.

Delivery · Webhooks

We call you when the score moves

Webhooks turn the stream around: rather than holding a connection open, you give us an HTTPS address and we send each event to it as a signed POST. Good for serverless functions, notification pipelines and anything that would rather react than listen.

  • Free: not included
  • Starter: not included
  • Pro: not included
  • Business: included
HTTPS POST · JSONHMAC-SHA256Retries ≈ 9 hours
01

How delivery works

Set it up once in the dashboard; after that every matching event becomes one request to each endpoint that wants it.

  1. 01

    Register

    In the dashboard's Webhooks tab, add the URL that should receive events. It must be https:// on a public host; plain HTTP and private addresses are refused.

  2. 02

    Keep the secret

    Each endpoint gets its own signing secret, shown once when you create it or rotate it. Store it next to your API key.

  3. 03

    Choose what to hear

    Pick event types and narrow by sport, league or fixture. Leave a filter empty and it lets everything through.

Pick webhooks when

  • you run on functions or short-lived workers that cannot hold a socket;
  • you care about scores, status, events and lineups rather than every field;
  • you want our retries to cover your downtime.

Pick the WebSocket when

  • you need every change to a match, odds or a race, not just four event types;
  • latency matters more than simplicity;
  • you want numbered frames with replay. See the WebSocket guide.
02

Event types

Two types follow every live match your filters allow. Two are detailed enough that you name the fixtures you want them for.

TypeSent whenFor which fixtures
fixture.scoreThe current score of either side changes.Every live fixture that passes your filters.
fixture.statusThe match kicks off, enters a new period or a break, reaches full time, or is postponed or cancelled.Every live fixture that passes your filters.
fixture.eventsMatch events are added, corrected or removed: goals with their scorers, cards, substitutions.Only fixtures you list in the endpoint's fixture ids.
fixture.lineupsThe official lineups appear and whenever they change in play. We start looking about 90 minutes before kick-off; most teams publish around an hour before.Only fixtures you list in the endpoint's fixture ids.
pingYou send a test delivery from the dashboard.The endpoint you tested.

For fixture.score and fixture.status, data.changes is a partial Fixture with the same shape as a WebSocket delta, so the code that merges deltas merges these too. For events and lineups the most robust pattern is to treat the webhook as a nudge and read the full list from the REST endpoint below, which is always complete.

03

Filters

You can narrow each endpoint with up to four optional filters — by event type, sport, league id and fixture id. Only events that match every filter you set are delivered.

One endpoint's filters, and four events put through them

Event types

  • fixture.score
  • fixture.status

andSports

  • football

andLeague ids

  • 17
  • 8

andFixture ids

  • empty: any
  • fixture.score · football · league 17Delivered: passes all four filters
  • fixture.status · football · league 52Not delivered: league 52 is not in the list
  • fixture.score · tennis · league 2391Not delivered: tennis is not in the list
  • fixture.events · football · league 8Not delivered: type not selected, and no fixture ids named

League and fixture filters take our ids, the same ones the REST API returns. A common setup is one broad endpoint for scores and status across a few leagues, plus a second endpoint whose fixture ids you update from your own code for the matches that need events and lineups.

04

The request we send

One POST per event per endpoint, with a small JSON body and four headers.

fixture.score
POST /webhooks/sports HTTP/1.1
Host: your-server.example.com
Content-Type: application/json
DSA-Signature: t=1793551606,v1=5f0c…
DSA-Event: fixture.score
DSA-Event-Id: evt_8a1f…

{
  "id": "evt_8a1f…",
  "type": "fixture.score",
  "createdAt": "2026-11-01T16:46:46.208Z",
  "data": {
    "fixtureId": 211521,
    "sport": "football",
    "leagueId": 17,
    "changes": { "homeScore": { "current": 1, "periods": { "period1": 1 } } }
  }
}
DSA-Signature
t=1793551606,v1=5f0c…

Timestamp and HMAC. Verify it before anything else.

DSA-Event
fixture.score

The event type, so you can route before parsing the body.

DSA-Event-Id
evt_8a1f…

Same as id in the body. Stays identical across retries.

Content-Type
application/json

The body is always JSON.

id
evt_…

Unique per event.

createdAt
ISO-8601, UTC

When we created the event, to the millisecond.

05

Verifying signatures

Anyone can POST to a public URL. The signature proves the request came from us and was not altered or replayed.

Anatomy of the signature header

DSA-Signature: t=1793551606,v1=5f0c9e…41d2

t · when we signed

Unix seconds at the moment of signing. Refuse anything more than five minutes away from your clock, so a captured request cannot be played back later.

v1 · what we signed

Lower-case hex of an HMAC-SHA256, keyed with the endpoint's signing secret, over the timestamp, a full stop and the raw request body.

v1 = hex(HMAC_SHA256(secret, t + "." + raw_body))
  1. 1Read the raw bodyBefore any JSON middleware touches it. The HMAC covers the exact bytes we sent.
  2. 2Split the headerSeparate t and v1 at the comma, then each name from its value at the first equals sign.
  3. 3Check the timeReject the request if t is more than five minutes from now.
  4. 4Recompute and compareHMAC-SHA256 over t, a full stop and the body, keyed with the signing secret; compare with v1 in constant time.
// webhook.ts: receive and verify events (Node 22+, no dependencies)
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const SECRET = process.env.WEBHOOK_SIGNING_SECRET!; // shown once, when the endpoint is created
const TOLERANCE_S = 300;
const seen = new Set<string>(); // use Redis or your database in production

export function verify(header: string | undefined, raw: Buffer, secret: string): boolean {
  if (!header) return false;
  const parts = new Map(header.split(",").map((kv) => kv.split("=", 2) as [string, string]));
  const t = parts.get("t");
  const v1 = parts.get("v1");
  if (!t || !v1) return false;

  const age = Date.now() / 1000 - Number(t);
  if (!Number.isFinite(age) || Math.abs(age) > TOLERANCE_S) return false; // stale or replayed

  const expected = createHmac("sha256", secret).update(`${t}.`).update(raw).digest();
  const given = Buffer.from(v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

createServer((req, res) => {
  const chunks: Buffer[] = [];
  req.on("data", (c: Buffer) => chunks.push(c));
  req.on("end", () => {
    const raw = Buffer.concat(chunks); // the exact bytes: verify before parsing
    if (!verify(req.headers["dsa-signature"] as string | undefined, raw, SECRET)) {
      res.writeHead(401).end();
      return;
    }
    res.writeHead(204).end(); // answer inside 10 seconds, then do the work

    const event = JSON.parse(raw.toString("utf8"));
    if (seen.has(event.id)) return; // a retry of something already handled
    seen.add(event.id);
    enqueue(event);
  });
}).listen(3000);

function enqueue(event: { id: string; type: string; createdAt: string; data: Record<string, unknown> }) {
  // Hand off to your job queue. data.changes is a partial Fixture:
  // deep-merge it exactly like a WebSocket delta.
}
06

Responses and retries

Answer with any 2xx status within 10 seconds and the delivery is done. Anything else counts as a failure, and we try again.

One event, every attempt failing≈ 8 h 36 min from first try to failed
  1. t = 0Delivery
  2. +10 sRetry 1
  3. +1 minRetry 2
  4. +5 minRetry 3
  5. +30 minRetry 4
  6. +2 hRetry 5
  7. +6 hRetry 6
  8. thenFailed
Counts as success
2xx within 10 s

Any status from 200 to 299 will do; 204 No Content is the tidiest.

Counts as failure
anything else

4xx, 5xx, a timeout, a connection error and redirects too: we do not follow a 3xx, so point the endpoint at the final URL.

Retry schedule
10 s · 1 min · 5 min · 30 min · 2 h · 6 h

Each wait follows the previous failed attempt. After the sixth retry the delivery is marked failed.

Disabled endpoints
24 h of failures

If every attempt to an endpoint fails for a whole day, we stop sending to it. Fix it, then re-enable it in the dashboard.

07

Duplicates and order

Deliveries can arrive more than once and not always in the order things happened. Two fields make both harmless.

DSA-Event-Id · id

Seen it? Skip it.

A retry carries the same id as the original. Record each id you process (a unique index in your database works well) and ignore repeats. A response that timed out on our side may have succeeded on yours, so duplicates are normal, not an error.

createdAt

Older than what you have? Skip it.

A retried score can land after a newer one. Keep the newest createdAt you applied per fixture and event type, and do not let an older event overwrite newer state.

08

Before you go live

  • Verify on raw bytesCompute the HMAC over the body exactly as it arrived. Parsing and re-serialising JSON changes the bytes.
  • Compare in constant timetimingSafeEqual in Node, hmac.compare_digest in Python. A plain == leaks how much of a guess was right.
  • Reject old timestampsFive minutes either way is a sensible window. Keep your server clock synced with NTP.
  • Acknowledge firstReturn 2xx as soon as the signature checks out; do the slow work from a queue.
  • Deduplicate by event idRemember ids for longer than the retry window, a day or more, so a late retry is recognised.
  • Never trust arrival orderCompare createdAt before applying a change, or re-read the fixture over REST when order matters.
  • Guard the secretStore the signing secret like an API key. If it may have leaked, rotate it in the dashboard; the new one is shown once.
  • Watch for failuresAfter 24 hours in which every attempt failed, the endpoint is disabled. Turn it back on in the dashboard once it is fixed.

Testing locally? Endpoints must be public HTTPS, so expose your machine through a tunnel and use the dashboard's test delivery, which sends a ping event.