Webhooks
RoundsWon webhooks send every tournament event to your server as it happens: players joining, matches ready, results and finished tournaments, signed and retried for four days.
A webhook is an address on your server that RoundsWon sends every tournament event to, the moment it happens. Use it to post results in Discord, update your own site, start the next match on a game server, or keep your own copy of a tournament.
Add a webhook
On your developer page, add your address under Webhooks. Pick one tournament or every tournament you host, and every event or only some. Your address must be public and use https.
Press Send a test to check it works. Your server should answer with any 2xx status within 10 seconds.
What arrives
Each event is a POST with a JSON body:
{
"id": "evt_6745f1c2a9d3e8b1f0a2c4d6",
"object": "event",
"tournament": { "id": "6745f1...", "slug": "abc123" },
"seq": 14,
"type": "match.result",
"at": "2026-11-14T20:15:00.000Z",
"by": { "kind": "host", "id": "6745f0..." },
"refs": { "stage": 0, "match": "r1-0" },
"data": { "scores": [2, 1] },
"match": { "key": "r1-0", "status": "done", "sides": [{ "entry": "a1b2c3", "score": 2, "result": "win" }, { "entry": "d4e5f6", "score": 1, "result": "loss" }] }
}
idis unique. You may get an event twice, so skip anidyou have handled.seqis the event's place in the tournament's list. The engine's consequences of an event (match.ready,tournament.finished) share itsseqand arrive right after it, with ids ending_1,_2.by.kindis who sent it:host,admin,scorer,player, orsystemfor the engine's own consequences.matchis the match as it is now, when the event is about one.
Every event type
| Type | When |
|---|---|
tournament.created, tournament.updated | The tournament was made or its settings changed |
tournament.started, tournament.finished | The first stage started, everyone was placed |
tournament.cancelled, tournament.verified | The host cancelled it, or confirmed the results |
staff.added, staff.removed | An admin or scorer was added or removed |
entry.added, entry.updated, entry.removed | A player or team entered, changed seed, or left |
entry.checked_in, entry.disqualified, entry.opted_in | Check in, disqualification, a sponsor email opt in |
stage.started, stage.finished, stage.round_opened | A stage started or ended, the next swiss round was drawn |
match.ready | Both sides of a match are known, it can be played |
match.reported, match.disputed | A captain reported a score, two reports disagree |
match.result | A match has its result |
score.submitted, score.rejected | A leaderboard score came in or was turned down |
payout.sent, payout.unsent, payout.received | A prize was paid, taken back, or the winner says it arrived |
Check it came from RoundsWon
Every request has a RoundsWon-Signature header like t=1763150100,v1=5f8a.... v1 is an HMAC SHA256 of the timestamp, a dot and the raw body, keyed with your webhook's signing secret (whsec_..., on your developer page). Check it before trusting the event, and turn down old timestamps so a recorded request can not be replayed.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
app.post('/roundswon', express.text({ type: '*/*' }), (req, res) => {
const [, t, v1] = req.get('RoundsWon-Signature').match(/t=(\d+),v1=(\w+)/)
const expected = crypto.createHmac('sha256', process.env.ROUNDSWON_SECRET).update(`${t}.${req.body}`).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
if (!fresh || !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) return res.sendStatus(400)
const event = JSON.parse(req.body)
if (event.type === 'match.result') console.log(`Match ${event.refs.match} finished ${event.data.scores.join(' to ')}`)
res.sendStatus(200)
})
app.listen(3000)
import hashlib, hmac, json, os, time
from flask import Flask, request
app = Flask(__name__)
@app.post('/roundswon')
def roundswon():
parts = dict(p.split('=') for p in request.headers['RoundsWon-Signature'].split(','))
body = request.get_data(as_text=True)
expected = hmac.new(os.environ['ROUNDSWON_SECRET'].encode(), f"{parts['t']}.{body}".encode(), hashlib.sha256).hexdigest()
if abs(time.time() - int(parts['t'])) > 300 or not hmac.compare_digest(parts['v1'], expected):
return '', 400
event = json.loads(body)
return '', 200
Retries
If your server does not answer with a 2xx, RoundsWon tries again after 1 minute, 5 minutes, 30 minutes, 2, 6 and 12 hours, then once a day, for about four days in all. After the last try the webhook is switched off and you get an email. Turn it back on from your developer page once it is fixed, then catch up with GET /tournaments/{code}/events?after={seq} from the API.
The developer page shows the last 50 sends for each webhook, with what your server answered.
Order
Events usually arrive in order, but a retry can land after a newer event. Use seq to put them in order, and the event list to fill any gap.