# SETTO External API — endpoint reference

Base URL: `https://api-production-ea80.up.railway.app/v1`
Auth: `Authorization: Bearer $SETTO_API_TOKEN` on every request.
All six operations are `GET`. Every id is a UUID v4 exposed as `idx`.

Canonical source: <https://docs.setto.io/openapi.json>.

## Operations

| operationId | Method + path | Tag |
| --- | --- | --- |
| `getMe` | `GET /v1/external/me` | Organization |
| `listTournaments` | `GET /v1/external/tournaments` | Tournaments |
| `getTournament` | `GET /v1/external/tournaments/{idx}` | Tournaments |
| `listDivisions` | `GET /v1/external/tournaments/{idx}/divisions` | Divisions |
| `listTeams` | `GET /v1/external/tournaments/{idx}/teams` | Teams |
| `getRound` | `GET /v1/external/rounds/{idx}` | Rounds |

## `getMe`

`GET /v1/external/me` — no parameters.

Response:

| Field | Type | Notes |
| --- | --- | --- |
| `organization` | object | `idx`, `name`, `slug`, `avatar`, `website` |
| `token` | object | `idx`, `name`, `prefix`, `scopes`, `createdAt`, `expiresAt`, `lastUsedAt` |
| `rateLimit` | object | `limit` (120), `windowSeconds` (60) |

`token.prefix` is the visible head of the secret; the secret itself is never
returned.

## `listTournaments`

`GET /v1/external/tournaments`

| Parameter | In | Required | Type | Default | Notes |
| --- | --- | --- | --- | --- | --- |
| `status` | query | no | enum | — | `DRAFT`, `UPCOMING`, `IN_PROGRESS`, `COMPLETED`, `ARCHIVED` |
| `sport` | query | no | enum | — | `PADEL`, `TENNIS`, `PICKLEBALL` |
| `populate` | query | no | csv | — | `club` |
| `limit` | query | no | integer | `25` | 1–100 |
| `offset` | query | no | integer | `0` | ≥ 0 |

Response: `{ data: Tournament[], meta: { limit, offset, total } }`, ordered by
`startDate` descending. Unfiltered results include `DRAFT` and `ARCHIVED`
tournaments.

## `getTournament`

`GET /v1/external/tournaments/{idx}`

| Parameter | In | Required | Type | Default |
| --- | --- | --- | --- | --- |
| `idx` | path | yes | uuid | — |
| `populate` | query | no | csv | `club,divisions` |

Legal `populate` values: `club`, `club.courts`, `divisions`,
`divisions.rounds`, `sponsors`, `circuit`, `circuitCategory`,
`rankedTiebreakRules`.

Response: one `Tournament`.

## `listDivisions`

`GET /v1/external/tournaments/{idx}/divisions`

| Parameter | In | Required | Type | Default |
| --- | --- | --- | --- | --- |
| `idx` | path | yes | uuid | — |
| `populate` | query | no | csv | `rounds` |

Legal `populate` values: `rounds`, `teams`, `teams.playerProfiles`,
`teams.leagueTeam`, `circuitCategory`.

Response: the tournament's divisions (categories). Hidden ones are included
with `isVisible: false`.

## `listTeams`

`GET /v1/external/tournaments/{idx}/teams`

| Parameter | In | Required | Type | Default |
| --- | --- | --- | --- | --- |
| `idx` | path | yes | uuid | — |
| `populate` | query | no | csv | `playerProfiles,division` |
| `division` | query | no | uuid | — |

Legal `populate` values: `playerProfiles`, `captainProfile`, `leagueTeam`,
`division`. `division` restricts the result to one category.

## `getRound`

`GET /v1/external/rounds/{idx}`

| Parameter | In | Required | Type | Default |
| --- | --- | --- | --- | --- |
| `idx` | path | yes | uuid | — |
| `populate` | query | no | csv | the common set (below) |

Legal `populate` values:

```
division
pools
pools.teams
pools.standings
pools.standings.team
pools.standings.teamStats
games
games.homeTeam
games.awayTeam
games.winner
games.court
games.score
games.score.sets
draws
draws.rounds
draws.rounds.games
draws.rounds.games.homeTeam
draws.rounds.games.awayTeam
draws.rounds.games.winner
draws.rounds.games.court
draws.rounds.games.score
draws.rounds.games.score.sets
```

Omitting `populate` gives the common set: `division`, `pools` (+ `teams`),
`games` (+ `homeTeam`, `awayTeam`, `winner` — but NOT `games.score`), and
`draws.rounds.games` (+ `homeTeam`, `awayTeam`, `winner`, `score`, `score.sets`).
That is a large payload — narrow it when you only need standings or only need
the bracket.

Note the asymmetry: bracket games come with their scores by default, flat
`games` do not. Ask for `games.score,games.score.sets` explicitly when you read
results off the flat array — and remember that any `populate` you send REPLACES
the default list, so re-list everything else you still need.

## Populate semantics (all endpoints)

- Comma-separated list, no spaces.
- A relation is present only when requested, or when it is in the default list.
- Nested paths pull their parents in implicitly.
- An unknown value returns `400` and the message enumerates the legal values.

## Data model

```
Organization
  └─ Tournament       type: TEAM_LEAGUE | LEAGUE | TOURNAMENT | INDIVIDUAL_LEAGUE
                      sport: PADEL | TENNIS | PICKLEBALL
                      status: DRAFT → UPCOMING → IN_PROGRESS → COMPLETED → ARCHIVED
       └─ Division    "categoría": isDoubles, isVisible, price
            └─ Round  type: ROUND_ROBIN_ROUND | SINGLE_ELIMINATION_ROUND | QUALIFIERS_ROUND
                 ├─ Pool   group + standings
                 ├─ Game   status: UPCOMING | FINISHED | CANCELLED | FORFEIT | BYE
                 └─ Draw → DrawRound → games   (bracket shape)
```

| Object | Notable fields |
| --- | --- |
| `Tournament` | `idx`, `slug`, `name`, `type`, `sport`, `status`, `startDate`, `endDate`, `timezone`, `publicUrl` (`https://www.setto.io/t/<slug>`) |
| `Division` | `idx`, `name`, `number`, `color`, `isDoubles`, `isVisible`, `price` |
| `Round` | `idx`, `type`, `isVisible`; `division` may be `null` on a `TEAM_LEAGUE` round |
| `Game` | `status`, `score.sets[]`, `homeTeamDetails` / `awayTeamDetails` describing placeholder slots such as "Winner of Game 3" |
| `Team` | `idx`, `seed`, `players[]` = `{ idx, firstName, lastName, avatar }`, `division`, optional `leagueTeam` |
| `Standing` | `rank` is the **stored** rank; the public site applies head-to-head tiebreaks on top, so a rendered table can differ |

## Pagination

Only `listTournaments` paginates (`limit`, `offset`, `meta.total`). The other
five endpoints return the whole collection for the resource.

## Rate limits

120 requests / 60 s per token, plus a pre-auth ceiling of 300 requests / 60 s per
IP. The IP ceiling returns the same `429` `RATE_LIMITED` with `Retry-After` but
no `X-RateLimit-*` headers.

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | 120 |
| `X-RateLimit-Remaining` | Requests left in the window |
| `X-RateLimit-Reset` | Unix seconds when the window resets |
| `Retry-After` | On `429` only — seconds to wait |

Responses carry strong ETags; `If-None-Match` turns an unchanged read into a
`304` with no body. It still spends one request of the rate limit — what it
saves is bandwidth.

## What is never returned

No emails, phone numbers or social handles; no payment or billing data; no
tournaments outside the token's organization (including personal tournaments).
