Factuarea APIDevelopers

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:

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

TierKey/minKey/monthCompany/minCompany/monthIncluded with
Free1010010100The 10-day trial.
Starter305,0009015,000The Emprendedor plan.
Pro30050,000900150,000The Empresario plan.
Scale1,2005,000,0003,60015,000,000The 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:

TierCommon band/minReserved floor per active company/minPortfolio/monthActive keys in portfolio
Free10101001
Starter18020150,00015
Pro1,800301,500,000500
Scale7,2006050,000,0002,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:

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. 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: 1747314060

And on a 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_exceededrate_limit_exceeded
Companycompany_rate_limit_exceededcompany_monthly_quota_exceeded
Managed portfolioportfolio_rate_limit_exceededportfolio_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-*:

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

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

On this page

Need a hand?Contact support