- Document size
- 10 MB
- Idempotency
- 24 h
Every organisation has a limit on how many requests it can send in a given time. The limit depends on your plan and applies separately to each group of endpoints. Every response tells you what your limit is and how much of it is left. Once you exceed it you get a 429 with the exact time after which you can try again.
What is counted
| Call | The limit is counted per |
|---|---|
Enterprise API (/api/v1/…) with an API key or a session | the whole organisation — all its API keys and users together |
SAPI-SK (/sapi/document…) | each SAPI client separately (X-Peppol-Participant-Id) |
| Unauthenticated calls (registration, the SAPI token endpoint) | IP address |
Endpoint groups
Expensive operations have their own counter and do not count towards the general limit — validating in a loop will not block you from sending invoices.
| Group | Endpoints |
|---|---|
api | everything in the Enterprise API that is not in another group (sending and reading invoices…) |
validate | POST /api/v1/documents/validate — pre-flight validation |
reachability | GET /api/v1/participants/:id/reachability — receiver reachability |
company-lookup | GET /api/v1/invoicing/company/:ico — buyer details by company ID (IČO) |
invoicing-channel | GET /api/v1/invoicing/channel — delivery channel before issuing an invoice |
sapi | SAPI-SK /sapi/document… |
Limits per plan
| Group | MINI | START | PRO | CORPORATE | ENTERPRISE |
|---|---|---|---|---|---|
api | 20 / s | 50 / s | 100 / s | 100 / s | 200 / s |
sapi (per client) | 10 / s | 25 / s | 50 / s | 50 / s | 100 / s |
validate | 10 / min | 30 / min | 60 / min | 150 / min | 300 / min |
reachability | 30 / min | 30 / min | 60 / min | 60 / min | 120 / min |
company-lookup | 30 / min | 30 / min | 30 / min | 30 / min | 30 / min |
invoicing-channel | 30 / min | 30 / min | 60 / min | 60 / min | 120 / min |
If a limit is too tight, get in touch — an individual one can be agreed. It shows up in the headers of your responses right away.
How the limit is consumed
The limit is not a window that resets all at once at the top of the minute. Capacity is
refilled continuously — with validate at 60 per minute, one call frees up every second.
On top of that you have a burst: several calls you can send at once without waiting. On the PRO plan that is 10 validations at once, then one per second on average. An unused burst refills while you are idle.
- evenly spread calls within the limit never hit it,
- a short spike is absorbed by the burst,
- sustained load above the limit ends in a 429.
Headers
RateLimit-Policy: "validate";q=60;w=60
RateLimit: "validate";r=9;t=1
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 1790668801
| Header | Meaning |
|---|---|
RateLimit-Policy | your limit: the group, quota q and window w in seconds — here 60 requests per 60 s |
RateLimit | the current state: r calls that would pass right now, t seconds until the burst is full again |
X-RateLimit-Limit | same as q |
X-RateLimit-Remaining | same as r |
X-RateLimit-Reset | when the burst is full again, as a Unix timestamp in seconds |
RateLimit and RateLimit-Policy follow the IETF draft
RateLimit header fields for HTTP.
X-RateLimit-* is sent for older clients — the content is the same, reading one pair is enough.
The headers are on every response to a request that passed authentication and input checks — successful or not. They are missing from a response with which authentication rejects the request (401, 403 for a suspended account) and from 400/413 raised by the request shape check: those are answered before the request is counted, and they do not count towards the limit.
Exceeding the limit
HTTP/1.1 429 Too Many Requests
Retry-After: 1
RateLimit-Policy: "validate";q=60;w=60
RateLimit: "validate";r=0;t=10
{
"statusCode": 429,
"error": "RATE_LIMIT_EXCEEDED",
"code": "ERR-429",
"message": "Too many requests. Retry after 1 seconds.",
"retry_after": 1
}
Retry-After is the exact number of seconds until the next call frees up, not an
estimate. The retry_after field carries the same value. Do not confuse it with t in the
RateLimit header — that is the time until the whole burst is back (10 validations on the
PRO plan in this example). On SAPI-SK the 429 uses the
SAPI-SK envelope with code SAPI-TEMP-002 and retryable: true.
A rejected call is not processed — no invoice is sent and no credit is used. Retrying it is safe.
Recommended client behaviour
- On a 429, wait
Retry-Afterseconds and retry. Do not retry immediately or in a tight loop. - Watch
RateLimitduring batch processing — slow down asrapproaches zero, before a 429 arrives. - Parallel processes share one organisation limit. Split the capacity between them or send through a single queue.
- Derive your pace from
RateLimit-Policy—q / wis the average calls per second. - Do not use pre-flight validation for bulk checks. A sent invoice is validated anyway; pre-flight validation is for checking before you send.
async function callWithLimit(url, options, attempts = 5) {
for (let i = 0; i < attempts; i++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const wait = Number(res.headers.get("Retry-After") ?? 1);
await new Promise((r) => setTimeout(r, wait * 1000));
}
throw new Error("Rate limit still exceeded after retries");
}