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ímite | 120 peticiones |
| Ventana | 60 segundos |
| Alcance | Un 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
| Cabecera | En | Significado |
|---|---|---|
X-RateLimit-Limit | autenticadas | Peticiones permitidas por ventana — 120 |
X-RateLimit-Remaining | autenticadas | Peticiones que te quedan en la ventana actual |
X-RateLimit-Reset | autenticadas | Timestamp Unix, en segundos, de cuándo se reinicia la ventana |
Retry-After | solo en 429 | Segundos que hay que esperar antes de reintentar |
HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1789238460Las 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
{
"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
429se 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-Remainingbaja de ~10, haz una pausa hastaX-RateLimit-Reset. Prevenir el429es 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"'
# 304const 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ón | Costo |
|---|---|
| Pedir en cada vista de página | 1 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 nocturna | Unas 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.
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.
Listar torneos
Usa GET /v1/external/tournaments para encontrar los torneos de tu organización, filtrarlos por estado y deporte, y paginar el archivo de una temporada.