Rate limits
The API limits how fast one caller can send requests. The limits are enforced on our servers. They are the same for every tenant.
How a limit counts
Section titled “How a limit counts”Each limit is a fixed window. A counter starts with your first request and resets when the window ends. Because the window is fixed, a burst that crosses a window edge can get close to twice the limit for a short time.
“IP” below always means the address Cloudflare sees for your request (the
CF-Connecting-IP value). An X-Forwarded-For header is ignored.
API-key limits
Section titled “API-key limits”| Bucket | Counted per | Limit | code on 429 |
|---|---|---|---|
Secret key (sk_) | key | 350 requests per minute (300 plus a burst allowance of 50) | rate_limited |
Publishable key (pk_) | key + IP | 60 requests per minute | rate_limited |
Hold creation (POST /v1/holds) with a pk_ key | key + IP | 10 requests per minute, on top of the publishable limit | rate_limited |
A pk_ request to POST /v1/holds counts against both the publishable
bucket and the hold-creation bucket. sk_ keys have no extra hold-creation
bucket.
Active-hold caps (POST /v1/holds)
Section titled “Active-hold caps (POST /v1/holds)”A hold blocks real inventory for 15 minutes. So the API also caps how many
holds can be active at the same time. An active hold is a hold in state
held whose expiry time has not passed yet.
| Cap | Applies to | Limit |
|---|---|---|
| Per publishable key + IP | pk_ keys | 2 active holds |
| Per tenant | all holds made with pk_ keys | 50 active holds |
- Over a cap, the API returns
429withcode: "active_hold_limit_reached".Retry-Aftertells you when the oldest counted hold expires. - A retry with the same
Idempotency-Keyis never refused. It returns the hold that request already created (200). sk_keys are exempt from both caps, and their holds do not count toward the per-tenant cap. A secret key is used from your own server, which often books for many guests from one IP address. It is still limited by the secret-key bucket above, and you can restrict it to your server’s IP addresses.- To free a slot: confirm the hold, or let it expire.
Guest verification (OTP) limits
Section titled “Guest verification (OTP) limits”These buckets are counted per hold (the reservation ID), whoever calls.
| Route | Limit | code on 429 |
|---|---|---|
POST /v1/holds/{id}/otp (send a code) | 5 per 15 minutes per hold | rate_limited |
POST /v1/holds/{id}/otp/verify (check a code) | 10 per 15 minutes per hold | rate_limited |
Each code is valid for 10 minutes and allows at most 5 wrong guesses. After
that, every guess gets the same 400 bad_code answer. Ask for a new code.
Dashboard sign-in limits
Section titled “Dashboard sign-in limits”These routes are for the operator dashboard. They have no API key, so they are counted by IP address (and, for the password-reset request, by email).
| Route | Counted per | Limit | code on 429 |
|---|---|---|---|
POST /v1/auth/login | IP | 20 per minute | rate_limited |
POST /v1/auth/password-reset/request | IP | 10 per 15 minutes | rate_limited |
POST /v1/auth/password-reset/request | email address | 3 per 15 minutes | rate_limited |
POST /v1/auth/password-reset/confirm | IP | 10 per 15 minutes | rate_limited |
- The email in the password-reset bucket is compared without case and without surrounding spaces.
- The password-reset request bucket counts every request, whether or not
the email belongs to a user. So a
429says nothing about whether an account exists. - The email bucket is per email address, not per workspace. A person has one password for all their workspaces, so the workspace you send (if any) does not change the count.
- Login also has a separate lockout. After 5 failed sign-ins for one
person (one email, across all workspaces) in 15 minutes, or 20 failed
sign-ins from one IP in 15 minutes, sign-in is refused with the normal
401 invalid_credentialsuntil the window passes.
When the rate limiter itself is down
Section titled “When the rate limiter itself is down”The counters live in a Cloudflare Durable Object. If that service cannot be reached:
- Secret-key and publishable-key buckets fail open. Your request goes through. An outage of the limiter must not block all API traffic.
- Security buckets fail closed. Hold creation, guest OTP, login and
password-reset requests get
429 rate_limitedwithRetry-After: 30. Letting them through without limits would open the abuse window they exist to close.
Neither case affects the double-booking guarantee. That check lives in the database, not in the rate limiter.
Request size limit
Section titled “Request size limit”A request body larger than 64 KB gets 413 with
code: "payload_too_large" before any other check runs. The Stripe webhook
(POST /v1/webhooks/stripe) allows up to 256 KB.
If you keep hitting a limit
Section titled “If you keep hitting a limit”The limits are the same for every tenant and cannot be changed per tenant
today. If your real traffic keeps hitting a limit, talk to your operator
contact. Do not add client retries that ignore Retry-After.
Next steps
Section titled “Next steps”- Errors — the error shape, including
429and413. - Authentication — which key kind gets which limit.