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: 41They 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:
| 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
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. 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:
- The SII rate-limits, and does not publish its numbers.
- 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.
Root keys and rotation
Mint one key per customer empresa without a human opening a dashboard, rotate secrets with no downtime, and the cold start of a brand new account.
Create documents
The minimal issue call, the full issue call, what we fill in for you, and how to read the documents you have already issued.