Facturiadocs
Authentication

Rate limits

The published buckets, the headers that tell you where you stand, and the two SII realities worth designing for anyway.

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

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

BucketLimitWindowCounted by
General API300 req/minsliding minutecredential
POST /v1/documents10 req/ssliding secondcredential
POST /v1/tax/f29/filings10 req/minsliding minutecredential + empresa
POST /v1/tax/sync10 req/minsliding minutecredential + empresa
POST /v1/documents/{document_id}/send20 req/minsliding minutecredential
Artifact downloads (PDF, XML, received)60 req/minsliding minutecredential
https://ver.facturia.cl/d/{slug}60 req/minsliding minutethe 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

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:

BucketLimitWindowCounted by
POST /account/register · POST /account/login10 per minute60 sIP address, across every account
POST /account/register · POST /account/login5 per minute60 semail 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. Both windows count every attempt, the successful ones included.

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

{
  "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. 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

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.

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

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.

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.

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, and in JSON at 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 page has the full catalogue.

On this page