SETTO API

Authentication

How SETTO External API tokens work — personal and organization tokens, the bearer header, live vs test prefixes, the six permissions, what each token reaches, expiry, rotation and revocation, and the difference between 401 and 403.

Every request to the External API carries one credential: a SETTO API token, sent in the Authorization header as a bearer token. A token is either personal — it belongs to you and reaches every tournament you manage — or an organization's — it belongs to one organization and reaches only that organization's tournaments. Tokens are created in the SETTO organizer dashboard, read by default, and are shown in full exactly once. This page covers the two kinds of token, the header format, the two token prefixes, what a token can and cannot see, the six permissions a token can carry, and how to rotate or revoke one.

Connecting an AI assistant?

claude.ai, ChatGPT, Codex and Claude Code can skip the token entirely: they sign you in with your SETTO account and get an access token of their own, which is an API token too (same header, same prefix) and acts as you. See Sign in with your SETTO account.

The header

GET /v1/external/tournaments HTTP/1.1
Host: api.setto.io
Authorization: Bearer setto_live_YOUR_TOKEN_HERE

There is no alternative. The API does not accept a token in a query string, a cookie or an X-Api-Key header — only Authorization: Bearer. A missing or malformed header is a 401, never a silent anonymous read.

Token format

A token is a prefix plus a secret. The first 16 characters are the prefix, which SETTO stores in clear so the dashboard can show you which token is which; the rest is the secret, which SETTO stores only as a hash.

PrefixMeaning
setto_live_…A production token. Works against the production API.
setto_test_…A non-production token. Rejected by the production API.

Use setto_test_ tokens when you point an integration at a staging or local SETTO API; use setto_live_ for anything real. Both carry the same permissions.

The prefix is what you see everywhere the token is displayed — in the dashboard list and in the token.prefix field of GET /v1/external/me. Log the prefix when you need to tell two tokens apart. Never log the whole token.

Two kinds of token

Personal token («Token personal»)Organization token («Token de la organización»)
OwnerYouOne organization
Where to create itConfiguración → Tokens de API, scope PersonalConfiguración → Tokens de API with the organization as the scope, or the organization's own page, tab API
Who can create itAny SETTO user, for themselvesThe organization's creator or a member with the ADMIN role
What it readsEvery tournament you manage right now: the ones you created (personal tournaments included), the ones you were invited to manage, and the tournaments of every organization you belong to, including the ones shared into itThe organization's tournaments, including the ones shared into it
Limit10 active tokens per user10 active tokens per organization
When access changesLeave an organization or lose an invitation, and the next request for one of those tournaments is a 404Keeps reading; every permission beyond read stops working once the person who created it is no longer an ADMIN of the organization
GET /v1/external/meowner.type: "user", organization: nullowner.type: "organization", organization filled in

Which to use

  • An organization token for systems the organization owns: a club website, a sync job, an assistant several people share. It reaches nothing outside the organization and keeps reading when people come and go.
  • A personal token for your own assistant (Claude Code, Codex, Cursor, VS Code — see the MCP server) or a script you run yourself. It reaches everything you manage, and follows your access as it changes.
  • No token at all for claude.ai, ChatGPT, Codex or Claude Code: they can sign in with your SETTO account and act as you, with the permissions you approve.

Creating a token

In app.setto.io open Configuración → Tokens de API. The scope picker at the top of Configuración decides whose token you create: keep it on Personal for a personal token, or pick an organization for an organization token. Then choose Crear token.

  • Anyone can create personal tokens for themselves, and only they see them — not even the ADMINs of their organizations.
  • Only the organization creator or a member with the ADMIN role may create, list or revoke an organization's tokens. Other members get a 403 from the management endpoints; the dashboard tells them so. The organization's own page, tab API, lists the same tokens.
  • A name is required. Name tokens after their consumer (Club website, Claude Desktop), not after a person.
  • Under Permisos, «Leer» is always ticked; tick any other permission the consumer needs (see What a token can change).
  • Expiry is optional: never, 30, 90 or 365 days. An expired token returns 401 with EXPIRED_API_TOKEN.
  • A user may hold at most 10 active personal tokens, and an organization at most 10 active organization tokens; the two limits are separate. Revoke one before creating the eleventh.

Copy the secret immediately

The full token appears once, in the dialog that follows creation. SETTO keeps only a hash, so nobody — not you, not SETTO support — can recover it later. If it is lost, revoke and re-create.

What a token can change

A token carries one or more of six permissions («Permisos»), its scopes. Every token carries read, and that is all most tokens ever need. You tick the others under Permisos when you create the token; the token list shows a badge for each one it carries (schedule reads «Programa partidos»).

Permission («Permiso»)ScopeWhat it allowsAPI routes today
«Leer»readSee tournaments, categories, teams, results, schedules and the activity. Always includedEvery GET, the schedule proposal and the previews (they write nothing)
«Programar partidos»scheduleSet and move the day, time and court of games not yet playedPOST /v1/external/tournaments/{idx}/schedule/placements
«Gestionar torneos»manageCreate and edit tournaments, categories, seeds, teams and players; register teams; replace a player («Sustitución») or withdraw a team («Baja»); set a category's format, its groups and its knockout drawPOST /v1/external/tournaments and the other tournament setup and draw writes (reference)
«Cargar resultados»resultsRecord, correct and clear game resultsPOST and DELETE /v1/external/tournaments/{idx}/games/{gameIdx}/result
«Página pública»landingChange the design, sections, sponsors and visibility of the public pageNone yet
«Enviar comunicaciones»communicationsEmail the participants of your tournamentsNone yet

Today the API writes three kinds of thing: the tournament setup (creating and editing tournaments and their categories, registering and editing teams and players, «Sustitución», «Baja») and its draws (a category's format, its groups, its knockout draw and «Confirmar cuadro»), which need manage; game results, which need results; and the start time, court and length of a tournament's games, which needs schedule. The other two permissions can already be granted and are reserved for the writes that come next; a token that carries only them can do nothing more than read today. No permission changes a price, a paid mark, a payment record or a refund; a withdrawal closes the team's unpaid payment links. A token without the permission a route needs gets 403 INSUFFICIENT_SCOPE. GET /v1/external/me lists a token's scopes.

Every permission is also capped by what the person behind the token can do in the dashboard, and who that is depends on the kind of token:

  • a personal token writes in any tournament its owner manages at the moment of the request;
  • an organization token with any permission beyond read can only be created by an organization ADMIN, and writes only while that person is still an ADMIN of the organization. Once they are not, the write is 403 INSUFFICIENT_SCOPE, and a current ADMIN creates a new token;
  • an OAuth access token — what claude.ai, ChatGPT, Codex or Claude Code get when you sign in with your SETTO account — acts as you, with the permissions you approved on the consent screen.

What a token can see

Which tournaments a token reaches depends on its kind:

  • a personal token reaches every tournament its owner manages at the moment of the request — the ones they created (personal tournaments included), the ones they accepted an invitation to manage, and the tournaments of every organization they belong to, including the ones shared into it. It is worked out again on every request: leave an organization, and its tournaments answer 404 on the next call;
  • an organization token reaches the tournaments whose home organization is that organization, plus the ones shared into it.

For every tournament it reaches, a token reads:

  • the tournament — including ones in DRAFT and ARCHIVED, and including tournaments whose public page is turned off (isPublic: false);
  • the divisions, teams, rounds, pools, draws, games, scores and standings that hang off those tournaments — including entries flagged isVisible: false.

A tournament's organization says where it lives. With an organization token it is always the token's organization. With a personal token it is the tournament's home organization, or null for a personal tournament.

No token reads:

  • other organizations' data through an organization token, even when its creator belongs to both. One organization token, one organization;
  • personal contact data — emails, phone numbers, social handles and the like are stripped from every response;
  • payment or billing data — registration fees paid, payment methods, Stripe identifiers and organizer notes are not part of the contract.

Because hidden and draft material is returned, treat the payload as internal until you filter it. If you are rendering a public page, filter on status and on isVisible yourself.

Keep tokens server-side

A setto_live_ token grants read access to everything it reaches: a whole organization, or — for a personal token — every tournament you manage. It belongs in a server environment variable, a secret manager or a serverless function — never in a browser bundle, a mobile app, a public repository, or a URL.

If you need tournament data in a browser, put a small server route in front of the API:

app/api/standings/route.ts
export async function GET() {
  const res = await fetch(
    'https://api.setto.io/v1/external/rounds/9d3c6a84-7e15-4b02-8f6d-1c4a9e7b53f2',
    {
      // SETTO_API_TOKEN is server-only. Never prefix it with NEXT_PUBLIC_.
      headers: { Authorization: `Bearer ${process.env.SETTO_API_TOKEN}` },
      next: { revalidate: 60 },
    },
  );

  // Check the STATUS before the body. An error payload has no `isVisible` and
  // no `pools`, so projecting it would answer 200 with an empty result and hide
  // an expired token behind "this round has no standings".
  if (res.status === 404) {
    return new Response('Not found', { status: 404 });
  }

  if (!res.ok) {
    // Your credential's problem is not your visitor's. Log the detail, return a
    // generic upstream failure — never forward the upstream body or status.
    console.error(`SETTO API ${res.status} while loading standings`);
    return Response.json(
      { error: 'upstream_unavailable' },
      { status: 502 },
    );
  }

  const round = await res.json();

  // Your token sees hidden and draft material; this route does not require one,
  // so anything returned here is public. A hidden round answers 404 like a
  // missing one — never confirm that it exists.
  if (round.isVisible === false) {
    return new Response('Not found', { status: 404 });
  }

  // ...then publish an explicit projection rather than the upstream payload, so
  // a field added to the API later cannot leak through this route by default.
  return Response.json({
    name: round.name,
    pools: (round.pools ?? []).map((pool) => ({
      name: pool.name,
      standings: (pool.standings ?? []).map((row) => ({
        rank: row.rank,
        team: row.team?.name,
        points: row.awardedPoints,
      })),
    })),
  });
}

Cache the projection, never the upstream response — a cached full payload is a leak waiting for the next person who reaches for it.

The same rule applies to the Try it panel in the reference: it fires the request from your own browser, so paste a throwaway or soon-to-be rotated token rather than your production one.

Rotation and revocation

Tokens do not rotate themselves. To rotate:

  1. Create a second token with the same intent (Website widget (new)).
  2. Deploy the new value to the consumer.
  3. Confirm traffic moved — token.lastUsedAt on the new token starts advancing.
  4. Revoke the old token in Configuración → Tokens de API, under the scope it was created in.

Revocation takes effect immediately: the next request with that token is a 401 with INVALID_API_TOKEN. Revoke first and ask questions later if a token leaks — nothing about the API is stateful, so a revoked-and-replaced token costs you nothing but a redeploy.

Rotate on a schedule if you can, and always when someone with access leaves.

401 vs 403

They mean different things and want different responses.

StatusCodesWhat happenedWhat to do
401MISSING_API_TOKENNo Authorization header, or it was not Bearer setto_…Fix the request
401INVALID_API_TOKENUnknown, revoked, or wrong-environment token (a setto_test_ token in production)Re-issue the token
401EXPIRED_API_TOKENThe token's expiresAt has passedCreate a new token
403INSUFFICIENT_SCOPEThe token is valid but does not carry the permission the route needs (or, for an organization token, its creator is no longer an ADMIN)Do not retry

In short: 401 means we do not know who you are — stop and get a working credential. 403 means we know exactly who you are and the answer is still no — retrying with the same token will never work.

An OAuth access token answers 401 once its app is disconnected in Configuración → Conexiones or its 30 days run out; the app then refreshes it or asks you to sign in again. Its 403 INSUFFICIENT_SCOPE is fixed by connecting the app again and ticking the missing permission.

A resource this token cannot reach — another organization's, or a tournament you no longer manage — is not a 403. It is a 404 (TOURNAMENT_NOT_FOUND / ROUND_NOT_FOUND), identical to the response for an id that does not exist anywhere — the API never confirms the existence of data you are not allowed to read. See Errors.

On this page