SETTO API

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.

Cada token de API puede hacer 120 peticiones por 60 segundos. Cada respuesta autenticada lleva tres cabeceras que te dicen en qué punto vas de la ventana actual, y un 429 lleva una cuarta que te dice cuánto esperar. Ese presupuesto es por token, no por organización, así que repartir una carga entre dos tokens lo duplica — hasta el segundo techo, por IP, que se explica abajo. Cachear es la solución más barata.

El presupuesto

Límite120 peticiones
Ventana60 segundos
AlcanceUn token de API

GET /v1/external/me reporta esos mismos números, así que un cliente puede descubrirlos al arrancar en lugar de tenerlos escritos a mano:

{ "rateLimit": { "limit": 120, "windowSeconds": 60 } }

Un segundo techo, por IP

Antes de leer ningún token, la API también limita a 300 peticiones por 60 segundos por dirección IP. Todos los tokens que llamen desde el mismo host lo comparten. Cruzarlo es el mismo 429 con el mismo código RATE_LIMITED y su Retry-After, pero — como todavía no se identificó ningún token — sin las cabeceras X-RateLimit-*. Un 429 cuya respuesta no trae X-RateLimit-Remaining es el techo por IP, no el del token.

Cabeceras de respuesta

CabeceraEnSignificado
X-RateLimit-LimitautenticadasPeticiones permitidas por ventana — 120
X-RateLimit-RemainingautenticadasPeticiones que te quedan en la ventana actual
X-RateLimit-ResetautenticadasTimestamp Unix, en segundos, de cuándo se reinicia la ventana
Retry-Aftersolo en 429Segundos que hay que esperar antes de reintentar
HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1789238460

Las cuatro se exponen a los navegadores vía CORS, así que un proxy de front-end también puede leerlas.

Cómo manejar un 429

429 Too Many Requests
{
  "statusCode": 429,
  "code": "RATE_LIMITED",
  "message": "RATE_LIMITED"
}

Respeta Retry-After. No sondees el endpoint hasta que ceda.

async function withRateLimit(fn, { attempts = 5 } = {}) {
  for (let attempt = 0; attempt < attempts; attempt++) {
    const res = await fn();
    if (res.status !== 429) return res;

    const retryAfter = Number(res.headers.get('Retry-After') ?? 1);
    const jitter = Math.random() * 0.3 * retryAfter;
    await new Promise((r) => setTimeout(r, (retryAfter + jitter) * 1000));
  }
  throw new Error('SETTO API: still rate limited after retries');
}

Dos reglas que importan más que el bucle de reintentos:

  • Serializa, no abras el abanico. Diez peticiones en paralelo que reintentan cada una ante un 429 se van a sincronizar y a golpear juntas la frontera de reinicio. Agrega jitter, o corre con un límite de concurrencia pequeño (2–4) y deja que drene.
  • Frena antes de estrellarte. Si X-RateLimit-Remaining baja de ~10, haz una pausa hasta X-RateLimit-Reset. Prevenir el 429 es gratis; recuperarse de él no.

Peticiones condicionales

Las respuestas llevan un ETag fuerte. Devuélvelo en If-None-Match y un recurso sin cambios responde 304 Not Modified sin cuerpo — que es la forma más barata de sondear una ronda a la que todavía no le capturan un resultado. Un 304 sigue gastando una petición de ambos límites; lo que ahorra es el payload y el parseo de tu lado, así que combínalo con una caché en tu servidor en lugar de tratarlo como gratis.

# Primera lectura: captura el ETag
curl -sD - -o round.json \
  https://api.setto.io/v1/external/rounds/9d3c6a84-7e15-4b02-8f6d-1c4a9e7b53f2 \
  -H "Authorization: Bearer $SETTO_API_TOKEN" | grep -i '^etag:'
# etag: "2f31-Xy7Qr0hV3n2mJk8sLdTg5pWcAe4"

# Lecturas posteriores: pide solo los cambios
curl -s -o /dev/null -w '%{http_code}\n' \
  https://api.setto.io/v1/external/rounds/9d3c6a84-7e15-4b02-8f6d-1c4a9e7b53f2 \
  -H "Authorization: Bearer $SETTO_API_TOKEN" \
  -H 'If-None-Match: "2f31-Xy7Qr0hV3n2mJk8sLdTg5pWcAe4"'
# 304
const cache = new Map(); // url -> { etag, body }

async function getCached(url, token) {
  const hit = cache.get(url);
  const res = await fetch(url, {
    headers: {
      Authorization: `Bearer ${token}`,
      ...(hit ? { 'If-None-Match': hit.etag } : {}),
    },
  });

  if (res.status === 304 && hit) return hit.body;

  const body = await res.json();
  const etag = res.headers.get('ETag');
  if (etag) cache.set(url, { etag, body });
  return body;
}

Guarda el ETag junto al payload, indexado por la URL completa incluyendo la cadena de populate — un populate distinto es una representación distinta y va a tener un ETag distinto.

Diseñar para el límite

Un marcador en vivo no necesita 120 peticiones por minuto.

PatrónCosto
Pedir en cada vista de página1 petición por visitante — no hagas esto
Caché del lado del servidor, TTL de 30–60 s~1–2 peticiones por minuto por ronda, sin importar el tráfico
Sondeo condicional cada 30 s~2 peticiones por minuto, casi todas 304
Exportación completa nocturnaUnas cuantas decenas de peticiones, una vez

Pon la caché en tu servidor, no en el navegador: el navegador no puede guardar tu token de todos modos (ver Autenticación).

Si de verdad necesitas más de 120 peticiones por minuto — una federación grande sincronizando decenas de torneos — emite un segundo token para el trabajo en lote, para que la exportación no deje sin aire a tus páginas en vivo. Una organización puede tener hasta 10 tokens activos.

En esta página