SETTO API

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_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 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 ADMIN role may create, list or revoke tokens. Other members get a 403 from 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 401 with EXPIRED_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 organization is that organization — 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.

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:

app/api/standings/route.ts
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:

  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 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.

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 scope the route needsDo 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.

On this page