Webhooks
Register endpoints to receive real-time event notifications from Tournament Suite.
Webhooks let you receive notifications when events occur in your project — instead of polling the API repeatedly. When a subscribed event fires, Tournament Suite sends an HTTP request to your endpoint with the event payload.
Set up a webhook
1. Register your endpoint
In your project's Settings → Developer → Webhooks, create a subscription with your endpoint URL, the events you want to receive, and a delivery configuration (retry limits, timeout, and optional circuit breaker settings — see Delivery configuration below).
Your endpoint must:
- Be publicly reachable over HTTP or HTTPS (HTTPS is required in production)
- Not resolve to a localhost, loopback, private-network, or link-local address
When you create the subscription, Tournament Suite validates that the URL is reachable and not pointed at an internal or private address. If you enable health checks in the delivery configuration, Tournament Suite also periodically issues a plain GET request to your endpoint (or a separate health-check URL you specify) and expects one of a configurable set of status codes back — there is no ownership-verification handshake or challenge exchange.
2. Save the signing secret
If you configure HMAC signing (see below), Tournament Suite generates a signing secret when the subscription is created. Store this secret securely — you will use it to verify the authenticity of every incoming webhook request.
3. Subscribe to events
Choose the event types your subscription should receive from the available events catalog. You can also scope a subscription with an aggregate-type filter, specific tournament IDs, specific user IDs, or custom field-matching conditions.
Authenticate outbound requests
Each subscription chooses one authentication mode for the requests Tournament Suite makes to your endpoint:
| Mode | Behavior |
|---|---|
none | No authentication headers added |
hmac_signature | Signs the payload and adds an X-Webhook-Signature header (see below) |
bearer_token | Adds Authorization: Bearer <token> using the token you configured |
basic_auth | Adds Authorization: Basic <base64(username:password)> |
custom_header | Adds a header name/value pair you specify |
You can combine HMAC signing with any of the other modes — signing verifies the payload wasn't tampered with, while the auth header lets your endpoint authenticate the caller.
Verify webhook signatures
When HMAC signing is enabled, every webhook request includes an X-Webhook-Signature header containing the hex-encoded HMAC-SHA256 digest of the request body (no sha256= prefix). Verify it to ensure the payload came from Tournament Suite and was not tampered with:
import crypto from 'crypto';
function verifyWebhook(
payload: string,
signatureHeader: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader, 'hex'),
Buffer.from(expected, 'hex'),
);
}
app.post('/webhooks/tournamentsuite', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-webhook-signature'] as string;
if (!sig || !verifyWebhook(req.body.toString(), sig, process.env.WEBHOOK_SECRET!)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString());
// process event
res.sendStatus(200);
});
Event payload format
{
"id": "dlv_01HXYZ",
"event": {
"id": "evt_01HXYZ",
"type": "tournament.started",
"timestamp": "2026-07-01T09:00:00.000Z",
"version": 1
},
"data": {
"aggregateId": "550e8400-e29b-41d4-a716-446655440000",
"aggregateType": "tournament",
"payload": {
"tournamentId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Summer Open 2026"
},
"metadata": {}
},
"webhook": {
"subscriptionId": "sub_01HXYZ",
"attempt": 1,
"isRetry": false
},
"signature": "<hex-digest>"
}
id identifies this delivery attempt (not the underlying event). The domain event's own ID and type live under event; the actual event data is nested under data.payload, with data.aggregateId / data.aggregateType identifying the entity the event is about. webhook.attempt and webhook.isRetry tell you whether this is the first attempt or a retry.
Available events
Tournament events
| Event | When it fires |
|---|---|
tournament.created | A tournament is created |
tournament.published | A tournament is made public |
tournament.updated | A tournament's details are changed |
tournament.started | A tournament begins |
tournament.completed | A tournament concludes |
tournament.cancelled | A tournament is cancelled |
Registration events
| Event | When it fires |
|---|---|
registration.opened | Registration opens for a tournament |
registration.closed | Registration closes for a tournament |
registration.full | A tournament reaches its participant cap |
Participant events
| Event | When it fires |
|---|---|
participant.registered | A player or team registers |
participant.approved | A registration is approved |
participant.rejected | A registration is rejected |
participant.checked_in | A participant checks in |
participant.eliminated | A participant is eliminated from the bracket |
participant.disqualified | A participant is removed |
Match events
| Event | When it fires |
|---|---|
match.created | A match is scheduled |
match.started | A match begins |
match.completed | A match result is confirmed |
match.disputed | A result is disputed |
match.rescheduled | A match's scheduled time changes |
match.forfeited | A match is forfeited by a participant |
Circuit events
| Event | When it fires |
|---|---|
circuit.created | A circuit is created |
circuit.season.started | A circuit season begins |
circuit.season.completed | A circuit season concludes |
Payment events
| Event | When it fires |
|---|---|
payment.received | A payment is received |
payout.processed | A payout is successfully processed |
payout.failed | A payout attempt fails |
Broadcast events
| Event | When it fires |
|---|---|
broadcast.started | A broadcast goes live |
broadcast.ended | A broadcast ends |
User events
| Event | When it fires |
|---|---|
user.created | A user account is created |
user.verified | A user completes verification |
user.suspended | A user account is suspended |
Anti-cheat events
| Event | When it fires |
|---|---|
anticheat.detection_raised | An anti-cheat rule flags a session |
anticheat.session_completed | An anti-cheat monitoring session ends |
You can also fetch the current catalog of subscribable event types — with descriptions and example payloads — from the GET /webhooks/event-types endpoint rather than hardcoding it.
Delivery configuration
Retry and reliability behavior is per-subscription configuration, not a fixed platform default. When you create a subscription you set:
| Field | Range | Purpose |
|---|---|---|
timeoutMs | 1,000–120,000 | How long Tournament Suite waits for your endpoint to respond before treating the attempt as failed |
maxRetries | 0–10 | Maximum retry attempts per delivery |
retryBaseDelayMs | 1,000–60,000 | Delay before the first retry |
retryBackoffMultiplier | 1–10 | Multiplier applied to the delay on each subsequent retry (exponential backoff) |
If your endpoint returns a non-2xx status or does not respond within your configured timeoutMs, Tournament Suite retries using your configured backoff. Deliveries that are still unresolved after 24 hours expire and are no longer retried automatically. You can inspect delivery history — including status, response codes, and timing — under GET /webhooks/subscriptions/:id/deliveries.
Circuit breaker
If a subscription's deliveries keep failing, Tournament Suite can automatically pause it: enabling enableCircuitBreaker on the delivery configuration, along with a failure circuitBreakerThreshold and circuitBreakerTimeoutMs, causes the subscription to open its circuit breaker after repeated failures — pending and new deliveries stop being attempted until the timeout elapses (at which point delivery is tried again to test recovery) or you reset it manually via POST /webhooks/subscriptions/:id/circuit-breaker/reset.
Rate limiting and IP restrictions
A subscription can also set a rateLimitConfig (maximum requests per minute, per hour, per day, and a short burst allowance) to cap how fast Tournament Suite sends it deliveries, and an ipWhitelist of allowed IPs or CIDR ranges used to validate inbound requests associated with the subscription.
Replay and bulk retry
Beyond the passive 24-hour retry window, you can act on failed deliveries directly:
POST /webhooks/subscriptions/:id/retry-failed— re-queues every currently failed delivery for the subscription.POST /webhooks/subscriptions/:id/deliveries/:deliveryId/replay— re-queues one specific delivery.
Analytics
Each subscription exposes delivery analytics at GET /webhooks/subscriptions/:id/analytics — success rate, a response-time timeline, and a breakdown of failures by error type — and GET /webhooks/dashboard summarizes totals and health across all of your subscriptions.
Best practices
Always respond with 200 OK immediately, then process the event asynchronously. This avoids timeouts and duplicate retries caused by slow processing.
app.post('/webhooks/tournamentsuite', async (req, res) => {
res.sendStatus(200); // acknowledge immediately
// process in the background
setImmediate(() => handleEvent(req.body));
});Was this helpful?
