Factuarea APIDevelopers

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:

  1. API key — per-minute sliding window and monthly volume.
  2. Company — the aggregate of every key of the company.
  3. 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

TierKey/minKey/monthCompany/minCompany/monthIncluded with
Free1010010100The 10-day trial.
Starter305,0009015,000The Esencial plan.
Pro30050,000900150,000The Profesional and Avanzado plans.
ScaleAs per contractAs per contractAs per contractAs per contractThe 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:

TierCommon band/minReserved floor per tax ID/minPortfolio/monthActive keys in the account
Free10101001
Starter18020150,00015
Pro1,800301,500,000500
ScaleAs per contractAs per contractAs per contractAs 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:

HeaderMeaning
X-RateLimit-LimitPer-minute limit of your tier.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUNIX timestamp when a slot frees up (one slot exits the window).
Retry-AfterOnly 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: 1747314060

And 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: 1747314007

Error 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:

AxisPer minutePer month
API keyrate_limit_exceededmonthly_quota_exceeded
Companycompany_rate_limit_exceededcompany_monthly_quota_exceeded
Account portfolioportfolio_rate_limit_exceededportfolio_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-*:

WorkloadStable codeWhat is measured
Email sendsemail_recipient_budget_exceededDistinct recipients in the window, across credentials.
Email deliveryemail_delivery_circuit_openA temporary provider/company safety cut after delivery failures; it is not a quota.
New accountsaccount_probation_limit_exceededA reduced age-based allowance until the account matures, verifies its AEAT census or activates a paid subscription.
PDF endpointspdf_generation_budget_exceededHourly render work, by company or individual document (subcode).
Imports and uploadsupload_in_flight_budget_exceededTemporary bytes crossing disk concurrently, not occupied storage.
Packaged exportsexport_budget_exceededHourly 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:

  1. New quotas apply immediately.
  2. The monthly quota consumed at the previous tier does not reset: only the monthly cap grows.
  3. 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.

On this page

Need a hand?Contact support