Skip to main content

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.

RoleWhoCan
OwnerThe user who created the tournamentEverything, including delete and editing co-organizers
OrganizerOwner, listed organizers, or server staff when the tournament is bound to a Discord serverSettings, lifecycle, participants, match results
ParticipantA user on a rosterRegister/withdraw, check in, self-report, confirm/dispute, attachments
AnyoneNo 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​

MethodPathWhoWhat
POST/api/tournamentsany userCreate (standalone, or bound with guild_id + channel_id)
GET/api/tournaments/mineany userTournaments you own or organize
GET/api/tournaments?guild_id=server staffA Discord server's tournaments
GET/api/tournaments/{id}organizerFull bracket (organizer projection)
PATCH/api/tournaments/{id}organizerSettings / metadata (organizers: owner only)
DELETE/api/tournaments/{id}ownerDelete

Lifecycle​

MethodPathWhoWhat
POST/api/tournaments/{id}/startorganizerSeed and generate the bracket
POST/api/tournaments/{id}/advanceorganizerStart any ready matches
POST/api/tournaments/{id}/finalizeorganizerFinalize (pays out prize pools when bound to a queue)
POST/api/tournaments/{id}/resetorganizerBack to pending, keeping participants and check-in
POST/api/tournaments/{id}/seedorganizerReorder seeds before the start: order, shuffle or by_mmr
POST/api/tournaments/{id}/swaporganizerSwap two participants' bracket positions: a, b

Participants​

MethodPathWhoWhat
POST/api/tournaments/{id}/participantsorganizerAdd a participant by name (pre-start)
PATCH/api/tournaments/{id}/participants/{pid}organizerRename / substitute: name
DELETE/api/tournaments/{id}/participants/{pid}organizerRemove (pre-start) or disqualify (forfeit)
POST/api/tournaments/{id}/checkinorganizerSet a participant's check-in: participant_id, checked_in
POST/api/tournaments/{id}/registerparticipantRegister yourself; team tournaments take team_name and make you captain
POST/api/tournaments/{id}/withdrawparticipantWithdraw before the start (a captain withdraws the team)
POST/api/tournaments/{id}/checkin/selfparticipantCheck yourself in inside the check-in window: checked_in (default true)
POST/api/tournaments/{id}/participants/{pid}/memberscaptain / organizerAdd a roster member: user_id
DELETE/api/tournaments/{id}/participants/{pid}/members/{user_id}captain / organizerRemove a roster member
POST/api/tournaments/{id}/join/{code}participantJoin a team with its invite code
POST/api/tournaments/{id}/invitesorganizerInvite users to an invite-only tournament: user_ids
DELETE/api/tournaments/{id}/invites/{user_id}organizerRevoke an invite
POST/api/tournaments/{id}/notificationsparticipantOpt in/out of a Discord DM when your match is ready: dm

Matches​

MethodPathWhoWhat
POST/api/tournaments/{id}/matches/{mid}/reportorganizer / participantReport a result. An organizer's report applies at once; a participant's waits for the opponent
POST/api/tournaments/{id}/matches/{mid}/confirmopponentConfirm the other side's report (applies it)
POST/api/tournaments/{id}/matches/{mid}/disputeopponentDispute it, with an optional note; an organizer resolves
POST/api/tournaments/{id}/matches/{mid}/resolveorganizerAccept the pending report (accept: "pending") or rule a result (winner_id, score, games)
POST/api/tournaments/{id}/matches/{mid}/resetorganizerReopen a completed match
PATCH/api/tournaments/{id}/matches/{mid}organizernote, scheduled_at (unix seconds), station, underway
POST/api/tournaments/{id}/matches/{mid}/attachmentsorganizer / participantAttach 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 / organizerRemove an attachment

Discord and import​

MethodPathWhoWhat
POST/api/tournaments/{id}/attachorganizer + server staffBind a pending tournament to a queue channel: guild_id, channel_id
POST/api/tournaments/{id}/detachorganizerUnbind a pending tournament from its channel
POST/api/tournaments/{id}/importorganizerImport a Challonge bracket: challonge_url_or_id, optional api_key, username, user_map

Public reads (no token)​

MethodPathWhat
GET/public/tournamentsBrowse public tournaments, newest first. Query: game, format, status, q, page
GET/public/tournaments/{slug}One tournament (public projection)
GET/public/tournaments/{slug}/bracket.pngBracket image for embeds
GET/public/users/{user_id}/tournamentsA 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"
}'
FieldValues
formatsingle_elimination, double_elimination, round_robin, swiss, two_stage, free_for_all
visibilitypublic (listed), unlisted (link only), private (organizers, participants and invited users only)
registration.modeopen, invite, closed; plus cap, opens_at, closes_at (unix seconds)
checkinopens_before_sec before starts_at, auto_drop unchecked participants at start
urlThe slug for /t/<slug>: lowercased, keeping letters, digits, - and _; globally unique (409 if taken)
auto_start_on_fillStart once the signup cap fills. Queue tournaments only (bound with channel_id); ignored for standalone ones
Engine settingspoints_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}.

typedata
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.