Quotas are per API key and per plan. They are counted on the key rather than on your IP address, so an integration behind a shared address is not throttled by a neighbour, and one spread across many addresses does not escape its quota.
The quotas
| Plan | Requests per minute | Requests per hour |
|---|---|---|
| Groei | 60 | 600 |
| Pro | 120 | 2,000 |
| Zakelijk | 300 | 10,000 |
Mutations carry a further ceiling of 30 per minute on top of the quota above, whatever the plan. Writes hit the overlap constraint, send email and enqueue background work, so they cost more than a read. It is not tiered, because writing already requires Pro.
Both the per-minute and the hourly limit apply. The hourly ceiling exists so that a bug cannot sustain the per-minute rate for a day.
An unrecognised plan gets the Groei quota rather than no quota, so a business on a custom plan fails safe rather than uncapped.
Headers
Every response carries the state of the most constrained limiter that was consulted, whether or not you were throttled:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The ceiling for that window |
X-RateLimit-Remaining | Requests left in it, 0 when blocked |
X-RateLimit-Reset | Epoch seconds at which capacity frees up, as GitHub and Stripe send it |
A 429 adds Retry-After in seconds.
Backing off
async function request(url, init, attempt = 0) {
const response = await fetch(url, init);
if (response.status !== 429 || attempt >= 5) return response;
const retryAfter = Number(response.headers.get('Retry-After') ?? 1);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return request(url, init, attempt + 1);
}
Honour Retry-After rather than retrying immediately or on a fixed interval. A client that
retries a 429 straight away spends its whole next window being refused, and from the outside it
looks exactly like an attack.
Better still, do not get throttled. Watch X-RateLimit-Remaining on the responses you are
already reading and slow down before it reaches zero. It costs nothing: the header is on every
response.
Designing inside the quota
Do not poll for changes. Polling GET /api/v1/appointments every few seconds is the single
most common way to exhaust an hourly quota, and it is also the slowest way to hear about a
change. Webhooks deliver the event as it happens and cost you no quota at all: registering an
endpoint is a Pro feature and is documented separately. If you are on Groei and cannot use
webhooks, poll with updatedSince and a
generous interval rather than re-reading the whole list.
Page with limit=100. The default is 50. Walking a long list at the maximum halves your
request count for the same data. See pagination.
Cache what does not move. Services, staff and categories change a few times a year. Availability changes constantly and must not be cached.
Do not call GET /api/v1/me per request. Once at startup is enough.
The other limits
Two limits exist that are not about your throughput.
Failed authentication is limited by IP, because a request that never authenticated has no key
to count against. Without it, someone could probe key prefixes as fast as our servers scale. A
correctly configured client never reaches it; a client looping on a 401 will, which is a
reason to stop retrying a 401 rather than a reason to worry about the limit.
Webhook replays and test pings are limited per business, not per key. They are the same buttons the business owner has in the product, so both paths draw on one bucket. See the webhooks documentation for those numbers.