SETTO API

Pagination

How limit and offset work on GET /v1/external/tournaments, what the meta envelope contains, and why the other list endpoints return everything at once.

Exactly one endpoint paginates: GET /v1/external/tournaments. It takes limit and offset, returns its rows under data, and reports the window under meta. The other two list endpoints — divisions and teams — return a bare JSON array with every row for that tournament, because both are bounded by how many categories and entries a single tournament can hold.

The envelope

GET /v1/external/tournaments?limit=2&offset=0
{
  "data": [
    {
      "idx": "3c9b7f52-6d41-4a8e-b1f0-2e7d5c9a4b18",
      "name": "Torneo Apertura 2026"
    },
    { "idx": "a41e08d7-33b2-4c96-8f5a-6b0d2e1c7934", "name": "Liga Otoño 2026" }
  ],
  "meta": { "limit": 2, "offset": 0, "total": 37 }
}
FieldMeaning
dataThe rows in this window
meta.limitThe page size actually applied
meta.offsetThe offset actually applied
meta.totalTotal rows matching the filters, ignoring limit and offset

Parameters

ParameterTypeDefaultRange
limitinteger251100
offsetinteger00 or greater

Values outside the range are a 400, not a clamp: limit=0, limit=101 and offset=-1 all fail validation. Non-integers fail too.

Ordering

Tournaments come back ordered by startDate descending — the most recent season first. The order is stable across pages for a fixed data set.

Walking every page

Stop when you have collected meta.total rows, or when a page comes back short.

async function listAllTournaments(token, params = {}) {
  const BASE = 'https://api-production-ea80.up.railway.app/v1';
  const limit = 100;
  const all = [];
  let offset = 0;
  let total = Infinity;

  while (offset < total) {
    const query = new URLSearchParams({
      ...params,
      limit: String(limit),
      offset: String(offset),
    });
    const res = await fetch(`${BASE}/external/tournaments?${query}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!res.ok) throw new Error(`SETTO API ${res.status}`);

    const { data, meta } = await res.json();
    all.push(...data);
    total = meta.total;
    offset += limit;

    if (data.length === 0) break; // defensive: never loop forever
  }

  return all;
}
def list_all_tournaments(token, **params):
    BASE = "https://api-production-ea80.up.railway.app/v1"
    headers = {"Authorization": f"Bearer {token}"}
    limit, offset, total, out = 100, 0, None, []

    while total is None or offset < total:
        res = requests.get(
            f"{BASE}/external/tournaments",
            headers=headers,
            params={**params, "limit": limit, "offset": offset},
            timeout=10,
        )
        res.raise_for_status()
        body = res.json()
        if not body["data"]:
            break
        out.extend(body["data"])
        total = body["meta"]["total"]
        offset += limit

    return out

Filter before you paginate

status and sport are applied before the window, so meta.total reflects the filtered set. Narrowing with status=IN_PROGRESS is almost always cheaper than paging through an archive.

Offset drift

Offset pagination reads a moving list. If a tournament is created, or its startDate is edited, while you are between pages, a row can shift across the page boundary and be returned twice or skipped.

For a long export:

  • page with limit=100 so there are fewer boundaries to cross;
  • de-duplicate on idx as you accumulate;
  • or pin the window with a filter that will not change under you, such as status=COMPLETED.

Do not treat offset as a cursor you can persist between runs — re-run the walk from offset=0 each time.

The other lists

GET /v1/external/tournaments/{idx}/divisions and GET /v1/external/tournaments/{idx}/teams take no limit or offset and return a JSON array directly — no data/meta envelope:

GET /v1/external/tournaments/{idx}/divisions
[
  {
    "idx": "7b2f4d16-9c83-4e50-a7d1-3f6c8b204e59",
    "name": "Cuarta Fuerza Varonil",
    "number": 1
  },
  {
    "idx": "c58a1e93-0b74-42df-96e8-5a3d7f1b28c0",
    "name": "Segunda Fuerza Femenil",
    "number": 2
  }
]

Divisions are ordered by number ascending. Teams are ordered by seed ascending with unseeded entries last, then by name.

If a very large tournament makes those payloads uncomfortable, narrow the teams call with the division filter — ?division={divisionIdx} — rather than asking for a page size that does not exist.

On this page