Tournaments API
Native tournaments (single/double elimination, round robin, Swiss, two-stage and
free-for-all) are run over one REST API, whether or not the tournament is bound
to a Discord queue channel. The web dashboard uses the same routes, so anything
you can do on neatqueue.com/t/<slug>/manage you can script.
Every route below is also listed, with its full request/response schema, under Tournaments in the generated API reference in the sidebar.
Authentication
Generate an API token with /webhooks generatetoken in Discord and send it in the
Authorization header. The Bearer prefix is optional.
curl https://api.neatqueue.com/api/tournaments/mine \
-H "Authorization: Bearer YOUR_API_TOKEN"
The token acts as you: what it may do is decided per tournament, exactly as on the website.
| Role | Who | Can |
|---|---|---|
| Owner | The user who created the tournament | Everything, including delete and editing co-organizers |
| Organizer | Owner, listed organizers, or server staff when the tournament is bound to a Discord server | Settings, lifecycle, participants, match results |
| Participant | A user on a roster | Register/withdraw, check in, self-report, confirm/dispute, attachments |
| Anyone | No token needed | /public/... reads of public and unlisted tournaments |
private tournaments are viewable only by their organizers, participants and
invited users (send your token on the /public/... reads); everyone else gets
404 (not 403), so their existence never leaks.
Errors are JSON {"detail": "..."}: 400 bad input, 401 missing/invalid
token, 403 not allowed, 404 unknown (or private), 409 wrong state (for
example registering after the start), 422 validation error.
Ids: tournament ids are JSON numbers (always below 2^53, so they are safe in JavaScript). User ids, server ids and other Discord snowflakes are returned as strings (they overflow JavaScript numbers); send them as numbers or strings. Participant and match ids are small integers.
Route table
Paths are relative to https://api.neatqueue.com. {id} is the tournament id.
Tournaments
| Method | Path | Who | What |
|---|---|---|---|
| POST | /api/tournaments | any user | Create (standalone, or bound with guild_id + channel_id) |
| GET | /api/tournaments/mine | any user | Tournaments you own or organize |
| GET | /api/tournaments?guild_id= | server staff | A Discord server's tournaments |
| GET | /api/tournaments/{id} | organizer | Full bracket (organizer projection) |
| PATCH | /api/tournaments/{id} | organizer | Settings / metadata (organizers: owner only) |
| DELETE | /api/tournaments/{id} | owner | Delete |
Lifecycle
| Method | Path | Who | What |
|---|---|---|---|
| POST | /api/tournaments/{id}/start | organizer | Seed and generate the bracket |
| POST | /api/tournaments/{id}/advance | organizer | Start any ready matches |
| POST | /api/tournaments/{id}/finalize | organizer | Finalize (pays out prize pools when bound to a queue) |
| POST | /api/tournaments/{id}/reset | organizer | Back to pending, keeping participants and check-in |
| POST | /api/tournaments/{id}/seed | organizer | Reorder seeds before the start: order, shuffle or by_mmr |
| POST | /api/tournaments/{id}/swap | organizer | Swap two participants' bracket positions: a, b |
Participants
| Method | Path | Who | What |
|---|---|---|---|
| POST | /api/tournaments/{id}/participants | organizer | Add a participant by name (pre-start) |
| PATCH | /api/tournaments/{id}/participants/{pid} | organizer | Rename / substitute: name |
| DELETE | /api/tournaments/{id}/participants/{pid} | organizer | Remove (pre-start) or disqualify (forfeit) |
| POST | /api/tournaments/{id}/checkin | organizer | Set a participant's check-in: participant_id, checked_in |
| POST | /api/tournaments/{id}/register | participant | Register yourself; team tournaments take team_name and make you captain |
| POST | /api/tournaments/{id}/withdraw | participant | Withdraw before the start (a captain withdraws the team) |
| POST | /api/tournaments/{id}/checkin/self | participant | Check yourself in inside the check-in window: checked_in (default true) |
| POST | /api/tournaments/{id}/participants/{pid}/members | captain / organizer | Add a roster member: user_id |
| DELETE | /api/tournaments/{id}/participants/{pid}/members/{user_id} | captain / organizer | Remove a roster member |
| POST | /api/tournaments/{id}/join/{code} | participant | Join a team with its invite code |
| POST | /api/tournaments/{id}/invites | organizer | Invite users to an invite-only tournament: user_ids |
| DELETE | /api/tournaments/{id}/invites/{user_id} | organizer | Revoke an invite |
| POST | /api/tournaments/{id}/notifications | participant | Opt in/out of a Discord DM when your match is ready: dm |
Matches
| Method | Path | Who | What |
|---|---|---|---|
| POST | /api/tournaments/{id}/matches/{mid}/report | organizer / participant | Report a result. An organizer's report applies at once; a participant's waits for the opponent |
| POST | /api/tournaments/{id}/matches/{mid}/confirm | opponent | Confirm the other side's report (applies it) |
| POST | /api/tournaments/{id}/matches/{mid}/dispute | opponent | Dispute it, with an optional note; an organizer resolves |
| POST | /api/tournaments/{id}/matches/{mid}/resolve | organizer | Accept the pending report (accept: "pending") or rule a result (winner_id, score, games) |
| POST | /api/tournaments/{id}/matches/{mid}/reset | organizer | Reopen a completed match |
| PATCH | /api/tournaments/{id}/matches/{mid} | organizer | note, scheduled_at (unix seconds), station, underway |
| POST | /api/tournaments/{id}/matches/{mid}/attachments | organizer / participant | Attach an https:// link (url, label); at most 10 per match (participants can add only while it holds fewer than 5) |
| DELETE | /api/tournaments/{id}/matches/{mid}/attachments/{aid} | adder / organizer | Remove an attachment |
Discord and import
| Method | Path | Who | What |
|---|---|---|---|
| POST | /api/tournaments/{id}/attach | organizer + server staff | Bind a pending tournament to a queue channel: guild_id, channel_id |
| POST | /api/tournaments/{id}/detach | organizer | Unbind a pending tournament from its channel |
| POST | /api/tournaments/{id}/import | organizer | Import a Challonge bracket: challonge_url_or_id, optional api_key, username, user_map |
Public reads (no token)
| Method | Path | What |
|---|---|---|
| GET | /public/tournaments | Browse public tournaments, newest first. Query: game, format, status, q, page |
| GET | /public/tournaments/{slug} | One tournament (public projection) |
| GET | /public/tournaments/{slug}/bracket.png | Bracket image for embeds |
| GET | /public/users/{user_id}/tournaments | A player's tournament history with placements. Query: page |
The public reads accept an optional token: with one, registered,
my_participant_id and your own team's member ids are filled in, and
/public/users/... also lists unlisted tournaments to the player themself and
any tournament to its organizers.
Examples
Create a tournament
All fields except max_participants are optional. Without channel_id the
tournament is standalone (web only).
curl -X POST https://api.neatqueue.com/api/tournaments \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring Cup",
"url": "spring-cup",
"format": "double_elimination",
"max_participants": 16,
"team_size": 1,
"best_of": 3,
"game": "Valorant",
"visibility": "public",
"starts_at": 1767225600,
"registration": {"mode": "open", "cap": 16},
"checkin": {"opens_before_sec": 1800, "auto_drop": true},
"grand_final": "reset"
}'
| Field | Values |
|---|---|
format | single_elimination, double_elimination, round_robin, swiss, two_stage, free_for_all |
visibility | public (listed), unlisted (link only), private (organizers, participants and invited users only) |
registration.mode | open, invite, closed; plus cap, opens_at, closes_at (unix seconds) |
checkin | opens_before_sec before starts_at, auto_drop unchecked participants at start |
url | The slug for /t/<slug>: lowercased, keeping letters, digits, - and _; globally unique (409 if taken) |
auto_start_on_fill | Start once the signup cap fills. Queue tournaments only (bound with channel_id); ignored for standalone ones |
| Engine settings | points_win, points_tie, points_loss, allow_ties, rank_by, tiebreakers, best_of_by_round, grand_final (reset/single/skip), hide_seeds, sequential_pairings, players_per_match, placement_points, ffa_rounds |
PATCH /api/tournaments/{id} takes the same fields (plus organizers,
signup_cap, round_schedule); bracket-shape settings are rejected with 409
after the start.
The tournament object
Every mutation returns the full tournament. Abridged:
{
"id": 1290000000000000,
"slug": "spring-cup",
"name": "Spring Cup",
"format": "double_elimination",
"status": "underway",
"best_of": 3,
"team_size": 1,
"signup_cap": 16,
"game": "Valorant",
"visibility": "public",
"starts_at": 1767225600,
"discord": null,
"champion_id": null,
"participants": [
{
"id": 1,
"seed": 1,
"name": "Alpha",
"final_rank": null,
"eliminated": false,
"forfeited": false,
"checked_in": true,
"members": [{"display_name": "alpha", "user_id": "1111", "mmr": 1000}],
"roster_complete": true,
"captain_id": "1111"
}
],
"matches": [
{
"id": 1,
"bracket": "winners",
"round": 1,
"state": "open",
"best_of": 3,
"player1_id": 1,
"player2_id": 8,
"winner_id": null,
"score": null,
"games": [],
"pending_report": null,
"disputed": false,
"attachments": [],
"scheduled_at": null,
"station": null
}
],
"standings": [],
"can_manage": true,
"my_participant_id": null,
"registered": false
}
status is pending, underway or complete. A match state is pending
(a slot is still TBD), open, underway, complete or bye. On the public
projection, member user_id/mmr and captain_id are only shown for your own
team, and seed is null when hide_seeds is on.
Report a result
curl -X POST https://api.neatqueue.com/api/tournaments/ID/matches/1/report \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"winner_id": 1, "score": "2-1",
"games": [{"score1": 13, "score2": 9}, {"score1": 7, "score2": 13}, {"score1": 13, "score2": 11}]}'
winner_id is a participant id. Organizers may also send forfeit or
override (re-report a completed match), and free-for-all matches take
placements: [{"participant_id": 1, "placement": 1, "score": 42}] instead of a
winner. A participant's report may carry attachment_url as proof; it is
stored on pending_report until the opponent calls /confirm or /dispute.
Player history
curl https://api.neatqueue.com/public/users/1111/tournaments
{
"items": [
{
"id": 1290000000000000,
"name": "Spring Cup",
"slug": "spring-cup",
"format": "double_elimination",
"status": "complete",
"placement": 2,
"game": "Valorant",
"starts_at": 1767225600,
"timestamp": 1767220000,
"participant_count": 16
}
],
"page": 1,
"page_size": 24,
"has_more": false
}
placement is the final rank once the bracket is complete, else null.
/public/tournaments returns the same page shape (without placement).
Realtime updates
Bracket pages update live over a WebSocket. No token is needed for public and unlisted tournaments:
wss://api.neatqueue.com/realtime?tournament_id=TOURNAMENT_ID
Then subscribe (an anonymous socket may hold up to 5 tournament rooms, and may send nothing else):
{"type": "subscribe_tournament", "data": {"tournament_id": "TOURNAMENT_ID"}}
Messages are {"type": ..., "data": ...}. They are refetch hints, not bracket
data: on each one, re-read GET /public/tournaments/{slug}.
type | data |
|---|---|
tournament_update | {"server_id": "..." or null, "tournament_id": "..."} |
match_ready | {"tournament_id": "...", "match_id": 1, "participant_ids": [1, 8]} |
Send {"type": "unsubscribe_tournament", "data": {"tournament_id": "..."}} to
leave a room.