Authentication
How SETTO External API tokens work — the bearer header, live vs test prefixes, organization scope, expiry, rotation and revocation, and the difference between 401 and 403.
Every request to the External API carries one credential: an organization API
token, sent in the Authorization header as a bearer token. Tokens are issued
from the SETTO organizer dashboard, belong to an organization rather than to
a person, are read-only, and are shown in full exactly once. This page covers the
header format, the two token prefixes, what a token can and cannot see, and how
to rotate or revoke one.
The header
GET /v1/external/tournaments HTTP/1.1
Host: api-production-ea80.up.railway.app
Authorization: Bearer setto_live_YOUR_TOKEN_HEREThere 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.
| Prefix | Meaning |
|---|---|
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 are read-only.
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.
Creating a token
In app.setto.io go to Organización, select the organization, open the API tab, and choose Create token.
- Only the organization creator or a member with the
ADMINrole may create, list or revoke tokens. Other members get a403from the management endpoints; the tab tells them so. - A name is required. Name tokens after their consumer (
Club website,Airtable sync), not after a person. - Expiry is optional: never, 30, 90 or 365 days. An expired token returns
401withEXPIRED_API_TOKEN. - An organization may hold at most 10 active tokens at a time. 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 see
A token is scoped to one organization. It reads:
- every tournament whose
organizationis that organization — including ones inDRAFTandARCHIVED, 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.
It does not read:
- personal tournaments — tournaments created by a user outside any organization are invisible to every token, even the owner's;
- other organizations' data, even if you are a member of both. One token, one organization; use one token per 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 your whole organization. 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:
export async function GET() {
const res = await fetch(
'https://api-production-ea80.up.railway.app/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:
- Create a second token with the same intent (
Website widget (new)). - Deploy the new value to the consumer.
- Confirm traffic moved —
token.lastUsedAton the new token starts advancing. - Revoke the old token from the API tab.
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.
| Status | Codes | What happened | What to do |
|---|---|---|---|
401 | MISSING_API_TOKEN | No Authorization header, or it was not Bearer setto_… | Fix the request |
401 | INVALID_API_TOKEN | Unknown, revoked, or wrong-environment token (a setto_test_ token in production) | Re-issue the token |
401 | EXPIRED_API_TOKEN | The token's expiresAt has passed | Create a new token |
403 | INSUFFICIENT_SCOPE | The token is valid but does not carry the scope the route needs | 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.
A resource belonging to another organization 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.