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
| Event | When | Payload shape |
|---|---|---|
match.scheduled | New match added to the fixture list | {event, match: Match, occurred_at} |
match.started | Status transition: scheduled → live | {event, match: Match, occurred_at} |
match.completed | Status transition: live → completed (with final score) | {event, match: Match, occurred_at} |
tournament.bracket_updated | Bracket 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
Content-Type: application/jsonX-Stadar-Signature: <hex sha256 hmac of the body, secret-keyed>X-Stadar-Event-Id: ev_01HMXYZ...(=delivery_idin body — idempotency)X-Stadar-Event-Type: match.completedUser-Agent: Stadar-Webhooks/v1 (+https://stadar.net/docs/webhooks)
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
- Free + Hobbyist — webhooks endpoint is reachable for listing/inspecting, but
POST /v1/account/webhooksreturns 402 with an upgrade link. - Pro — full webhook subscriptions, all event types, all filters.
- Scale — same as Pro plus higher delivery concurrency.
Check your effective capability with GET /v1/account/me/capabilities or via the SDK’s client.account.capabilities() method.