SETTO API

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

404 Not Found
{
  "statusCode": 404,
  "code": "TOURNAMENT_NOT_FOUND",
  "message": "TOURNAMENT_NOT_FOUND"
}
CampoTipoNotas
statusCodenumberRefleja el estado HTTP
codestringEstable, en mayúsculas con guiones bajos. Ramifica sobre este
messagestringLegible para humanos; puede ser igual a code

Todos los códigos

EstadocodeSignificado¿Reintentar?
400INVALID_IDUn identificador de ruta no es un UUID v4. Se rechaza antes de ejecutar el handler, así que nunca llega a ser un 404No — 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 UUIDNo — corrige la petición
400INVALID_CURSOREl cursor enviado a GET /v1/external/activity no es un nextCursor que haya devueltoNo — quítalo o reenvía nextCursor
401MISSING_API_TOKENNo hay cabecera Authorization, o no es Bearer setto_…No
401INVALID_API_TOKENToken desconocido, revocado o de otro entornoNo
401EXPIRED_API_TOKENEl expiresAt del token ya pasóNo — crea un token nuevo
403INSUFFICIENT_SCOPEToken válido, pero le falta el permiso que esta ruta necesitaNo
404TOURNAMENT_NOT_FOUNDNo existe ese torneo para este tokenNo
404ROUND_NOT_FOUNDNo existe esa ronda para este tokenNo
429RATE_LIMITEDMás de 120 peticiones en los últimos 60 segundos para este tokenSí — 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.

400 Bad Request
{
  "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:

  1. Un valor de populate que 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.
  2. limit fuera de 1..100, u offset menor que 0.
  3. Un valor de status o sport que no es miembro del enum.
  4. Un filtro division que 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:

400 Bad Request
{
  "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 idx que no existe en ningún lado de SETTO;
  • un idx que pertenece a una organización que este token no alcanza;
  • un idx de 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 idx que 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:

EstadocodeSignificado¿Reintentar?
400INSCRIPTION_COST_BELOW_PLATFORM_FEELa 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 cubreNo — el organizador sube el precio o deja de absorber la comisión en el panel; la API nunca cambia precios
403DIVISION_LIMIT_REACHED, TEAM_LIMIT_REACHEDEl plan del torneo no admite más categorías o equipos (details: limit, current, tier)No — el organizador mejora el plan en el panel
404DIVISION_NOT_FOUND, TEAM_NOT_FOUND, PLAYER_NOT_FOUND, GAME_NOT_FOUNDNo hay esa categoría, equipo, jugador o partido en este torneo, para este tokenNo
409SCHEDULE_BUSY, TOURNAMENT_BUSYSe estaba guardando otro cambio del mismo torneoSí — una vez, la misma petición (y la misma Idempotency-Key)
409PREVIEW_STALEEl 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
409FORMAT_HAS_PLAYED_GAMES, ROUND_HAS_PLAYED_GAMES, KNOCKOUT_HAS_PLAYED_GAMESUn formato, unos grupos o un cuadro reemplazarían partidos con resultado (details los lista)No — borrar esos resultados lo decide el organizador
409DRAW_SEATS_WITHDRAWN_TEAMLa clasificación final pone a un equipo dado de baja en un lugar del cuadroNo — el organizador elige quién toma su lugar (otro equipo o un BYE) y la confirmación se repite con replace
409GAME_HAS_RESULTEl partido ya tiene un resultadoNo — muéstraselo al organizador; solo con su sí se reemplaza con overwrite: true
409IDEMPOTENCY_IN_PROGRESSUna petición con la misma Idempotency-Key sigue en cursoSí — la misma petición, en unos segundos
409TEAM_HAS_PAYMENTSEl equipo tiene rastro de pago (un pago, una marca de pagado o un enlace de pago), así que la API no lo borraNo — borrarlo, y cualquier reembolso, se hace en el panel
410CONFIRMATION_EXPIREDEl confirmationToken de una vista previa tiene más de 15 minutosNo — vuelve a pedir la vista previa
422IDEMPOTENCY_KEY_REUSEDEsa Idempotency-Key pertenece a otra peticiónNo — una escritura nueva lleva una llave nueva
429TOURNAMENT_CREATE_DAILY_CAP20 torneos creados por la API en 24 horasSí — 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

En esta página