> For the complete documentation index, see [llms.txt](https://docs.ohalee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ohalee.com/products/modern-tournament/website.md).

# The website

API tokens, correcting a tournament that has gone wrong, the stream overlay, player records and webhooks.

Everything on this page lives on [tournament.ohalee.com](https://tournament.ohalee.com) and needs an active subscription. The plugin side is in [configuration.md](/products/modern-tournament/configuration.md).

## API tokens

Your account has one permanent key, shown on the dashboard. It works, and it always will — but it carries every permission there is, and the only way to change it is to refresh it, which breaks every server using it at once.

A **token** is the alternative. Create one per server, give it a name you will recognise, and it can be revoked on its own without touching anything else.

<table><thead><tr><th width="200">Scope</th><th>Lets a token</th></tr></thead><tbody><tr><td><code>tournament:read</code></td><td>Read tournaments, participants and brackets</td></tr><tr><td><code>tournament:write</code></td><td>Create tournaments, add and remove players, draw the bracket</td></tr><tr><td><code>match:write</code></td><td>Report match results and match state</td></tr><tr><td><code>admin</code></td><td>Override a result, void a match, release a tournament from its server</td></tr></tbody></table>

A new token defaults to the first three, which is exactly what a Minecraft server does. It deliberately does **not** get `admin`: a key sitting in a config file on a game server is the last one that should be able to rewrite a result.

Paste the token into `api_key` in `settings.yml` exactly as you would the old key — the plugin does not care which kind it is given.

{% hint style="warning" %}
The token is shown **once**, when you create it. Only a hash of it is stored, so it cannot be recovered or shown again. If you lose it, create another and revoke the old one — which is the whole reason for having more than one.
{% endhint %}

A token that lacks a scope is refused with `403` and told which scope it needed, so a misconfigured key is diagnosable from the response rather than from guesswork.

## When a tournament goes wrong

Open a tournament from your dashboard and you get a panel the public bracket does not show.

**Which server holds it.** A tournament is executed by one Minecraft server, which claims it on start. The panel names that server and when it claimed it, so "the tournament will not start" stops being a guessing game.

**What a crash left behind.** A match that a server was fighting when it died is listed separately. Usually you need do nothing: when that server comes back and attaches again, the match is reopened and played. The listing is for the case where it is not coming back.

**Setting a winner.** Pick a match, pick the winner, give a reason. The reason is required and is recorded.

**Voiding a match.** Discards its result and reopens it.

{% hint style="info" %}
Changing a match **replays the bracket** from the draw. Everything that followed from that match is recomputed, because those later matches only happened as a consequence of it — so a result recorded between two teams who no longer meet cannot be kept, and you are told how many were discarded.

Flipping a semi-final, for instance, drops both the final *and* the third-place match: the third-place match is between the two beaten semi-finalists, and you have just changed who those are.
{% endhint %}

Every correction is written to an audit log with who made it, when, and why, shown at the bottom of the same panel. A bracket that changed with nobody able to say why is worse than one that is simply wrong.

## Stream overlay

Every tournament has an overlay at:

```
https://tournament.ohalee.com/<uuid>/overlay
```

Add it as a **browser source** in OBS. It paints no background, so whatever is behind it in your scene shows through, and it shows only what is being fought right now and who has won — a bracket is not something anybody can read at overlay size.

Several matches appear at once if you have raised `max_parallel_matches`, and a best-of shows its running score. It holds the same live connection as the public bracket page, so an overlay left open for a whole tournament reconnects and catches up on its own.

Append `?bg=1` to give it a background when you want to check it in an ordinary browser, where transparent renders as white and looks broken.

## Player records

The dashboard shows a table of everybody who has played in your tournaments: how many they entered, how many they won, their match record and their win rate.

These are counted from the stored matches every time the page is opened, not kept as running totals. That means correcting a result — or deleting a tournament — is reflected immediately, with no way for the numbers to drift from the matches they describe.

A player is followed by their Minecraft UUID, so somebody who changes their name keeps their record and shows up under the name they use now.

## Webhooks

Post tournament events to your own endpoint — a Discord relay, a scoreboard, your own site.

Add an `https` URL on the dashboard and choose which events you want, or choose none to receive everything, including events added in future versions.

<table><thead><tr><th width="240">Event</th><th>Sent when</th></tr></thead><tbody><tr><td><code>tournament.seeded</code></td><td>The bracket is drawn and the tournament starts</td></tr><tr><td><code>tournament.finished</code></td><td>The deciding match is won</td></tr><tr><td><code>match.completed</code></td><td>A match is decided</td></tr><tr><td><code>match.overridden</code></td><td>Somebody corrected the bracket by hand</td></tr></tbody></table>

The body is JSON:

```json
{
  "event": "match.completed",
  "tournament": "8f14e45f-ceea-467a-9f6a-1f0f0e39c1a2",
  "sent_at": "2026-08-08T20:31:04.882Z",
  "data": {
    "bracket": "WINNERS",
    "round": 2,
    "order_number": 0,
    "winner_team_number": 3,
    "cause": "KILL",
    "seq": 41
  }
}
```

### Verifying a delivery

Every request carries a signature, so your endpoint can tell a real delivery from anybody who found the URL:

```
X-UT-Event: match.completed
X-UT-Signature: t=1786000264,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
```

`v1` is `HMAC-SHA256(secret, "<t>.<raw body>")` in hex, where `t` is the Unix timestamp in the same header and the body is the **raw** bytes, before any JSON parsing.

To verify: check `t` is within a few minutes of now, recompute the HMAC over `t` and the raw body, and compare it to `v1` — with a constant-time comparison, not `==`. The timestamp is part of what is signed, so a captured delivery cannot be re-stamped with a fresh one and still verify.

The secret is shown once when you add the webhook, the same as an API token.

### Delivery and retries

A delivery is written to the database in the same transaction as the change it describes, so you cannot be told about something that did not happen, and cannot miss something that did.

If your endpoint is unreachable or answers with an error, the delivery is retried: thirty seconds, then a minute, doubling to a maximum of six hours, over twelve attempts — a bit over a day in total. An endpoint down for an hour is not hammered for the hour, and one down overnight still gets everything when it comes back.

A `4xx` other than `408` or `429` is treated as final. That is your endpoint saying it will never accept this delivery, and retrying cannot change the answer.

Anything given up on is shown on the dashboard next to the webhook, with the last error, so a misconfigured endpoint is visible rather than silent.

{% hint style="success" %}
Answer `2xx` as soon as you have the delivery, and do your work afterwards. A slow endpoint is retried after ten seconds, which for a handler that is merely slow means doing the work twice.
{% endhint %}
