Rate limits
Per-key, company and managed-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.
- Managed portfolio — the managing company and all its active child companies, only when the credential belongs to a portfolio.
The limits depend on your API key tier. The tier is derived from your
company's Factuarea plan — 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 the company still has an active eligible plan.
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 Emprendedor plan. |
| Pro | 300 | 50,000 | 900 | 150,000 | The Empresario plan. |
| Scale | 1,200 | 5,000,000 | 3,600 | 15,000,000 | 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 before the
company, and the company before the managed portfolio. 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.
A managed portfolio has a separate flat catalog per tier:
| Tier | Common band/min | Reserved floor per active company/min | Portfolio/month | Active keys in portfolio |
|---|---|---|---|---|
| Free | 10 | 10 | 100 | 1 |
| Starter | 180 | 20 | 150,000 | 15 |
| Pro | 1,800 | 30 | 1,500,000 | 500 |
| Scale | 7,200 | 60 | 50,000,000 | 2,000 |
The reserved per-company floor is evaluated first. A child 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. These values are also currently in shadow mode pending
calibration from real closing-period traffic.
Portfolio rejections use portfolio_rate_limit_exceeded and
portfolio_monthly_quota_exceeded. Creating more child companies 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. Seconds until you can retry. |
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 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 | rate_limit_exceeded |
| Company | company_rate_limit_exceeded | company_monthly_quota_exceeded |
| Managed portfolio | portfolio_rate_limit_exceeded | portfolio_monthly_quota_exceeded |
All use type: rate_limit_error. Respect Retry-After when present; portfolio
errors also expose the retry instant/seconds in error.details.
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)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/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
company's persistent aggregate storage is full and does not clear by waiting.
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. Managed companies use the accounting firm’s identity. 422 sender_identity_not_verified requires correcting that identity; a 429 concerns a separate sending budget. See sending documents.