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 |
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 alcance que esta ruta necesita | No |
404 | TOURNAMENT_NOT_FOUND | No existe ese torneo para esta organización | No |
404 | ROUND_NOT_FOUND | No existe esa ronda para esta organización | 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 esta organización no puede ver 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 esta organización no tiene ese recurso. No distingue
entre:
- un
idxque no existe en ningún lado de SETTO; - un
idxque pertenece a otra organización; - un
idxque pertenece a un torneo personal, fuera de cualquier organización.
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 el token de la organización que es su dueña.
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.