# Rate limits (/en/docs/autenticacion/limites)

## The headers [#the-headers]

Every **authenticated** response carries:

```
RateLimit-Limit: 300
RateLimit-Remaining: 287
RateLimit-Reset: 41
```

They report the **tightest bucket that applies**, not the general one: a request that is one
emission away from exhausting its per-second quota says so, even with 280 general requests left.

On a `429`, the `Retry-After` header (in seconds) tells you exactly how long to wait. Do not guess
with exponential backoff when the response carries the number.

Unauthenticated endpoints are not counted and do not carry these headers: `GET /v1/error_codes`
serves a constant that holds nobody's data.

## The published buckets [#the-published-buckets]

By default, **per credential** — a key or an account session, each mode counted separately:

| Bucket                                  | Limit       | Window         | Counted by           |
| --------------------------------------- | ----------- | -------------- | -------------------- |
| General API                             | 300 req/min | sliding minute | credential           |
| `POST /v1/documents`                    | 10 req/s    | sliding second | credential           |
| `POST /v1/tax/f29/filings`              | 10 req/min  | sliding minute | credential + empresa |
| `POST /v1/tax/sync`                     | 10 req/min  | sliding minute | credential + empresa |
| `POST /v1/documents/{document_id}/send` | 20 req/min  | sliding minute | credential           |
| Artifact downloads (PDF, XML, received) | 60 req/min  | sliding minute | credential           |
| `https://ver.facturia.cl/d/{slug}`      | 60 req/min  | sliding minute | **the link**         |

What matters more than the numbers:

* **Filing and syncing are counted apart.** They share the limit, not the counter: a single one
  would let a burst of syncs spend the quota the filing needs on the 20th.
* **Sending by email has its own bucket** because it takes a list of addresses from the caller and
  attaches a PDF — the exact shape of a mail cannon. 20/min is generous for invoicing and useless
  for anything else.
* **Public links are counted per slug, not per caller.** `/d/{slug}` is the only route without a
  credential, so the link is the only stable thing to count. There is no per-IP counting.
* **There is no burst allowance.** An earlier version of this table carried a "Burst" column that
  contradicted its own text; the limiter enforces the sliding windows above and nothing else. Size
  your retries to the limit, not to a burst.

These are **starting limits**, and they go up when you ask: tell us your volume and we adjust. You
do not need to change plan to get more throughput. We publish them so you can size retries and
backoff, not as a competitive promise.

Test mode enforces the same limits as live, so a load test is representative.

## The cold start has caps of its own [#the-cold-start-has-caps-of-its-own]

`POST /account/register` and `POST /account/login` carry no credential, so they cannot be counted
per credential. They are counted on two axes at once, and both are published because a fleet of
agents behind one NAT hits the first on its very first call and deserves to know the number instead
of discovering it:

| Bucket                                           | Limit             | Window | Counted by                       |
| ------------------------------------------------ | ----------------- | ------ | -------------------------------- |
| `POST /account/register` · `POST /account/login` | **10 per minute** | 60 s   | IP address, across every account |
| `POST /account/register` · `POST /account/login` | **5 per minute**  | 60 s   | email address, from wherever     |

The per-IP cap bounds CPU: each of those doors costs a full bcrypt at cost 12, and the machine has
one vCPU. The per-email cap is the half a botnet cannot dodge by rotating addresses. &#x2A;*Both windows
count every attempt, the successful ones included.**

Past either one you get a `429` with `Retry-After` in seconds and this body:

```json
{
  "status": 429,
  "message": "Demasiados intentos. Vuelve a intentar en un momento.",
  "errors": ["auth_throttled"]
}
```

That is the cold start's envelope (`{status, message, data, errors}`), not `/v1`'s — they are two
different surfaces and each answers with its own. &#x2A;*Program against
`errors[0] === 'auth_throttled'`, never against the `message`.**

> **Trap — The rejection does not tell you which of the two caps you hit**
>
> And that is on purpose: saying "too many attempts for this email" would turn the limiter into an
> account-enumeration oracle — anyone could find out which addresses have an account. There is no
> captcha, also on purpose: this surface is for agents. If a fleet legitimately needs more than 10
> registrations per minute out of one egress, talk to us before building a retry loop around it: the
> answer is a platform fix.

## How a `429` behaves [#how-a-429-behaves]

A `429` is of type `rate_limit_error`. Emission is queued on our side: a burst above 10 req/s is a
client-side `429`, **never a lost document**. Retry with the `Retry-After` we handed back and the
document goes out.

```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');
}
```

Reusing the same `Idempotency-Key` on every retry is what guarantees that five attempts produce one
document and not five.

## There is no plan quota to run out of [#there-is-no-plan-quota-to-run-out-of]

The limits above are **rate** limits. There is a single plan, monthly, **per empresa**, with
**unlimited documents**, so there is no document balance to burn down and no tier to cross: if
something is slowing you, it is the pace, not the volume. Connecting a second empresa does not move
you toward a quota either: it is one more subscription, at the same price, with the same rate
limits.

The dictionary publishes a `402 plan_limit_reached` and today **nothing emits it**. It stays because
a published code is not withdrawn without twelve months' notice, and because a client that already
knows how to handle a `402` deserves something to program against. Its own entry says so:
[`plan_limit_reached`](/en/docs/errores/plan_limit_reached).

The other `402`, `subscription_required`, **is enforced** — but it is not a quota: it is an empresa
with no active subscription attempting one of five real acts. It is spelled out in
[The error envelope](/en/docs/errores#el-402).

## The two realities upstream [#the-two-realities-upstream]

Above our own limits there are two realities of the SII (Chile's tax authority) worth designing for:

1. **The SII rate-limits, and does not publish its numbers.**
2. **The SII has scheduled maintenance windows.**

Neither one will lose you a document — emission is queued — but a document we already accepted will
sit in `queued` longer during an SII outage. You will see it on the document itself before anywhere
else: `status` does not advance and `Facturia-Poll-After` keeps handing back a next attempt instead
of disappearing. The availability of our own components is at
[status.facturia.cl](https://status.facturia.cl), and in JSON at
[`status.facturia.cl/status.json`](https://status.facturia.cl/status.json); if something has been
stuck longer than is reasonable, write to us with the `request_id` and we will tell you which of the
two halves you are in.

> **Build on webhooks**
>
> An integrator polling tightly through an SII outage rate-limits itself exactly when there is least
> information to be had. An integrator listening for `document.accepted` notices no difference beyond
> the latency. The [Webhooks](/en/docs/webhooks) page has the full catalogue.
