---
title: "Create a tournament"
method: POST
path: "/tournaments"
tags: ["Tournament: Lifecycle"]
---

# Create a tournament

`POST /tournaments`

Creates a tournament as a DRAFT with approval status NOT_SUBMITTED. Nothing is public yet: call submit-for-approval, then open registration once staff approve.

HOST AUTHORITY. `hostType` and `hostId` are yours to choose, but not to invent - you must actually hold the authority you name, or the request is refused. `user` requires `hostId` to be your own user id (otherwise 403 error_forbidden); `org` requires an active OWNER or ADMIN membership of that organization (otherwise 403 error_not_org_admin); `league` requires an active league ADMIN or OWNER membership (otherwise 403 error_league_admin_required). You must also be eligible to host at all - a paid plan, or an account at least 30 days old, and not competitively banned - or the request is 403 error_not_eligible_to_host.

BILLING. Creation is billed to YOU, the key owner, because there is no tournament yet to resolve a host from. From this point on the billing flips: every organizer request against the tournament is billed to the HOST - the organization owner or the league owner - including requests made by tournament staff you add later. Choosing a host is therefore the act that binds that person's developer plan, which is exactly why the authority check above is strict.

Unlike the rest of the tournament family this endpoint takes no `identifier`, so it is caller-billed and requires only the tournaments lifecycle capability on the key plus the tournamentOrganizerApi feature on your own plan.

Supply an optional `Idempotency-Key` request header to make retries safe: an identical key and body replays the original tournament id and slug without creating a second tournament, while the same key with a different body returns 409 error_idempotency_key_conflict. Without the header a retried request creates a SECOND tournament, because a tournament has no natural key to collapse on.

## Request body

- TournamentCreateRequestBody — A new tournament. Fields this schema does not list are IGNORED rather than rejected, so a misspelled key silently does nothing - this matters most inside `defaultMatchConfig`, where a dropped key would produce a tournament whose matches cannot be generated.
  - `hostType` 'user' | 'org' | 'league', required — Who hosts the tournament. This choice, together with `hostId`, decides which plan is billed for every later organizer request on the tournament - see the endpoint description.
  - `hostId` string, required — Your own user id when hostType is `user`; the organization id when `org`; the league id when `league`. You must genuinely hold that authority - see the endpoint description.
  - `name` string, required — Tournament name, 3-80 characters after trimming.
  - `description` string
  - `rules` string
  - `gameId` string, required — Convex game id.
  - `playlistId` string — Convex game playlist id.
  - `teamSize` integer, required — Players per side.
  - `defaultMatchConfig` TournamentDefaultMatchConfig, required — Match settings stamped onto every match this tournament generates. Required, and sent whole - there is no per-field default.
    - `gameModeId` string, required — Game mode every generated match is played in. Validated against the game's allowed modes when a match is generated, NOT here - a mode id that does not belong to `gameId` is accepted at create time and surfaces at start time as a match_generation_failed activity entry.
    - `mapSelectionType` 'COMPETITIVE' | 'FLEX', required
    - `mapPreferenceMode` 'PREFERRED' | 'VETO' | 'MANUAL', required — Overridden to MANUAL for the grand final when formatConfig.grandFinalAdvantage.type is MAP_SELECTION.
    - `platform` 'PC' | 'XBOX' | 'PLAYSTATION' | 'CONSOLE_ONLY' | 'CROSSPLAY', required
    - `inputDevice` 'ALL' | 'CONTROLLER' | 'KB_M', required
    - `region` 'NONE' | 'NA_EAST' | 'NA_WEST' | 'EU' | 'ASIA' | 'OCEANIA' | 'SOUTH_AMERICA' | 'MIDDLE_EAST' | 'AFRICA', required
    - `selectedMaps` string[], required — Map pool for every generated match. A per-round `mapPoolOverride` REPLACES this array for that round rather than merging into it.
    - `selectedObjectives` string[]
    - `gameSpecificOptions` object
    - `lockedRules` object — Rule keys neither team may renegotiate on a generated match.
  - `format` 'SINGLE_ELIMINATION' | 'DOUBLE_ELIMINATION' | 'ROUND_ROBIN' | 'SWISS', required — Bracket format of a tournament.
  - `formatConfig` TournamentFormatConfig, required — Format-specific configuration. Sent whole - it replaces the stored object rather than merging into it.
    - `bestOfLadder` object, required — Best-of ladder. Both fields are required when formatConfig is sent.
      - `default` integer, required — Default best-of for every round.
      - `overrides` object, required — Per-round best-of overrides, keyed by round key (e.g. `GF`, `R1`).
    - `grandFinalAdvantage` object, required
      - `type` 'NONE' | 'WIN_HEADSTART' | 'MAP_SELECTION' | 'BRACKET_RESET', required
      - `winHeadstart` integer — Maps the upper-bracket finalist starts ahead by. WIN_HEADSTART only.
    - `swissRoundCount` integer — Swiss only. Number of rounds to play.
    - `roundRobinPasses` union — Round-robin only. 1 = single pass, 2 = double pass.
      - 1
      - 2
  - `minParticipants` integer, required
  - `maxParticipants` integer, required — Entrant cap. Also bounded by the format ceiling (SINGLE_ELIMINATION and DOUBLE_ELIMINATION 128, SWISS 64, ROUND_ROBIN 32) and by the host plan's participant limit, both checked server-side.
  - `registrationMode` 'OPEN' | 'APPLICATION' | 'INVITE_ONLY', required
  - `registrationStartsAt` integer — Unix ms.
  - `registrationEndsAt` integer — Unix ms.
  - `checkInRequired` boolean, required
  - `checkInOpensMinutesBefore` integer
  - `freeAgentsEnabled` boolean, required — Whether unaffiliated players may join a free-agent pool. NOT editable afterwards through the update endpoint.
  - `seedingMethod` 'MANUAL' | 'RANDOM' | 'REGISTRATION' — Defaults to the platform default when omitted. NOT editable afterwards.
  - `startsAt` integer — Unix ms.
  - `endsAt` integer — Unix ms.
  - `prizePool` number
  - `prizeCurrency` string
  - `prizeBreakdown` TournamentPrizeBreakdownEntry[]
    - `placement` integer, required
    - `amount` number
    - `description` string
  - `prizeDescription` string
  - `bannerStorageId` string — Convex storage id from POST /uploads/image with slot=banner (max 2 MB). An id minted by POST /uploads/image-url carries no ownership record and is rejected with 400 error_image_blob_unbound.
  - `logoStorageId` string — Convex storage id from POST /uploads/image with slot=avatar (max 1 MB). An id minted by POST /uploads/image-url carries no ownership record and is rejected with 400 error_image_blob_unbound.
  - `socialLinks` TournamentSocialLinks — Organizer social links.
    - `website` string
    - `x` string
    - `discord` string
    - `youtube` string
    - `twitch` string
    - `instagram` string
    - `tiktok` string
  - `sponsorTags` TournamentSponsorTag[] — Bounded by the host plan's sponsor-tag limit.
    - `prefix` 'POWERED_BY' | 'SPONSORED_BY' | 'PRESENTED_BY' | 'CUSTOM', required
    - `customPrefix` string — Used only when prefix is CUSTOM.
    - `name` string, required
    - `url` string
    - `logoStorageId` string — Convex storage id for the sponsor logo, from POST /uploads/image with slot=avatar (max 1 MB). An id minted by POST /uploads/image-url is rejected with 400 error_image_blob_unbound. On an update, re-send the id already stored on each sponsor row you are not changing.
  - `visibility` 'PUBLIC' | 'UNLISTED', required
  - `leaguePointsEnabled` boolean — LEAGUE HOSTS ONLY. Sending it with a user or org host is a 400.
  - `leaguePointsByPlacement` TournamentPlacementPointsEntry[] — LEAGUE HOSTS ONLY. Omit to use the platform default curve; an empty array is rejected.
    - `placement` integer, required — Final placement, 1 = champion.
    - `points` number, required — League points awarded, 0-1000.

## Response `200`

The draft tournament was created.

- TournamentCreateResponse — The newly created draft tournament.
  - `success` true, required
  - `tournamentId` string, required
  - `slug` string, required — Derived from the name and de-duplicated server-side, so it is NOT predictable from the name you sent. Every other tournament endpoint resolves by this slug, never by `tournamentId`, so store it.
  - `status` 'DRAFT', required — Always DRAFT. A guaranteed post-condition, not an echo - createCore hard-codes it.
  - `approvalStatus` 'NOT_SUBMITTED', required — Always NOT_SUBMITTED. Call submit-for-approval next; the tournament is invisible to the public discovery surface until staff approve it.
  - `timestamp` string, required

## Other responses

- `400` — Bad request (invalid body, cursor, limit, or date).
- `401` — Missing or invalid API key.
- `403` — API key lacks the required permission.
- `409` — Conflict.
- `429` — Rate limited.
- `500` — Internal server error.

---

[API](https://skmtc.net/teambattles/apis/teambattles-api.md) · [All operations](https://skmtc.net/teambattles/apis/teambattles-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/teambattles/teambattles-api/revisions/2ffe8fb53b7d/schema)
