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" }] }
}

Every event type

TypeWhen
tournament.created, tournament.updatedThe tournament was made or its settings changed
tournament.started, tournament.finishedThe first stage started, everyone was placed
tournament.cancelled, tournament.verifiedThe host cancelled it, or confirmed the results
staff.added, staff.removedAn admin or scorer was added or removed
entry.added, entry.updated, entry.removedA player or team entered, changed seed, or left
entry.checked_in, entry.disqualified, entry.opted_inCheck in, disqualification, a sponsor email opt in
stage.started, stage.finished, stage.round_openedA stage started or ended, the next swiss round was drawn
match.readyBoth sides of a match are known, it can be played
match.reported, match.disputedA captain reported a score, two reports disagree
match.resultA match has its result
score.submitted, score.rejectedA leaderboard score came in or was turned down
payout.sent, payout.unsent, payout.receivedA 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.