Webhooks
Receive signed HTTP callbacks when matches finish, tournaments change status, or registrations come in.
Webhooks push events to your server the moment something happens in a published tournament — no polling needed. Rollare currently delivers three event types: match.completed (a result was recorded), tournament.status_changed (draft → live → completed transitions, including automatic completion when a bracket finishes), and registration.created (an entrant submitted a registration form). Delivery is automatic for published tournaments; drafts and deleted events never emit.
Webhook endpoints are currently managed by the Rollare team (contact us with a payload URL and the events you want) while the integration is young. Each endpoint gets a per-endpoint signing secret, shown exactly once at creation. This mirrors how most webhook providers start; self-service management will follow once there are real consumers.
Receiving events
Rollare sends an HTTP POST with a JSON body for every event × endpoint pair. The body is a stable envelope:
{
"id": "dlv_01J9…",
"event": "match.completed",
"version": 1,
"created_at": "2026-10-22T12:00:00.000Z",
"data": {
"tournamentSlug": "summer-cup-2026",
"tournamentId": "0c18…",
"matchKey": "r3m2",
"divisionId": null,
"winnerParticipantId": "f2c0…",
"team1Score": 2,
"team2Score": 1
}
}
The id is the delivery id — unique per event × endpoint, stable across retries, and echoed in the X-Rollare-Delivery header. Keep a record of ids you have processed if you need at-least-once deduplication. data payload fields are kept minimal and contain no personal data: for registrations you get the public display name (the team name shown on the public bracket) and whether the entry landed on the waitlist — never emails or form answers. If a body would ever exceed 20 KB, Rollare sends a truncated summary (data.truncated: true) and you can pull the full state from the read API.
Request headers worth noting: X-Rollare-Event repeats the event type, X-Rollare-Delivery repeats the delivery id, and User-Agent is Rollare-Webhooks/1.0.
Verifying signatures
Every delivery is signed with your endpoint's secret using the same scheme Stripe popularized. The X-Rollare-Signature header looks like:
t=1761133200,v1=5f1a…(64 hex chars)
t is the unix timestamp of signing; v1 is HMAC-SHA256(secret, "${t}.${rawRequestBody}") hex-encoded. To verify: read the raw request body (before any JSON parsing), compute the HMAC with the timestamp from the header, compare against v1 with a constant-time comparison, and reject deliveries older than 5 minutes to prevent replay attacks. Do not parse the body first and re-serialize — key order and whitespace changes will break the signature.
Verification in Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyRollareSignature(secret, header, rawBody, toleranceSec = 300) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!match) return false;
const [, t, signature] = match;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
In Python:
import hmac, hashlib, time, re
def verify_rollare_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
match = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not match:
return False
t, signature = match.group(1), match.group(2)
if abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
In Ruby:
require 'openssl'
def rollare_signature_valid?(secret, header, raw_body, tolerance: 300)
match = header.to_s.match(/\At=(\d+),v1=([0-9a-f]{64})\z/) || return
t, signature = match.captures
return false if (Time.now.to_i - t.to_i).abs > tolerance
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{t}.#{raw_body}")
ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end
Retries, ordering, and failures
Delivery is at-least-once: any non-2xx response, connection error, or 10-second timeout is retried on a fixed backoff schedule — 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours (5 attempts total). After the final attempt the delivery is marked dead and the Rollare team gets an ops alert. Respond 2xx as soon as you have durably accepted the event (enqueue it internally); do slow work asynchronously. Delivery order is not guaranteed — events for the same tournament usually arrive in order, but retries can reorder; treat each payload as a state hint and reconcile with the matches endpoint when order matters.
If your endpoint is disabled (or deleted), pending deliveries are dropped and no new events are emitted until it is re-enabled. Disabling does not invalidate the signing secret; deleting does.
Quick checklist
- Expose an HTTPS POST endpoint (plain
http://localhostis accepted for local development). - Store the signing secret from creation time; verify every delivery as above.
- Acknowledge with any 2xx within 10 seconds; dedupe on
X-Rollare-Delivery. - To backfill state after downtime, call
GET /v1/tournaments/{slug}/matchesand reconcile.