Facturiadocs
Autenticación

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: 41

Reportan 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:

BucketLímiteVentanaSe cuenta por
API general300 req/minminuto deslizantecredencial
POST /v1/documents10 req/ssegundo deslizantecredencial
POST /v1/tax/f29/filings10 req/minminuto deslizantecredencial + empresa
POST /v1/tax/sync10 req/minminuto deslizantecredencial + empresa
POST /v1/documents/{document_id}/send20 req/minminuto deslizantecredencial
Descarga de artefactos (PDF, XML, recibidos)60 req/minminuto deslizantecredencial
https://ver.facturia.cl/d/{slug}60 req/minminuto deslizanteel 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:

BucketLímiteVentanaSe cuenta por
POST /account/register · POST /account/login10 por minuto60 sdirección IP, sobre todas las cuentas
POST /account/register · POST /account/login5 por minuto60 sdirecció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:

  1. El SII limita tasa y no publica sus cifras.
  2. 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.

En esta página