# Límites de tasa (/docs/autenticacion/limites)

## Los headers [#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 [#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 [#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. &#x2A;*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:

```json
{
  "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. &#x2A;*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` [#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.

```js
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 [#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`](/docs/errores/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](/docs/errores#el-402).

## Las dos realidades de arriba [#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](https://status.facturia.cl), y en JSON en
[`status.facturia.cl/status.json`](https://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](/docs/webhooks) tiene el catálogo completo.
