Rate limits
Per-key, company and account-portfolio quotas by tier. X-RateLimit-* headers and recommended back-off.
The public API evaluates three quota axes to guarantee fairness between tenants and protect the backend from bursts:
- API key — per-minute sliding window and monthly volume.
- Company — the aggregate of every key of the company.
- Account portfolio — the account the credential belongs to, only when that account holds two or more real tax IDs.
The limits depend on your API key tier. The tier is derived from the plan
of your account — it is never set per key or per request, there is no separate
capacity purchase, and it updates automatically when the plan changes. Only
historical grandfathered_addon migrations can preserve a higher tier, and only
while an eligible plan remains active.
Tiers
| Tier | Key/min | Key/month | Company/min | Company/month | Included with |
|---|---|---|---|---|---|
| Free | 10 | 100 | 10 | 100 | The 10-day trial. |
| Starter | 30 | 5,000 | 90 | 15,000 | The Esencial plan. |
| Pro | 300 | 50,000 | 900 | 150,000 | The Profesional and Avanzado plans. |
| Scale | As per contract | As per contract | As per contract | As per contract | The Enterprise plan. |
The key limits are enforced. The aggregate company limits are present in the
contract and currently run in observation (shadow) mode in the incoming
application build: they measure would-be rejections without returning a 429.
Do not treat observation mode as purchased capacity; it can be promoted to
enforcement after calibration without changing the request or response shape.
fact_test_ keys still consume their per-key quota, but company and portfolio
aggregate axes bypass sandbox companies so test traffic does not consume live
aggregate capacity.
Sliding window
The per-minute bucket is not a fixed window "60 seconds since 12:00".
It's a sliding window: at any point, the API counts how many accepted
requests there are in the last 60 seconds for your key. When the
counter equals the limit, subsequent requests respond 429 until
enough time has passed for the early requests to "drop off" the window.
Why: there's no "grace minute" every 60 seconds where you could send twice the limit. Fairer and more stable under real traffic.
Company and portfolio ceilings
When the aggregate axes are enforced, the API evaluates the key first, and the
per-minute ceilings before the monthly ones. A company rejection uses
company_rate_limit_exceeded or company_monthly_quota_exceeded: adding keys
does not raise those ceilings because every key is counted together.
The account portfolio
The third axis belongs to the account of the credential, not to the tax ID that a request names. A credential operates a portfolio when its own account holds at least two real tax IDs. The sandbox of the account does not count, so a titular NIF and its sandbox are not a portfolio, and archived tax IDs do not count either. With a single real tax ID this axis never applies: requests follow the key and company ceilings above and nothing else.
Two rules decide where a request counts:
- A tax ID reached through a relation counts in your portfolio. A request on a tax ID that another account puts within your reach, through a granted access or a managed account, adds to the portfolio of the account of your credential and to the reserved floor of the tax ID it names. It never adds to the portfolio of the account that owns that tax ID.
- A tax ID that leaves the account changes ceiling. When a tax ID moves to another account by a tax ID transfer, requests on it stop counting in the portfolio of the account it left and count in the portfolio of its new account, through that account's own credentials. Whether an account operates a portfolio is remembered for about a minute, so a change in how many tax IDs an account holds reaches the ceilings within that minute.
A portfolio has a separate flat catalog per tier:
| Tier | Common band/min | Reserved floor per tax ID/min | Portfolio/month | Active keys in the account |
|---|---|---|---|---|
| Free | 10 | 10 | 100 | 1 |
| Starter | 180 | 20 | 150,000 | 15 |
| Pro | 1,800 | 30 | 1,500,000 | 500 |
| Scale | As per contract | As per contract | As per contract | As per contract |
The reserved floor of a tax ID is evaluated first. A tax ID below its floor can still proceed when the common band is exhausted; the common band is therefore not an absolute portfolio total. The monthly quota has no floor and aggregates the whole portfolio. None of the four values depends on how many tax IDs the account holds.
The per-minute and monthly portfolio ceilings are not in force yet: they
currently run in observation (shadow) mode pending calibration from real
closing-period traffic. They measure the rejections they would have produced
without returning a 429, and while they only observe, the per-key ceiling keeps
applying as before.
The key cap works differently: it is checked when a key is created, not on each request, and it is already in force. It applies to a portfolio, since an account with a single tax ID only meets the per-company cap, and it comes on top of that cap, which is checked first. It counts every credential of the account that has not been revoked, test keys included. An expired key that nobody revoked still takes its place, so revoke the keys you no longer use. An account already over the cap keeps all its keys: only new ones are refused.
Portfolio rejections use portfolio_rate_limit_exceeded and
portfolio_monthly_quota_exceeded, and a key created over the cap answers 422
portfolio_max_api_keys_exceeded.
Adding tax IDs or API keys does not increase a flat portfolio ceiling; spread the
workload over time or request an audited company-specific increase.
Response headers
Every response (including 429) includes:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Per-minute limit of your tier. |
X-RateLimit-Remaining | Requests remaining in the current window. |
X-RateLimit-Reset | UNIX timestamp when a slot frees up (one slot exits the window). |
Retry-After | Only on 429, and never on the three monthly quota errors. Seconds until you can retry. A monthly counter renews at the start of the next calendar month. |
Example headers on a 200 response:
HTTP/1.1 200 OK
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1747314060And on a per-minute 429:
HTTP/1.1 429 Too Many Requests
Retry-After: 7
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1747314007Error code
{
"error": {
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"message": "Has superado el límite de peticiones. Vuelve a intentarlo en unos segundos.",
"request_id": "req_..."
}
}The stable code identifies which enforced axis was exhausted:
| Axis | Per minute | Per month |
|---|---|---|
| API key | rate_limit_exceeded | monthly_quota_exceeded |
| Company | company_rate_limit_exceeded | company_monthly_quota_exceeded |
| Account portfolio | portfolio_rate_limit_exceeded | portfolio_monthly_quota_exceeded |
All use type: rate_limit_error. Respect Retry-After on the per-minute ones.
The monthly ones carry no Retry-After: the counter renews at the start of the
next calendar month, so waiting a few seconds will not clear them.
Repeated authentication failures are throttled separately with
code: too_many_auth_failures.
Best practices
1. Respect Retry-After
import time, requests
def call_with_retry(url, **kwargs):
while True:
resp = requests.get(url, **kwargs)
if resp.status_code != 429:
return resp
sleep = int(resp.headers.get('Retry-After', 1))
time.sleep(sleep)The three monthly quota errors carry no Retry-After. A monthly 429 will not
clear by waiting a few seconds: stop retrying it and branch on error.code
instead.
2. Exponential back-off with jitter
For 5xx, where there's no Retry-After:
import random, time
def backoff(attempt):
return min(60, (2 ** attempt) * 0.1 + random.uniform(0, 0.5))
for attempt in range(5):
resp = requests.get(url)
if resp.status_code < 500:
break
time.sleep(backoff(attempt))3. Monitor X-RateLimit-Remaining
If your integration consistently approaches 10% of the limit, consider:
- Moving to a plan with a higher tier.
- Batching: instead of N POSTs, aggregate and do 1 POST.
- Caching frequent reads (products, taxes, series).
- Subscribing to webhooks instead of polling.
4. Webhooks > polling
If you poll /v1/companies/{company}/invoices?status=paid every minute to detect payments
you consume 30 rpm just for that. Subscribe to the invoice.paid event
and drop that to 0 requests.
5. Per-integration keys
If you have two integrations (an internal dashboard + an export cron), create two distinct keys: each key has its own per-minute and monthly buckets, so a heavy cron does not consume the dashboard's key bucket.
Separating keys does not increase aggregate capacity. The company and portfolio ceilings add the relevant keys together; use more keys for isolation and attribution, not as a quota bypass.
Administrative quotas
Some workloads have protection budgets independent of request rate. They do
not consume the key/company/portfolio buckets and do not appear in
X-RateLimit-*:
| Workload | Stable code | What is measured |
|---|---|---|
| Email sends | email_recipient_budget_exceeded | Distinct recipients in the window, across credentials. |
| Email delivery | email_delivery_circuit_open | A temporary provider/company safety cut after delivery failures; it is not a quota. |
| New accounts | account_probation_limit_exceeded | A reduced age-based allowance until the account matures, verifies its AEAT census or activates a paid subscription. |
| PDF endpoints | pdf_generation_budget_exceeded | Hourly render work, by company or individual document (subcode). |
| Imports and uploads | upload_in_flight_budget_exceeded | Temporary bytes crossing disk concurrently, not occupied storage. |
| Packaged exports | export_budget_exceeded | Hourly packaged documents or generated artifacts (subcode). |
These codes respond 429 with type: rate_limit_error; use Retry-After or
error.details.retry_at when present. Switching credentials does not bypass a
company workload budget. A separate 402 storage_quota_exceeded means the
account has reached its maximum usage; waiting does not resolve it: contact support.
Webhook endpoint-count caps are plan capacity, not a 429 budget; see
Pricing.
Tier upgrade
Changing tier does not require rotating keys. When your plan changes:
- New quotas apply immediately.
- The monthly quota consumed at the previous tier does not reset: only the monthly cap grows.
- Existing keys keep their
id; the new tier applies to all of them automatically.
Identity for document sending
Document sending may require sender identity proved by an active subscription or accepted census identification. A tax ID that is not the titular one takes it from the titular tax ID of its account, and the tax IDs of a managed account take it from the titular tax ID of the provider's account for as long as the management lasts. 422 sender_identity_not_verified requires correcting that identity; a 429 concerns a separate sending budget. See sending documents and the sending identity of a tax ID.