Rate limits
An undocumented limit is indistinguishable from an outage, so here are the numbers.
| Surface | Limit | Counted per |
|---|---|---|
POST /oauth2/token | 20 per minute | credential, and separately per calling address |
Everything under /v1 | 600 per minute | credential |
The /v1 limit is per credential, not per IP address. Two partners sharing
a cloud egress address do not throttle each other, and running your integration
from a dozen containers behind one NAT gateway does not count as one caller.
There is also a coarser per-address ceiling underneath, sized well above the
per-credential limit so that a single partner spending its full 600 never
reaches it. If you are seeing 429 while RateLimit-Remaining looks healthy,
you are sharing an address with a lot of traffic — tell us and we will look.
Reading your budget
Every /v1 response carries three headers:
RateLimit-Limit: 600
RateLimit-Remaining: 587
RateLimit-Reset: 42
RateLimit-Limit— requests per minute for your credential.RateLimit-Remaining— how many you have left right now.RateLimit-Reset— seconds until your budget is back to full.
The bucket refills continuously rather than resetting on a fixed boundary, so
you regain roughly ten requests a second as you go. You do not have to wait for
Reset to hit zero before the next request succeeds — that value is the wait
for the whole budget, not for the next single request.
Limits are enforced per API instance, and we run more than one. In practice this
means your effective ceiling is somewhat higher than the published number,
and RateLimit-Remaining can move non-monotonically as your requests land on
different instances. Treat it as a live signal to back off on, not as an exact
ledger.
When you exceed it
429 Too Many Requests, with a Retry-After in seconds and the same
problem-details body as every other error:
HTTP/1.1 429 Too Many Requests
Retry-After: 3
RateLimit-Limit: 600
RateLimit-Remaining: 0
RateLimit-Reset: 58
Content-Type: application/problem+json
{
"type": "https://api.valueverde.de/errors/rate-limited",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded for this API client. Retry after the period given in Retry-After, and see https://docs.valueverde.de/docs/rate-limits.",
"instance": "/v1/cooperatives",
"code": "RATE_LIMITED",
"trace_id": "9f2c1b7e-4d3a-4f18-93ac-2b6e5d0c8a71"
}
Sleep for Retry-After and try again. A refused request costs you nothing from
the bucket, so retrying does not push your own recovery further away — but
retrying without sleeping will not succeed any sooner either.
Quote trace_id if you contact support about a specific call.
Staying under it
Cache your access token. This is the single most common cause of a 429 on
the token endpoint. A token is valid for 15 minutes; mint one, hold it, and
re-mint when it expires. An integration that fetches a token before every call
will exhaust twenty a minute almost immediately — and it is doing an RSA
signature per call for no reason.
Use conditional requests on the catalogue. GET /v1/cooperatives and
GET /v1/cooperatives/{id} return ETag, and a 304 Not Modified still costs
a request but nothing else. See Pagination & caching.
Back off on the header, not on the failure. If you are checking
RateLimit-Remaining and slowing down as it approaches zero, you will rarely
see a 429 at all.
Ask before you engineer around it. If a bulk reconciliation genuinely needs more than 600 a minute, that is a reasonable thing to want — email partners@valueverde.de rather than sharding across credentials, which we would read as abuse.
Other surfaces
The public unauthenticated catalogue (/public/**) and the browser
authentication endpoints have their own, tighter, per-address limits. Neither is
part of the partner contract; if you are calling them from an integration, you
are on the wrong endpoint.