Skip to content

Endpoints

Every endpoint of the public API, generated from app/features/api-public/schema.ts. See the overview for how to get a token, the rules of using it and an example request. Every request sends the token as Authorization: Bearer <token>.

The examples under an endpoint are real requests the e2e tests made against it (mostly e2e/api-public.spec.ts), one per status code, kept by the last pnpm run docs:capture in docs/site/api-examples.json with arrays cut to their first items. An endpoint without examples has no test calling it.

Read endpoints

Work with a read or a write token. The type is the JSON the endpoint responds with.

GET

/api/user/{userId|discordId}

GetUserResponse
interface GetUserResponse {
	id: number;
	/** @example "Sendou" */
	name: string;
	/** @example "79237403620945920" */
	discordId: string;
	/** @example "https://sendou.ink/u/sendou" */
	url: string;
	/** @example "https://cdn.discordapp.com/avatars/79237403620945920/6fc41a44b069a0d2152ac06d1e496c6c.png" */
	avatarUrl: string | null;
	/** @example "FI" */
	country: string | null;
	socials: {
		twitch: string | null;
		/** YouTube channel ID @example "UCLN8s9Ogn71QcCYIPtb9mtw" */
		youtube: string | null;
		/** @deprecated */
		twitter: null;
		bsky: string | null;
	};
	plusServerTier: 1 | 2 | 3 | null;
	weaponPool: Array<ProfileWeapon>;
	badges: Array<Badge>;
	/** Teams user is member of. The main team is always first in the array. */
	teams: Array<GlobalTeamMembership>;
	/** Splatoon 3 splashtag name & ID, if one is set. @example "Sendou#2955" */
	inGameName: string | null;
	/** User's pronouns. @example { "subject": "he", "object": "him" } */
	pronouns: Pronouns | null;
	peakXp: number | null;
	/** Users current (or previous if it's off-season) ranked season (SendouQ & ranked tournaments) rank. Null if no rank for the season in question or the season does not have yet enough players on the leaderboard. */
	currentRank: SeasonalRank | null;
}
Example 200 /api/user/274

From creates read API token and calls public endpoint

Response

{
  "id": 274,
  "name": "Sendou",
  "discordId": "79237403620945920",
  "avatarUrl": null,
  "url": "https://sendou.ink/u/79237403620945920",
  "country": null,
  "inGameName": null,
  "pronouns": null,
  "plusServerTier": null,
  "socials": {
    "twitch": null,
    "youtube": null,
    "bsky": null,
    "twitter": null
  },
  "currentRank": null,
  "peakXp": null,
  "weaponPool": [],
  "badges": [],
  "teams": []
}
GET

/api/user/{userId|discordId|customUrl}/ids

GetUserIdsResponse
interface GetUserIdsResponse {
	id: number;
	/** @example "79237403620945920" */
	discordId: string;
	/** @example "sendou" */
	customUrl: string | null;
}
Example 200 /api/user/274/ids

From GET user IDs endpoint works without authentication

Response

{
  "id": 274,
  "discordId": "79237403620945920",
  "customUrl": null
}
GET

/api/team/{teamId}

GetTeamResponse
interface GetTeamResponse {
	id: number;
	/** Name of the global team. @example "Moonlight" */
	name: string;
	/** URL for the global team page. @example "https://sendou.ink/t/moonlight" */
	teamPageUrl: string;
	/** URL for the global team logo. @example "https://sendou.nyc3.cdn.digitaloceanspaces.com/pickup-logo-uReSb1b1XS3TWGLCKMDUD-1719054364813.webp" */
	logoUrl: string | null;
}
Example 200 /api/team/1

From every read endpoint responds to a read token

Response

{
  "id": 1,
  "name": "Team 1",
  "logoUrl": null,
  "teamPageUrl": "https://sendou.ink/t/team-1"
}
GET

/api/calendar/{year}/{week}

GetCalendarWeekResponse
type GetCalendarWeekResponse = Array<{
	/** @example "In The Zone 30" */
	name: string;
	tournamentId: number | null;
	/** @example "https://sendou.ink/to/9/brackets" */
	tournamentUrl: string | null;
	/** @example "2024-01-12T20:00:00.000Z" */
	startTime: string;
}>;
Example 200 /api/calendar/2026/43

From every read endpoint responds to a read token

Response

[
  {
    "name": "Waters Inc",
    "startTime": "2026-10-23T09:03:13.000Z",
    "tournamentId": 1,
    "tournamentUrl": "https://sendou.ink/to/1/brackets"
  }
]
GET

/api/user/{userId}/active-match

GetUsersActiveMatchResponse
interface GetUsersActiveMatchResponse {
	/** The user's current match ID or null if none */
	matchId: number | null;
	/** What kind of match the user is in right now */
	lobby: "sendouq" | "tournament" | null;
	/** The ID of the tournament (null for sendouq or no match) */
	tournamentId: number | null;
	/** The bracket index within the tournament (null for sendouq or no match). Can be used with GET /api/tournament/{tournamentId}/brackets/{bracketIdx} */
	bracketIdx: number | null;
}
Example 200 /api/user/276/active-match

From returns active SendouQ match for user

Response

{
  "matchId": 1,
  "lobby": "sendouq",
  "tournamentId": null,
  "bracketIdx": null
}
GET

/api/sendouq/match/{matchId}

GetSendouqMatchResponse
interface GetSendouqMatchResponse {
	teamAlpha: SendouqMatchTeam | null;
	teamBravo: SendouqMatchTeam | null;
	mapList: Array<MapListMap>;
}
Example 200 /api/sendouq/match/1

From every read endpoint responds to a read token

Response

{
  "mapList": [
    {
      "map": {
        "mode": "SZ",
        "stage": {
          "id": 22,
          "name": "Marlin Airport"
        }
      },
      "winnerTeamId": null,
      "source": "BOTH",
      "participatedUserIds": null,
      "ko": null
    },
    {
      "map": {
        "mode": "TC",
        "stage": {
          "id": 14,
          "name": "Manta Maria"
        }
      },
      "winnerTeamId": null,
      "source": "BOTH",
      "participatedUserIds": null,
      "ko": null
    },
    {
      "map": {
        "mode": "RM",
        "stage": {
          "id": 2,
          "name": "Hagglefish Market"
        }
      },
      "winnerTeamId": null,
      "source": "BOTH",
      "participatedUserIds": null,
      "ko": null
    }
  ],
  "teamAlpha": {
    "id": 1,
    "score": 0,
    "players": [
      {
        "userId": 292,
        "rank": {
          "name": "GOLD",
          "isPlus": false
        }
      },
      {
        "userId": 293,
        "rank": {
          "name": "GOLD",
          "isPlus": false
        }
      },
      {
        "userId": 294,
        "rank": {
          "name": "GOLD",
          "isPlus": false
        }
      }
    ]
  },
  "teamBravo": {
    "id": 2,
    "score": 0,
    "players": [
      {
        "userId": 296,
        "rank": {
          "name": "GOLD",
          "isPlus": false
        }
      },
      {
        "userId": 297,
        "rank": {
          "name": "GOLD",
          "isPlus": false
        }
      },
      {
        "userId": 298,
        "rank": {
          "name": "GOLD",
          "isPlus": false
        }
      }
    ]
  }
}
GET

/api/tournament/{tournamentId}

GetTournamentResponse
interface GetTournamentResponse {
	/** @example "In The Zone 30" */
	name: string;
	/** @example "https://sendou.ink/to/9/brackets" */
	url: string;
	/** @example "https://sendou-assets.nyc3.cdn.digitaloceanspaces.com/img/tournament-logos/itz.avif" */
	logoUrl: string | null;
	/** @example "2024-01-12T20:00:00.000Z" */
	startTime: string;
	teams: {
		registeredCount: number;
		checkedInCount: number;
	};
	brackets: TournamentBracket[];
	organizationId: number | null;
	/** Has the tournament concluded (results added to user profiles & no editing possible anymore) */
	isFinalized: boolean;
}
Example 200 /api/tournament/1

From every read endpoint responds to a read token

Response

{
  "name": "Waters Inc",
  "startTime": "2026-10-23T09:03:13.000Z",
  "url": "https://sendou.ink/to/1/brackets",
  "logoUrl": null,
  "teams": {
    "checkedInCount": 4,
    "registeredCount": 4
  },
  "brackets": [
    {
      "name": "Bracket",
      "type": "single_elimination"
    }
  ],
  "organizationId": null,
  "isFinalized": false
}
Example 401 /api/tournament/1

From GET request includes CORS headers in response

Response

{
  "error": "Missing Authorization header"
}
GET

/api/tournament/{tournamentId}/teams

GetTournamentTeamsResponse
type GetTournamentTeamsResponse = Array<{
	id: number;
	/** @example "Team Olive" */
	name: string;
	/** @example "2024-01-12T20:00:00.000Z" */
	registeredAt: string;
	checkedIn: boolean;
	/** URL for the tournament team page. @example "https://sendou.ink/to/9/teams/327" */
	url: string;
	/** URL for the global team page. @example "https://sendou.ink/t/moonlight" */
	teamPageUrl: string | null;
	/** Pickup team logos are only shown before the tournament starts to organizers and the team's own members. @example "https://sendou.nyc3.cdn.digitaloceanspaces.com/pickup-logo-uReSb1b1XS3TWGLCKMDUD-1719054364813.webp" */
	logoUrl: string | null;
	seed: number | null;
	/** Overall placement in the tournament. Null while the team is still playing. @example 5 */
	placement: number | null;
	/** Sets and maps the team has won and lost across every bracket it played. Null before the tournament has started. */
	stats: {
		setWins: number;
		setLosses: number;
		mapWins: number;
		mapLosses: number;
	} | null;
	/** Only shown before the tournament starts to organizers and the team's own members. */
	mapPool: Array<StageWithMode> | null;
	/** Non-resetting MMR used for autoseeding: average of the members' seeding power. Ranked and unranked tournaments feed separate values. */
	seedingPower: {
		ranked: number | null;
		unranked: number | null;
	};
	members: Array<{
		userId: number;
		/** @example "Sendou" */
		name: string;
		/** @example "79237403620945920" */
		discordId: string;
		/** @example "https://cdn.discordapp.com/avatars/79237403620945920/6fc41a44b069a0d2152ac06d1e496c6c.png" */
		avatarUrl: string | null;
		/** @example "FI" */
		country: string | null;
		captain: boolean;
		/** Splatoon 3 splashtag as set at the time of the tournament. Only for tournaments with "Require IGN's" enabled. @example "Sendou#2955" */
		inGameName: string | null;
		/** User's pronouns. @example { "subject": "he", "object": "him" } */
		pronouns: Pronouns | null;
		/** Switch friend code used for identification purposes. Only shown to the tournament's organizers and only for 30 days after the start (120 days for leagues). @example "1234-5678-9101" */
		friendCode: string | null;
		/** @example "2024-01-12T20:00:00.000Z" */
		joinedAt: string;
	}>;
}>;
Example 200 /api/tournament/1/teams

From returns set wins, map wins and placement of tournament teams

Response

[
  {
    "id": 1,
    "name": "Fadel and Sons",
    "url": "https://sendou.ink/to/1/teams/1",
    "teamPageUrl": null,
    "seed": 1,
    "placement": 1,
    "stats": {
      "setWins": 2,
      "setLosses": 0,
      "mapWins": 4,
      "mapLosses": 0
    },
    "registeredAt": "2026-10-23T11:03:13.000Z",
    "checkedIn": true,
    "seedingPower": {
      "ranked": null,
      "unranked": null
    },
    "members": [
      {
        "userId": 276,
        "name": "Spencer27",
        "discordId": "1113617910988800003",
        "avatarUrl": null,
        "country": null,
        "captain": true,
        "inGameName": null,
        "pronouns": null,
        "friendCode": "9592-1909-4032",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      },
      {
        "userId": 277,
        "name": "Sierra.Emmerich48",
        "discordId": "1113617910988800004",
        "avatarUrl": null,
        "country": "US",
        "captain": false,
        "inGameName": null,
        "pronouns": null,
        "friendCode": "7660-9946-6404",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      },
      {
        "userId": 278,
        "name": "Elias_Cormier86",
        "discordId": "1113617910988800005",
        "avatarUrl": null,
        "country": "TN",
        "captain": false,
        "inGameName": null,
        "pronouns": null,
        "friendCode": "1291-0231-8209",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      }
    ],
    "logoUrl": null,
    "mapPool": null
  },
  {
    "id": 2,
    "name": "Pagac - Sanford",
    "url": "https://sendou.ink/to/1/teams/2",
    "teamPageUrl": null,
    "seed": 2,
    "placement": 2,
    "stats": {
      "setWins": 1,
      "setLosses": 1,
      "mapWins": 2,
      "mapLosses": 2
    },
    "registeredAt": "2026-10-23T11:03:13.000Z",
    "checkedIn": true,
    "seedingPower": {
      "ranked": null,
      "unranked": null
    },
    "members": [
      {
        "userId": 280,
        "name": "Floyd.Haag68",
        "discordId": "1113617910988800007",
        "avatarUrl": null,
        "country": "WF",
        "captain": true,
        "inGameName": null,
        "pronouns": null,
        "friendCode": "5618-9675-2294",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      },
      {
        "userId": 281,
        "name": "Birdie.Wehner-Wintheiser",
        "discordId": "1113617910988800008",
        "avatarUrl": null,
        "country": null,
        "captain": false,
        "inGameName": null,
        "pronouns": null,
        "friendCode": "2049-9982-1172",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      },
      {
        "userId": 282,
        "name": "Greg0",
        "discordId": "1113617910988800009",
        "avatarUrl": null,
        "country": "IT",
        "captain": false,
        "inGameName": null,
        "pronouns": null,
        "friendCode": "1686-1376-3610",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      }
    ],
    "logoUrl": null,
    "mapPool": null
  },
  {
    "id": 3,
    "name": "Wisozk and Sons",
    "url": "https://sendou.ink/to/1/teams/3",
    "teamPageUrl": null,
    "seed": 3,
    "placement": 3,
    "stats": {
      "setWins": 0,
      "setLosses": 1,
      "mapWins": 0,
      "mapLosses": 2
    },
    "registeredAt": "2026-10-23T11:03:13.000Z",
    "checkedIn": true,
    "seedingPower": {
      "ranked": null,
      "unranked": null
    },
    "members": [
      {
        "userId": 284,
        "name": "Emerson65",
        "discordId": "1113617910988800011",
        "avatarUrl": null,
        "country": "YT",
        "captain": true,
        "inGameName": null,
        "pronouns": null,
        "friendCode": "4520-4447-5002",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      },
      {
        "userId": 285,
        "name": "Kenny.Dare",
        "discordId": "1113617910988800012",
        "avatarUrl": null,
        "country": "AE",
        "captain": false,
        "inGameName": null,
        "pronouns": null,
        "friendCode": "6686-7264-3088",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      },
      {
        "userId": 286,
        "name": "Mohamed.Koelpin-OReilly",
        "discordId": "1113617910988800013",
        "avatarUrl": null,
        "country": "MU",
        "captain": false,
        "inGameName": null,
        "pronouns": {
          "subject": "they",
          "object": "them"
        },
        "friendCode": "7183-4694-2343",
        "joinedAt": "2026-10-23T11:03:13.000Z"
      }
    ],
    "logoUrl": null,
    "mapPool": null
  }
]
GET

/api/tournament/{tournamentId}/players

GetTournamentPlayersResponse
type GetTournamentPlayersResponse = Array<{
	userId: number;
	matchIds: number[];
}>;
Example 200 /api/tournament/1/players

From every read endpoint responds to a read token

Response

[
  {
    "userId": 276,
    "matchIds": [
      1,
      3
    ]
  },
  {
    "userId": 277,
    "matchIds": [
      1,
      3
    ]
  },
  {
    "userId": 278,
    "matchIds": [
      1,
      3
    ]
  }
]
GET

/api/tournament/{tournamentId}/casted

GetCastedTournamentMatchesResponse
interface GetCastedTournamentMatchesResponse {
	/** Match currently assigned to each casting channel. Stays until the next match is assigned, so it may already be finished. */
	current: Array<{
		matchId: number;
		channel: TournamentCastChannel;
	}>;
	/** Matches that are locked to be casted. */
	future: Array<{
		matchId: number;
		channel: TournamentCastChannel;
	}>;
}
Example 200 /api/tournament/1/casted

From every read endpoint responds to a read token

Response

{
  "current": [
    {
      "matchId": 1,
      "channel": {
        "type": "TWITCH",
        "channelId": "sendouq",
        "url": "https://twitch.tv/sendouq"
      }
    }
  ],
  "future": []
}
GET

/api/tournament/{tournamentId}/streams

GetTournamentStreamsResponse
type GetTournamentStreamsResponse = Array<
	{
		platform: "TWITCH" | "YOUTUBE";
		/** Twitch login or YouTube channel ID. */
		channelId: string;
		/** Where the stream can be watched. @example "https://twitch.tv/iplsplatoon" */
		url: string;
		viewerCount: number;
	} & ({ type: "PLAYER"; userId: number } | { type: "CAST" })
>;
Example 200 /api/tournament/1/streams

From every read endpoint responds to a read token

Response

[]
GET

/api/tournament-match/{matchId}

GetTournamentMatchResponse
interface GetTournamentMatchResponse {
	teamOne: TournamentMatchTeam | null;
	teamTwo: TournamentMatchTeam | null;
	/** Name of the bracket this match belongs to. @example "Alpha Bracket" */
	bracketName: string | null;
	/** Name of the round this match belongs to. @example "Grand Finals" */
	roundName: string | null;
	/** Maps already played (with their results) followed by the upcoming ones. Once the set is over, only the played maps. `null` until both teams are known. */
	mapList: Array<MapListMap> | null;
	/** @example "https://sendou.ink/to/9/matches/695" */
	url: string;
}
Example 200 /api/tournament-match/1

From every read endpoint responds to a read token

Response

{
  "teamOne": {
    "id": 1,
    "score": 2
  },
  "teamTwo": {
    "id": 4,
    "score": 0
  },
  "url": "https://sendou.ink/to/1/matches/1",
  "mapList": [
    {
      "map": {
        "mode": "SZ",
        "stage": {
          "id": 1,
          "name": "Eeltail Alley"
        }
      },
      "participatedUserIds": [
        276,
        277,
        278
      ],
      "winnerTeamId": 1,
      "source": "TO",
      "ko": null
    },
    {
      "map": {
        "mode": "SZ",
        "stage": {
          "id": 2,
          "name": "Hagglefish Market"
        }
      },
      "participatedUserIds": [
        276,
        277,
        278
      ],
      "winnerTeamId": 1,
      "source": "TO",
      "ko": null
    }
  ],
  "bracketName": "Bracket",
  "roundName": "Semis"
}
GET

/api/tournament/{tournamentId}/brackets/{bracketIndex}

GetTournamentBracketResponse
interface GetTournamentBracketResponse {
	data: TournamentBracketData;
	teams: Array<{
		id: number;
		checkedIn: boolean;
	}>;
	meta: {
		/** How many teams per group? (round robin only) */
		teamsPerGroup?: number;
		/** How many groups? (swiss only) */
		groupCount?: number;
		/** How many rounds? (swiss only) */
		roundCount?: number;
	};
}
Example 200 /api/tournament/1/brackets/0

From every read endpoint responds to a read token

Response

{
  "data": {
    "stage": [
      {
        "id": 1,
        "name": "Bracket",
        "type": "single_elimination",
        "settings": {
          "consolationFinal": false
        },
        "number": 1,
        "createdAt": 1792753393
      }
    ],
    "group": [
      {
        "id": 1,
        "stageId": 1,
        "number": 1
      }
    ],
    "round": [
      {
        "id": 1,
        "stageId": 1,
        "groupId": 1,
        "section": "winners",
        "number": 1,
        "maps": {
          "count": 3,
          "type": "BEST_OF",
          "list": [
            {
              "mode": "SZ",
              "stageId": 1
            },
            {
              "mode": "SZ",
              "stageId": 2
            },
            {
              "mode": "SZ",
              "stageId": 3
            }
          ]
        },
        "isPlayableAt": null
      },
      {
        "id": 2,
        "stageId": 1,
        "groupId": 1,
        "section": "winners",
        "number": 2,
        "maps": {
          "count": 3,
          "type": "BEST_OF",
          "list": [
            {
              "mode": "SZ",
              "stageId": 1
            },
            {
              "mode": "SZ",
              "stageId": 2
            },
            {
              "mode": "SZ",
              "stageId": 3
            }
          ]
        },
        "isPlayableAt": null
      }
    ],
    "match": [
      {
        "id": 1,
        "stageId": 1,
        "groupId": 1,
        "roundId": 1,
        "number": 1,
        "startedAt": 1792753393,
        "scheduledAt": null,
        "winnerSide": "opponent1",
        "opponent1": {
          "id": 1,
          "position": 1,
          "score": 2,
          "totalKos": 0
        },
        "opponent2": {
          "id": 4,
          "position": 4,
          "score": 0,
          "totalKos": 0
        }
      },
      {
        "id": 2,
        "stageId": 1,
        "groupId": 1,
        "roundId": 1,
        "number": 2,
        "startedAt": 1792753393,
        "scheduledAt": null,
        "winnerSide": "opponent1",
        "opponent1": {
          "id": 2,
          "position": 2,
          "score": 2,
          "totalKos": 0
        },
        "opponent2": {
          "id": 3,
          "position": 3,
          "score": 0,
          "totalKos": 0
        }
      },
      {
        "id": 3,
        "stageId": 1,
        "groupId": 1,
        "roundId": 2,
        "number": 1,
        "startedAt": 1792753393,
        "scheduledAt": null,
        "winnerSide": "opponent1",
        "opponent1": {
          "id": 1,
          "score": 2,
          "totalKos": 0
        },
        "opponent2": {
          "id": 2,
          "score": 0,
          "totalKos": 0
        }
      }
    ]
  },
  "teams": [
    {
      "id": 1,
      "checkedIn": true
    },
    {
      "id": 4,
      "checkedIn": true
    },
    {
      "id": 2,
      "checkedIn": true
    }
  ],
  "meta": {}
}
GET

/api/tournament/{tournamentId}/brackets/{bracketIndex}/standings

GetTournamentBracketStandingsResponse
interface GetTournamentBracketStandingsResponse {
	finished: boolean;
	standings: Array<{
		tournamentTeamId: number;
		placement: number;
		/** (round robin & swiss only) id of the group the team played in. Placements are shared across groups, meaning e.g. every group's winner has the placement 1. */
		groupId?: number;
		stats?: {
			setWins: number;
			setLosses: number;
			mapWins: number;
			mapLosses: number;
			/** @deprecated points are no longer tracked, see koCount instead */
			points?: number;
			/** (round robin only) how many knockout wins the team has */
			koCount?: number;
			winsAgainstTied: number;
			lossesAgainstTied?: number;
			/** (swiss only) average win percentage of the team's opponents in sets, used as a tiebreaker */
			opponentSetWinPercentage?: number;
			/** (swiss only) average win percentage of the team's opponents in maps, used as a tiebreaker */
			opponentMapWinPercentage?: number;
		};
	}>;
}
Example 200 /api/tournament/1/brackets/0/standings

From every read endpoint responds to a read token

Response

{
  "finished": true,
  "standings": [
    {
      "tournamentTeamId": 1,
      "placement": 1
    },
    {
      "tournamentTeamId": 2,
      "placement": 2
    },
    {
      "tournamentTeamId": 4,
      "placement": 3
    }
  ]
}
GET

/api/org/{organizationId}

GetTournamentOrganizationResponse
interface GetTournamentOrganizationResponse {
	id: number;
	/** @example "Dapple Productions" */
	name: string;
	description: string | null;
	/** @example "https://sendou.ink/org/dapple-productions" */
	url: string;
	/** @example "https://sendou.nyc3.cdn.digitaloceanspaces.com/gBn45bbUMXM6359ZDQS5_-1722059432073.webp" */
	logoUrl: string | null;
	members: Array<TournamentOrganizationMember>;
	socialLinkUrls: Array<string>;
}
Example 200 /api/org/1

From every read endpoint responds to a read token

Response

{
  "id": 1,
  "name": "Organization 1",
  "description": null,
  "logoUrl": null,
  "socialLinkUrls": [],
  "url": "https://sendou.ink/org/organization-1",
  "members": [
    {
      "userId": 274,
      "discordId": "79237403620945920",
      "name": "Sendou",
      "pronouns": null,
      "role": "ADMIN",
      "roleDisplayName": null
    }
  ]
}

Write endpoints

Need a write token, and the token's owner to be an organizer of the tournament. The type is the JSON request body. A successful request responds with an empty 200, a failed one with 400 and { error: string, fieldErrors?: object }.

POST

/api/tournament/{id}/starting-brackets

TournamentStartingBracketsBody
interface TournamentStartingBracketsBody {
	startingBrackets: Array<{
		tournamentTeamId: number;
		startingBracketIdx: number;
	}>;
}
Example 200 /api/tournament/1/starting-brackets

From updates tournament starting brackets via API

Request body

{
  "startingBrackets": [
    {
      "tournamentTeamId": 1,
      "startingBracketIdx": 0
    }
  ]
}

Empty response

POST

/api/tournament/{id}/teams/upsert

TournamentUpsertTeamBody
interface TournamentUpsertTeamBody {
	/** Present when editing an existing registration, absent when adding a new team. */
	tournamentTeamId?: number;
	/** Team name for a pickup team. Either `name` or `teamId` must be given. */
	name?: string;
	/** Linked sendou.ink team id. Name and logo are sourced from the team. */
	teamId?: number;
	/** Roster member that is the team owner/captain. */
	ownerUserId: number;
	/** Full roster; members missing from the list are removed from the team. */
	members: Array<{
		userId: number;
		inGameName?: string;
	}>;
}
Example 200 /api/tournament/1/teams/upsert

From upserts tournament team registration via API

Request body

{
  "name": "Api Pickup",
  "ownerUserId": 280,
  "members": [
    {
      "userId": 280
    },
    {
      "userId": 281
    },
    {
      "userId": 282
    },
    {
      "userId": 283
    }
  ]
}

Empty response

Example 400 /api/tournament/1/teams/upsert

From returns 400 with field errors for invalid upsert registration body

Request body

{
  "ownerUserId": 280,
  "members": [
    {
      "userId": 280
    }
  ]
}

Response

{
  "error": "Validation failed",
  "fieldErrors": {
    "pickUpName": "forms:errors.regTeamNameRequired"
  }
}
POST

/api/tournament/{id}/teams/{tournamentTeamId}/add-member

TournamentTeamMemberBody
interface TournamentTeamMemberBody {
	userId: number;
}
Example 200 /api/tournament/1/teams/1/add-member

From adds member to tournament team via API

Request body

{
  "userId": 280
}

Empty response

Example 400 /api/tournament/1/teams/1/add-member

From returns 400 when user is not the organizer of this tournament

Request body

{
  "userId": 281
}

Response

{
  "error": "Not an organizer"
}
Example 401 /api/tournament/1/teams/1/add-member

From returns 401 for invalid token

Request body

{
  "userId": 280
}

Response

{
  "error": "Invalid token"
}
Example 403 /api/tournament/1/teams/1/add-member

From returns 403 when using read token for write endpoint

Request body

{
  "userId": 280
}

Response

{
  "error": "Write token required"
}
POST

/api/tournament/{id}/teams/{tournamentTeamId}/update-member-ign

TournamentUpdateMemberIgnBody
interface TournamentUpdateMemberIgnBody {
	userId: number;
	inGameName: string;
}
Example 200 /api/tournament/1/teams/1/update-member-ign

From updates member IGN via API

Request body

{
  "userId": 276,
  "inGameName": "NewName#9999"
}

Empty response

Types

The types the endpoints above share.

SendouqMatchPlayer

schema.ts
type SendouqMatchPlayer = {
	userId: number;
	/** User's rank at the start time of the match */
	rank: SendouQRank | null;
};

SendouQRank

schema.ts
type SendouQRank = { name: TierName; isPlus: boolean };

TournamentCastChannel

schema.ts
type TournamentCastChannel = {
	type: "TWITCH" | "YOUTUBE";
	/** Twitch login or YouTube channel ID. @example "iplsplatoon" */
	channelId: string;
	/** @example "https://twitch.tv/iplsplatoon" */
	url: string;
};

TournamentOrganizationMember

schema.ts
interface TournamentOrganizationMember {
	userId: number;
	/** @example "Sendou" */
	name: string;
	/** @example "79237403620945920" */
	discordId: string;
	/** User's pronouns. @example { "subject": "he", "object": "him" } */
	pronouns: Pronouns | null;
	role: "ADMIN" | "MEMBER" | "ORGANIZER" | "STREAMER";
	roleDisplayName: string | null;
}

Weapon

schema.ts
type Weapon = {
	id: number;
	name: string;
};

ProfileWeapon

schema.ts
type ProfileWeapon = Weapon & { isFiveStar: boolean };

GlobalTeamMembership

schema.ts
interface GlobalTeamMembership {
	/** ID for the global team page. */
	id: number;
	/** Role of the user in the team. */
	role: TeamMemberRole | null;
}

TeamMemberRole

schema.ts
type TeamMemberRole =
	| "CAPTAIN"
	| "CO_CAPTAIN"
	| "FRONTLINE"
	| "SLAYER"
	| "SKIRMISHER"
	| "SUPPORT"
	| "MIDLINE"
	| "BACKLINE"
	| "FLEX"
	| "SUB"
	| "COACH"
	| "CHEERLEADER";

SeasonalRank

schema.ts
interface SeasonalRank {
	tier: {
		name: RankTierName;
		isPlus: boolean;
	};
	/** Which season this rank is for. @example 7 */
	season: number;
}

RankTierName

schema.ts
type RankTierName =
	| "LEVIATHAN"
	| "DIAMOND"
	| "PLATINUM"
	| "GOLD"
	| "SILVER"
	| "BRONZE"
	| "IRON";

Badge

schema.ts
type Badge = {
	/** @example "Monday Afterparty" */
	name: string;
	count: number;
	/** @example "https://sendou-assets.nyc3.cdn.digitaloceanspaces.com/badges/monday.avif" */
	imageUrl: string;
	/** @example "https://sendou-assets.nyc3.cdn.digitaloceanspaces.com/badges/monday.gif" */
	gifUrl: string;
};

ModeShort

schema.ts
type ModeShort = "TW" | "SZ" | "TC" | "RM" | "CB";

Stage

schema.ts
type Stage = {
	id: number;
	name: string;
};

MapListMap

schema.ts
type MapListMap = {
	map: StageWithMode;
	/**
	 * One of the following:
	 * - id of the team that picked the map
	 * - "DEFAULT" if it was a default map, something went wrong with the algorithm typically
	 * - "RANDOM" if it was a random map from the tournament's map pool that neither team picked
	 * - "BOTH" both teams picked the map
	 * - "TO" if it was a TO pick (from predefined maplist)
	 * - "COUNTERPICK" if it was a counterpick
	 * - "ROLL" if it was randomly selected
	 */
	source:
		| number
		| "DEFAULT"
		| "RANDOM"
		| "BOTH"
		| "TO"
		| "COUNTERPICK"
		| "ROLL";
	/** `null` if the map has not been played yet. */
	winnerTeamId: number | null;
	participatedUserIds: Array<number> | null;
	/** (round robin only) whether the map ended in a knockout. `null` if not tracked. */
	ko: boolean | null;
};

TournamentMatchTeam

schema.ts
type TournamentMatchTeam = {
	id: number;
	score: number;
};

TournamentBracket

schema.ts
type TournamentBracket = {
	type: "double_elimination" | "single_elimination" | "round_robin" | "swiss";
	name: string;
};

TournamentBracketData

schema.ts
type TournamentBracketData = BracketData;