Skip to content

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.

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.

BucketCounted perLimitcode on 429
Secret key (sk_)key350 requests per minute (300 plus a burst allowance of 50)rate_limited
Publishable key (pk_)key + IP60 requests per minuterate_limited
Hold creation (POST /v1/holds) with a pk_ keykey + IP10 requests per minute, on top of the publishable limitrate_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.

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.

CapApplies toLimit
Per publishable key + IPpk_ keys2 active holds
Per tenantall holds made with pk_ keys50 active holds
  • Over a cap, the API returns 429 with code: "active_hold_limit_reached". Retry-After tells you when the oldest counted hold expires.
  • A retry with the same Idempotency-Key is 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.

These buckets are counted per hold (the reservation ID), whoever calls.

RouteLimitcode on 429
POST /v1/holds/{id}/otp (send a code)5 per 15 minutes per holdrate_limited
POST /v1/holds/{id}/otp/verify (check a code)10 per 15 minutes per holdrate_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.

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).

RouteCounted perLimitcode on 429
POST /v1/auth/loginIP20 per minuterate_limited
POST /v1/auth/password-reset/requestIP10 per 15 minutesrate_limited
POST /v1/auth/password-reset/requestemail address3 per 15 minutesrate_limited
POST /v1/auth/password-reset/confirmIP10 per 15 minutesrate_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 429 says 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_credentials until the window passes.

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_limited with Retry-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.

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.

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.

  • Errors — the error shape, including 429 and 413.
  • Authentication — which key kind gets which limit.