Límites de tasa
Los buckets publicados, los headers que te dicen dónde estás, y las dos realidades del SII para las que igual conviene diseñar.
Los headers
Cada respuesta autenticada lleva:
RateLimit-Limit: 300
RateLimit-Remaining: 287
RateLimit-Reset: 41Reportan el bucket más ajustado que aplica, no el general: un request que está a una emisión de agotar su cuota por segundo lo dice, aunque le queden 280 requests generales.
En un 429, el header Retry-After (en segundos) te dice exactamente cuánto esperar. No adivines
con backoff exponencial cuando la respuesta trae la cifra.
Los endpoints sin autenticación no se cuentan y no llevan estos headers: GET /v1/error_codes sirve
una constante sin datos de nadie.
Los buckets publicados
Por defecto, por credencial — una llave o una sesión de cuenta, cada modo por separado:
| Bucket | Límite | Ventana | Se cuenta por |
|---|---|---|---|
| API general | 300 req/min | minuto deslizante | credencial |
POST /v1/documents | 10 req/s | segundo deslizante | credencial |
POST /v1/tax/f29/filings | 10 req/min | minuto deslizante | credencial + empresa |
POST /v1/tax/sync | 10 req/min | minuto deslizante | credencial + empresa |
POST /v1/documents/{document_id}/send | 20 req/min | minuto deslizante | credencial |
| Descarga de artefactos (PDF, XML, recibidos) | 60 req/min | minuto deslizante | credencial |
https://ver.facturia.cl/d/{slug} | 60 req/min | minuto deslizante | el enlace |
Lo que importa más que los números:
- Declaración y sincronización se cuentan aparte. Comparten el límite, no el contador: uno solo dejaría que una ráfaga de sincronizaciones se gastara la cuota que la declaración necesita el 20.
- El envío por correo tiene bucket propio porque recibe una lista de direcciones del cliente y adjunta un PDF — la forma exacta de un cañón de correo. 20/min es generoso para facturar e inservible para cualquier otra cosa.
- Los enlaces públicos se cuentan por slug, no por quien llama.
/d/{slug}es la única ruta sin credencial, así que el enlace es lo único estable que contar. No hay conteo por IP. - No hay ráfaga. Una versión anterior de esta tabla traía una columna "Ráfaga" que contradecía su propio texto; el limitador aplica las ventanas deslizantes de arriba y nada más. Dimensiona tus reintentos al límite, no a una ráfaga.
Estos son límites de partida, y se suben cuando lo pides: cuéntanos tu volumen y ajustamos. No necesitas cambiar de plan para tener más throughput. Los publicamos para que puedas dimensionar reintentos y backoff, no como una promesa competitiva.
El modo test aplica los mismos límites que live, así que una prueba de carga es representativa.
El arranque en frío tiene sus propios topes
POST /account/register y POST /account/login no llevan credencial, así que no se pueden contar
por credencial. Se cuentan por dos ejes a la vez, y los dos están publicados porque una flota de
agentes detrás de un mismo NAT llega al primero en su primera llamada y merece saber el número en
vez de descubrirlo:
| Bucket | Límite | Ventana | Se cuenta por |
|---|---|---|---|
POST /account/register · POST /account/login | 10 por minuto | 60 s | dirección IP, sobre todas las cuentas |
POST /account/register · POST /account/login | 5 por minuto | 60 s | dirección de correo, desde donde sea |
El tope por IP acota la CPU: cada una de esas puertas cuesta un bcrypt completo con costo 12, y la máquina es de una vCPU. El tope por correo es la mitad que una botnet no puede esquivar rotando direcciones. Las dos ventanas cuentan todos los intentos, también los exitosos.
Pasado cualquiera de los dos recibes un 429 con Retry-After en segundos y este cuerpo:
{
"status": 429,
"message": "Demasiados intentos. Vuelve a intentar en un momento.",
"errors": ["auth_throttled"]
}Ese es el sobre del arranque en frío ({status, message, data, errors}), no el de /v1 — son dos
superficies distintas y cada una responde el suyo. Programa contra errors[0] === 'auth_throttled',
nunca contra el message.
Trampa
El rechazo no te dice cuál de los dos topes tocaste
Y es a propósito: decir «demasiados intentos para este correo» convertiría el limitador en un oráculo de enumeración de cuentas — cualquiera podría averiguar qué direcciones tienen cuenta. No hay captcha, también a propósito: esta superficie es para agentes. Si una flota legítimamente necesita más de 10 registros por minuto desde una misma salida, háblanos antes de construir un loop de reintentos alrededor: la respuesta es un arreglo de plataforma.
Cómo se comporta un 429
Un 429 es del tipo rate_limit_error. La emisión está encolada de nuestro lado: una ráfaga sobre
10 req/s es un 429 del lado del cliente, nunca un documento perdido. Reintenta con el
Retry-After que te devolvimos y el documento sale.
async function emitir(payload, idempotencyKey) {
for (let intento = 0; intento < 5; intento += 1) {
const res = await fetch('https://api.facturia.cl/v1/documents', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FACTURIA_KEY}`,
'Facturia-Empresa': process.env.EMPRESA,
'Idempotency-Key': idempotencyKey, // el mismo en cada reintento
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (res.status !== 429) return res;
const esperar = Number(res.headers.get('Retry-After') ?? 1);
await new Promise((r) => setTimeout(r, esperar * 1000));
}
throw new Error('rate limit persistente');
}Reutilizar el mismo Idempotency-Key en cada reintento es lo que garantiza que cinco intentos
produzcan un documento y no cinco.
No hay una cuota de plan que se pueda agotar
Los límites de arriba son de tasa. El plan es uno solo, mensual, por empresa y con documentos ilimitados, así que no existe un saldo de documentos que se consuma ni un tramo que se pase: si algo te frena, es el ritmo, no el volumen. Conectar una segunda empresa tampoco te acerca a ninguna cuota: es una suscripción más, al mismo precio, con los mismos límites de tasa.
El diccionario publica un 402 plan_limit_reached y hoy nada lo emite. Sigue ahí porque un
código publicado no se retira sin doce meses de aviso, y porque un cliente que ya sabe manejar un
402 merece tener contra qué programar. Lo dice su propia ficha:
plan_limit_reached.
El otro 402, subscription_required, sí se aplica — pero no es una cuota: es una empresa sin
suscripción activa intentando uno de cinco actos reales. Está detallado en
El sobre de error.
Las dos realidades de arriba
Por encima de nuestros límites hay dos realidades del SII para las que conviene diseñar:
- El SII limita tasa y no publica sus cifras.
- El SII tiene ventanas de mantención programadas.
Ninguna de las dos te va a perder un documento — la emisión está encolada — pero un documento que ya
aceptamos va a quedarse en queued más tiempo durante una caída del SII. Lo vas a notar en el propio
documento antes que en ninguna otra parte: el status no avanza y Facturia-Poll-After te sigue
devolviendo un próximo intento en vez de desaparecer. La disponibilidad de nuestros componentes está
en status.facturia.cl, y en JSON en
status.facturia.cl/status.json; si algo lleva detenido más
de lo razonable, escríbenos con el request_id y te decimos en cuál de las dos mitades estás.
Construye sobre webhooks
Un integrador que hace polling apretado durante una caída del SII se auto-limita justo cuando menos
información hay. Un integrador que escucha document.accepted no nota la diferencia salvo por la
latencia. La página de Webhooks tiene el catálogo completo.