DOCS / WEBHOOKS

Webhooks. Pro-tier push events.

Don’t poll. Subscribe. The four match-lifecycle events we emit are all you need to keep a real-time dashboard up to date.

Webhooks are a Pro-tier capability (webhooks in your effective capability set). Hobbyist+ can subscribe to a strictly limited preview event types; full coverage requires Pro.

Subscribing

POST /v1/account/webhooks with the URL we should POST events to:

POST /v1/account/webhooks
Authorization: Bearer $STADAR_API_KEY

{
  "url": "https://your-app.example.com/stadar-webhook",
  "events": ["match.completed", "tournament.bracket_updated"],
  "filters": {
    "games": ["cs2", "lol"],
    "tournaments": ["esl-pro-league-2026"]
  }
}

# → 201 Created
{
  "data": {
    "id": "wh_01HMXYZ...",
    "url": "https://your-app.example.com/stadar-webhook",
    "events": ["match.completed", "tournament.bracket_updated"],
    "filters": { "games": ["cs2", "lol"], "tournaments": ["esl-pro-league-2026"] },
    "secret": "whsec_abcd1234...",  // SHOWN ONCE; store this now
    "created_at": "2026-05-16T..."
  }
}

The secret is what you’ll use to verify signatures. We hash it on store; if you lose it, you’ll need to rotate via DELETE + re-subscribe.

Event types

EventWhenPayload shape
match.scheduledNew match added to the fixture list{event, match: Match, occurred_at}
match.startedStatus transition: scheduledlive{event, match: Match, occurred_at}
match.completedStatus transition: livecompleted (with final score){event, match: Match, occurred_at}
tournament.bracket_updatedBracket DAG changed (new round, seeding update){event, tournament: Tournament, occurred_at}

Every event ships under one common envelope:

{
  "event": "match.completed",
  "occurred_at": "2026-05-16T18:43:12Z",
  "delivery_id": "ev_01HMXYZ...",  // idempotency key — see below
  "match": {
    "id": 12345,
    "game": "cs2",
    "team_a": { ... },
    "team_b": { ... },
    "winner": "team_a",
    "score": "2-1",
    "tournament_id": "esl-pro-league-2026"
  }
}

Headers we send

The X-Stadar-Event-Id header is your idempotency key. We may retry on transient failures; check this id against the events you’ve already processed before re-acting.

Signature verification

Always verify before trusting the body. Our SDKs ship a one-line helper.

Python (stadar PyPI package)

from stadar.webhooks import verify_signature

@app.post("/stadar-webhook")
def handle(request):
    body = request.body  # raw bytes, not parsed JSON
    signature = request.headers["X-Stadar-Signature"]
    if not verify_signature(body, signature, secret="whsec_abcd1234..."):
        return 401
    event = json.loads(body)
    # ... handle event

TypeScript (@stadar/sdk npm package)

import { verifySignature } from '@stadar/sdk/webhooks';

app.post('/stadar-webhook', async (req, res) => {
  const body = await readRawBody(req);
  const signature = req.headers['x-stadar-signature'] as string;

  const ok = await verifySignature(body, signature, 'whsec_abcd1234...');
  if (!ok) return res.status(401).end();

  const event = JSON.parse(body.toString('utf8'));
  // ... handle event
});

Without our SDK — the wire format

If you’re not on Python or TypeScript, the format is plain HMAC-SHA256 over the raw request body, hex-encoded, optional sha256= prefix:

expected = HMAC-SHA256(secret, raw_body)
provided = (X-Stadar-Signature without optional "sha256=" prefix)
verify   = constant-time compare expected == provided

Use a constant-time comparison (Go’s subtle.ConstantTimeCompare, Python’s hmac.compare_digest, Node’s crypto.timingSafeEqual). Don’t naive-string-compare — the timing oracle is real.

Retries + dead letters

A delivery is “successful” iff your endpoint returns 2xx within 10 seconds. Anything else (4xx, 5xx, timeout, DNS failure) triggers a retry schedule:

1s · 5s · 30s · 5m · 30m · 6h — then archived to a dead-letter table.

After 6 retries (~7 hours total), the event lands in webhook_dead_letters. Operators can inspect failures via the admin panel (admins only — your dashboard never sees this table). Stuck on a debugging session and need us to look? Email [email protected] with the delivery_id.

Tier gating

Check your effective capability with GET /v1/account/me/capabilities or via the SDK’s client.account.capabilities() method.

See pricing