Rate limits and quotas
Three different things can refuse your request for reasons of volume, and they behave nothing alike.
A rate limit is about requests per minute. It answers 429, it is typed
rate_limited, and it clears within seconds of you slowing down.
A daily booking ceiling and an account volume ceiling are both about
bookings actually written, not requests made. They also answer 429, typed
daily_booking_limit and booking_volume_refused, but neither clears the way
a rate limit does. One waits for the workspace’s local midnight, the other
for the account’s billing period to roll over. Both are covered on this page,
below the rate-limit budgets.
A plan quota is a different thing again: it is about what the account
bought: active calendars and workspaces. It answers 402, and it clears when
the plan changes or the account is back under the cap.
Quotas are covered in the errors guide; the rest of
this page is the three 429s.
It’s a token bucket, and it refills continuously
Section titled “It’s a token bucket, and it refills continuously”This is the fact to design against, and it is the one most likely to be assumed wrong if you have built against a fixed-window limiter before.
Each (endpoint, caller) pair has its own bucket that starts full and refills continuously as time passes. You get a little more capacity every moment, not a lump sum credited at the top of a calendar minute. There is no minute boundary anywhere in this design, and nothing resets “at :00” for everybody at once.
Two consequences fall out of that:
Waiting even a few seconds gives you back some budget, not none. If you are refused, the bucket is refilling the whole time you wait, and there is no boundary to wait for.
The response tells you exactly how long to wait. A refusal that can clear
carries Retry-After, in seconds, computed from how full the bucket actually
is right now. Read it rather than guessing. See below.
A refused request costs you nothing
Section titled “A refused request costs you nothing”A request that is turned away with a 429 from the rate limiter does not
consume a token. Only a request that is actually admitted spends budget.
This is deliberate, and it is the opposite of a design where a refusal is
itself counted: retrying here does not dig you a deeper hole. It still is not
free in the sense that matters, because a tight retry loop keeps asking a
bucket that has not refilled yet. So read Retry-After rather than hammering
the endpoint, but a burst of refusals never leaves you worse off than a burst of
silence would have.
The budgets
Section titled “The budgets”Rate limits key on two axes, and both can apply to the same request:
- The workspace: the whole booking page’s shared capacity, regardless of who is calling.
- The caller address: one visitor’s own share of that capacity, so no
single caller can consume a whole workspace’s budget alone. IPv6 addresses
are truncated to their
/64for this, since that is commonly what one visitor is delegated and the low bits rotate; IPv4 addresses are kept whole.
| Endpoint | Per workspace | Per caller address |
|---|---|---|
GET /v1/availability |
300/min | 60/min |
GET /v1/quote |
300/min | 60/min |
POST /v1/bookings |
60/min | 10/min |
POST /v1/bookings/{id}/verify |
60/min | 10/min |
GET /v1/quote has its own bucket, so a customer choosing extras does not
spend the availability budget.
Both axes are checked, and either can be the one that refuses a given request. A workspace whose whole page is busy can refuse you even though you personally are well under 10/min, and vice versa.
The keyless customer routes under /v1/public get 30 requests per minute
per caller address, shared across all of them. Those routes are the bootstrap
call, and the manage link’s cancel and reschedule. They carry no key, so an address is the
only subject available.
A secret key is rate-limited too, just on a different axis: 3000 requests a minute, shared by every secret key the workspace holds (minting a second key does not grant fresh capacity). This is an abuse backstop against a leaked key or a runaway script, not metering. No legitimate integration ever approaches it.
An account key gets 600 requests a minute, shared by the account’s
total request rate against the four /v1/account routes, across every
account key and every workspace the account holds, for the same reason as the
secret-key ceiling above.
The headers a 429 carries
Section titled “The headers a 429 carries”A 429 from the rate limiter is a JSON body plus two headers:
{ "error": { "type": "rate_limited", "message": "Too many requests." }}Retry-After is seconds until the bucket has refilled enough to admit one
more request. RateLimit-Remaining is requests left in the bucket, always
0 on a response that carries it, since a refusal only happens once it
reaches zero.
Both are on Access-Control-Expose-Headers, so a browser-based integration
running on a publishable key can read them from JavaScript the same way it
reads type from the body. Neither header is required to act correctly,
because type alone is enough. But they save you from guessing.
daily_booking_limit also carries Retry-After (seconds until the
workspace’s local midnight), but never RateLimit-Remaining. See below.
booking_volume_refused carries neither. Do not assume every 429 from this
API has both headers; check which type you got.
The daily booking ceiling
Section titled “The daily booking ceiling”daily_booking_limit is not the rate limiter. It is a durable check on
bookings actually written, not requests made, and it survives a restart.
The token buckets above do not.
It applies only to POST /v1/bookings called with a publishable key. A
secret key is a workspace’s own server, every one of its calls arrives from
one address, and fifteen a day would cap the business’s own integration
rather than stop abuse.
One caller address is allowed 15 bookings at one workspace per workspace
local day. Once it has written its 15th, POST /v1/bookings refuses the
16th with a 429 typed daily_booking_limit until the workspace’s local
midnight, no matter how much rate-limit budget that caller still has. A
caller can meet both ceilings in the same request. When that happens, the
per-minute limiter is checked first and wins, so the response is
rate_limited, since that one clears again within seconds and
daily_booking_limit does not:
{ "error": { "type": "daily_booking_limit", "message": "This device has made too many bookings with this business today. Please try again tomorrow, or contact the business directly." }}Retry-After is set to the real time remaining until the workspace’s local
midnight, not a flat 24 hours, so it stays correct on the two days a year the
workspace’s timezone shifts. There is no RateLimit-Remaining, no count and
no limit in the body: this is an abuse backstop, not an allowance to design a
progress bar against.
It is keyed on (workspace, caller address), not the account and not the booking’s customer email. A shared address (an office, a kiosk) hits its own ceiling independent of who it is booking for, and a customer switching networks gets a fresh one. It does not mean the business is full: every other caller, and this same one tomorrow, can still book.
The account volume ceiling
Section titled “The account volume ceiling”booking_volume_refused is different again, and it is the one case on this
page where the advice is not “wait and retry.”
It is a platform safety limit on an account’s total bookings in its current billing period, across every workspace and every caller the account has, not one device at one workspace. No plan raises it and no plan is exempt from it; it is not something an upgrade resolves.
{ "error": { "type": "booking_volume_refused", "message": "Too many bookings. Please try again later." }}Retrying does not help. The ceiling clears only when the account’s
billing period rolls over, which may be weeks away, so a client that retries
on a backoff will simply keep being refused. There is no Retry-After, no
count and no limit anywhere in the body, not even for a secret key or an
account key, which do see numbers on a plan-quota 402. An account reaching
this has a runaway integration; the fix is to stop it. If you believe you
have reached this legitimately, contact support.
daily_booking_limit and booking_volume_refused can both apply to the same
request; when they do, daily_booking_limit is reported first, since it is
the narrower and more specific of the two.
Idempotency is the other half of retrying safely
Section titled “Idempotency is the other half of retrying safely”POST /v1/bookings accepts an Idempotency-Key header. It is the mechanism
that makes a retry safe, and it is worth using on every create rather than only
in your error path.
Send a fresh unique value per booking attempt. A UUID is ideal:
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7A repeat with the same key and the same body replays the stored response, including its original status code. You get the same booking id back, and no second appointment is created. This is what saves you when a request times out after the booking was actually made.
A repeat with the same key and a different body is refused with a 422
and idempotency_mismatch. The stored response is never replayed in that case:
reusing a key with different content is always a client bug, and replaying would
hide it.
Two details worth knowing:
- Only a
2xxis stored. A failed create is not remembered, so you are free to retry it cleanly with the same key. - Simultaneous requests do not replay. Two identical creates arriving at the
same instant cannot both take the slot, but the loser gets its own
409rather than a copy of the winner’s response. Replay is guaranteed for the sequential case, which is the one a timeout produces.
Retrying, in one paragraph
Section titled “Retrying, in one paragraph”Retry a 429 typed rate_limited after the Retry-After it carries. A few
seconds is often enough, since the bucket refills continuously and a refusal
never cost you anything. Retry a 429 typed daily_booking_limit only after
its own Retry-After, which in practice means tomorrow, at that one
workspace, from that one caller. Do not retry a 429 typed
booking_volume_refused. It will not clear until the account’s billing
period rolls over. Retry a 503 after a short backoff. A 503 means two
writes contended, and the request is expected to succeed. Retry a create with
the same Idempotency-Key you used the first time. Do not retry any other
4xx: the same request will get the same answer.