Errores
La forma del cuerpo de error, todos los códigos que devuelve la API externa de SETTO, qué significa cada uno y si reintentar sirve de algo.
Los errores regresan como JSON con el estado HTTP que esperarías y un code
legible por máquina sobre el que puedes ramificar. Ramifica sobre code, no
sobre el message legible por humanos — los mensajes pueden cambiar, los códigos
son parte del contrato. Hay una forma para cada error deliberado y una segunda,
un poco distinta, para los fallos de validación de la petición.
El cuerpo de error
{
"statusCode": 404,
"code": "TOURNAMENT_NOT_FOUND",
"message": "TOURNAMENT_NOT_FOUND"
}| Campo | Tipo | Notas |
|---|---|---|
statusCode | number | Refleja el estado HTTP |
code | string | Estable, en mayúsculas con guiones bajos. Ramifica sobre este |
message | string | Legible para humanos; puede ser igual a code |
Todos los códigos
| Estado | code | Significado | ¿Reintentar? |
|---|---|---|---|
400 | INVALID_ID | Un identificador de ruta no es un UUID v4. Se rechaza antes de ejecutar el handler, así que nunca llega a ser un 404 | No — corrige la petición |
400 | (validación, ver abajo) | Un parámetro de consulta falló la validación — un valor de populate desconocido, un limit fuera de 1..100, un division que no es UUID | No — corrige la petición |
400 | INVALID_CURSOR | El cursor enviado a GET /v1/external/activity no es un nextCursor que haya devuelto | No — quítalo o reenvía nextCursor |
401 | MISSING_API_TOKEN | No hay cabecera Authorization, o no es Bearer setto_… | No |
401 | INVALID_API_TOKEN | Token desconocido, revocado o de otro entorno | No |
401 | EXPIRED_API_TOKEN | El expiresAt del token ya pasó | No — crea un token nuevo |
403 | INSUFFICIENT_SCOPE | Token válido, pero le falta el permiso que esta ruta necesita | No |
404 | TOURNAMENT_NOT_FOUND | No existe ese torneo para este token | No |
404 | ROUND_NOT_FOUND | No existe esa ronda para este token | No |
429 | RATE_LIMITED | Más de 120 peticiones en los últimos 60 segundos para este token | Sí — después de Retry-After |
Cualquier cosa en el rango 5xx es una falla del lado de SETTO. Reintenta con
backoff exponencial y jitter; si persiste, el payload que pedías probablemente no
es el problema.
Errores de validación
Un 400 de la capa global de validación es la única respuesta que no usa la
forma uniforme: su message es un arreglo de strings, uno por regla fallida.
{
"statusCode": 400,
"message": [
"must be a valid enum value,club,club.courts,divisions,divisions.rounds,sponsors,circuit,circuitCategory,rankedTiebreakRules"
],
"error": "Bad Request"
}Manéjalo a la defensiva:
const body = await res.json();
const message = Array.isArray(body.message)
? body.message.join('; ')
: body.message;Las causas habituales, por frecuencia:
- Un valor de
populateque no está en la lista blanca de ese endpoint — las listas están en Populate, y el mensaje de error te enumera los valores legales. limitfuera de1..100, uoffsetmenor que0.- Un valor de
statusosportque no es miembro del enum. - Un filtro
divisionque no es un UUID v4.
El otro 400: un id de ruta mal formado
El 400 cubre dos formas distintas. El cuerpo con message como arreglo viene
de la capa de parámetros de consulta. Un identificador de ruta que no es un UUID
v4 — por ejemplo /v1/external/tournaments/no-es-uuid — se rechaza antes, y
responde con la forma uniforme de la API:
{
"statusCode": 400,
"code": "INVALID_ID",
"message": "INVALID_ID"
}La distinción que importa: un idx mal formado es un 400, nunca un 404.
Solo un id bien formado que este token no alcanza devuelve el 404 de abajo. Es
decir, INVALID_ID siempre significa tu cadena está mal, no el recurso no
existe.
El 404 esconde más de lo que dice
Un 404 significa este token no alcanza ese recurso. No distingue entre:
- un
idxque no existe en ningún lado de SETTO; - un
idxque pertenece a una organización que este token no alcanza; - un
idxde un torneo que el dueño del token no administra — o que ya no administra: el alcance de un token personal se calcula de nuevo en cada petición; - un
idxque pertenece a un torneo personal, que solo alcanza el token personal de su dueño.
Eso es deliberado: la API nunca confirma la existencia de datos que tu token no puede leer, así que los ids no se pueden sondear. En la práctica, si estás seguro de que el torneo existe, revisa que estés usando un token que lo alcance: el token de la organización que es su dueña, o el token personal de alguien que lo administra.
Escrituras rechazadas
Una escritura rechazada no escribió nada, y su cuerpo agrega details: una
entrada por cada partido, equipo o jugador que la detuvo, cada una
{ code, … } con las llaves que aplican (gameIdx, teamIdx, index,
field, limit, current, step, reason, …). La página de cada operación
en la referencia de la API lista los códigos que puede
contestar. Los que cambian la lógica de un cliente:
| Estado | code | Significado | ¿Reintentar? |
|---|---|---|---|
400 | INSCRIPTION_COST_BELOW_PLATFORM_FEE | La validación de precio del propio panel rechaza cualquier cambio del torneo (editarlo, publicarlo, regresarlo a borrador, archivarlo, restaurarlo) mientras el organizador absorbe la comisión de SETTO («Yo absorbo la comisión») y el precio («Costo de inscripción») no la cubre | No — el organizador sube el precio o deja de absorber la comisión en el panel; la API nunca cambia precios |
403 | DIVISION_LIMIT_REACHED, TEAM_LIMIT_REACHED | El plan del torneo no admite más categorías o equipos (details: limit, current, tier) | No — el organizador mejora el plan en el panel |
404 | DIVISION_NOT_FOUND, TEAM_NOT_FOUND, PLAYER_NOT_FOUND, GAME_NOT_FOUND | No hay esa categoría, equipo, jugador o partido en este torneo, para este token | No |
409 | SCHEDULE_BUSY, TOURNAMENT_BUSY | Se estaba guardando otro cambio del mismo torneo | Sí — una vez, la misma petición (y la misma Idempotency-Key) |
409 | PREVIEW_STALE | El torneo o la categoría cambió desde la vista previa (de una baja, un formato, unos grupos o un cuadro) | No — vuelve a pedir la vista previa |
409 | FORMAT_HAS_PLAYED_GAMES, ROUND_HAS_PLAYED_GAMES, KNOCKOUT_HAS_PLAYED_GAMES | Un formato, unos grupos o un cuadro reemplazarían partidos con resultado (details los lista) | No — borrar esos resultados lo decide el organizador |
409 | DRAW_SEATS_WITHDRAWN_TEAM | La clasificación final pone a un equipo dado de baja en un lugar del cuadro | No — el organizador elige quién toma su lugar (otro equipo o un BYE) y la confirmación se repite con replace |
409 | GAME_HAS_RESULT | El partido ya tiene un resultado | No — muéstraselo al organizador; solo con su sí se reemplaza con overwrite: true |
409 | IDEMPOTENCY_IN_PROGRESS | Una petición con la misma Idempotency-Key sigue en curso | Sí — la misma petición, en unos segundos |
409 | TEAM_HAS_PAYMENTS | El equipo tiene rastro de pago (un pago, una marca de pagado o un enlace de pago), así que la API no lo borra | No — borrarlo, y cualquier reembolso, se hace en el panel |
410 | CONFIRMATION_EXPIRED | El confirmationToken de una vista previa tiene más de 15 minutos | No — vuelve a pedir la vista previa |
422 | IDEMPOTENCY_KEY_REUSED | Esa Idempotency-Key pertenece a otra petición | No — una escritura nueva lleva una llave nueva |
429 | TOURNAMENT_CREATE_DAILY_CAP | 20 torneos creados por la API en 24 horas | Sí — después de Retry-After |
Cualquier otro rechazo de una escritura (un nombre ocupado, una categoría
llena, un equipo que ya tiene partidos, un choque de programación, …) es un
estado que decide el organizador: muéstrale los details en lugar de
reintentar.
Qué hacer en cada estado
const MAX_RETRIES = 3;
async function settoFetch(path, { token, attempt = 0, ...init } = {}) {
const res = await fetch(
`https://api.setto.io/v1${path}`,
{
...init,
headers: { ...init.headers, Authorization: `Bearer ${token}` },
},
);
if (res.ok) return res.json();
const body = await res.json().catch(() => ({}));
const canRetry = attempt < MAX_RETRIES;
const retry = async (waitMs) => {
await new Promise((r) => setTimeout(r, waitMs));
return settoFetch(path, { ...init, token, attempt: attempt + 1 });
};
switch (res.status) {
case 401:
// MISSING_API_TOKEN | INVALID_API_TOKEN | EXPIRED_API_TOKEN
throw new Error(`SETTO credential problem: ${body.code}`);
case 403:
throw new Error('SETTO token lacks the required scope');
case 404:
return null; // quien llama decide si un miss es fatal
case 429:
// Respeta Retry-After, pero ríndete en lugar de dar vueltas para siempre:
// un token siempre pasado de presupuesto giraría aquí indefinidamente.
if (!canRetry) throw new Error('SETTO API: sigue rate limited');
return retry(Number(res.headers.get('Retry-After') ?? 1) * 1000);
default:
// Solo 5xx, con backoff exponencial y jitter para que una flota de
// clientes no vuelva toda al mismo tiempo.
if (res.status >= 500 && canRetry) {
return retry(2 ** attempt * 500 + Math.random() * 250);
}
throw new Error(`SETTO API ${res.status}: ${JSON.stringify(body)}`);
}
}Reintenta 429 y 5xx, y ponle un límite — aquí son tres intentos. Nunca
reintentes 400, 401, 403 ni 404: la misma petición va a fallar igual, y
un bucle de reintentos sobre 401 solo quema tu límite de uso.
Relacionado
- Autenticación — el desglose completo de
401contra403. - Límites de uso — las cabeceras que te dejan
evitar el
429por completo.
Paginación
Cómo funcionan limit y offset en GET /v1/external/tournaments, qué contiene el sobre meta, y por qué los demás endpoints de listado devuelven todo de una vez.
Límites de uso
120 peticiones por 60 segundos por token, un segundo techo por IP, las cabeceras de respuesta X-RateLimit, cómo aplicar backoff ante un 429, y cómo las peticiones condicionales con ETag ahorran ancho de banda.