Rate limits

Every call to the v1 REST API and the MCP servers is counted against one or more windows (a minute, a day) per key, per business or per address. Each answer says where the call stands, and a call past a limit is refused with 429 and told when to try again.

The headers on every answer

Bookatu sends the fields of the IETF RateLimit header draft (draft-ietf-httpapi-ratelimit-headers), one set on every answer:

HeaderWhat it says
RateLimit-LimitThe limit of the window closest to running out.
RateLimit-RemainingCalls left in that window.
RateLimit-ResetSeconds until that window starts again.
RateLimit-PolicyEvery window the call was counted against, as limit;w=seconds, shortest first: 30;w=60, 2000;w=86400 is 30 a minute and 2,000 a day.
Retry-AfterOn every 429: seconds to wait before trying again.

A browser may read all of them: they are listed in Access-Control-Expose-Headers.

HTTP/1.1 200 OK
RateLimit-Limit: 30
RateLimit-Remaining: 29
RateLimit-Reset: 60
RateLimit-Policy: 30;w=60, 2000;w=86400
Bookatu-Version: 2026-10-30
Bookatu-Livemode: true

The limits

WhatCounted perLimit
Calls with a live key or connected app, Solo plan (standard tier)Each key, and the business's keys together30 a minute, 2,000 a day
Calls with a live key or connected app, Studio plan (standard tier)Each key, and the business's keys together60 a minute, 10,000 a day
Calls with a live key or connected app, Pro plan (standard tier)Each key, and the business's keys together180 a minute, 50,000 a day
Calls with a live key or connected app, Scale plan (standard tier)Each key, and the business's keys together600 a minute, 250,000 a day
Calls with a live key on the raised tierThat key, and its business's keys together while it calls4 times the plan's numbers above
Public calls with no key (business, services, staff, availability, book), Solo planEach address, at each business60 a minute, 5,000 a day
Public calls with no key (business, services, staff, availability, book), Studio planEach address, at each business120 a minute, 20,000 a day
Public calls with no key (business, services, staff, availability, book), Pro planEach address, at each business300 a minute, 100,000 a day
Public calls with no key (business, services, staff, availability, book), Scale planEach address, at each business600 a minute, 500,000 a day
Bookings through POST /book and POST /visits/bookEach address, at each business5 per 10 minutes, 15 a day
Bookings through the keyless doors, every address togetherEach business40 a day
The booking-management calls under /booking/{id}Each address30 per 5 minutes for the two reads, 10 per 10 minutes each for reschedule and cancel
GET /api/v1/discoverEach address60 per 5 minutes
MCP writes: create_booking and create_visit; the catalog writes togetherEach key or connected app30 per 10 minutes for bookings, 60 per 10 minutes for catalog writes
Calls with a key or app token that does not verify, and calls refused for a key of the other mode, across the whole APIEach address1,200 a minute (a call with no key at all is counted here too, but only its business's own limits refuse it)
Calls with a test key (bk_test_), whatever the tierEach test key, and the sandbox's keys together25 a minute, 1,000 a day
Public calls with no key at a sandboxEach address, at each sandbox30 a minute, 1,000 a day
GET /sandbox/outboxEach sandbox30 a minute
GET /sandbox/paymentsEach sandbox30 a minute
POST /sandbox/simulate/{event}Each sandbox10 a minute, 500 a day
Making a sandbox (POST /api/developer/sandboxes); resetting oneEach address; each person10 made a day per address and 5 per person, at most 3 at once; 20 calls to make one a day per address, refused ones included; 30 resets a day per address

A key is held to the plan of the business it belongs to. Keyless calls are counted per address at each business, so one busy client never uses up another's budget, and a key sent to a keyless endpoint is not counted on the key. A call refused before the endpoint counts it (a key without the endpoint's scope, another business's key, a business that is not found) is not counted on the key either. An answer the MCP server sends before it counts the call (the notifications/initialized 202, a parse error) says your key's windows as they stand.

Standard and raised

Every key is on the standard tier: its plan's numbers above. A Bookatu platform admin can put a live key on the raised tier, which gives that key, and its business's keys together while it calls, 4 times the plan's numbers: 120 a minute instead of 30 on Solo. Ask Bookatu support when your integration needs it, naming the key's prefix and the business. Only a platform admin can change a tier, and every change is in the business's audit log.

Test keys are always held to the sandbox's limits, whatever their tier: raised never raises a test key.

When you get a 429

The answer is a 429 with Retry-After, the headers above, and a body saying when to retry. Its words differ by endpoint; the status and Retry-After never do. Over MCP it is a JSON-RPC error -32000, Rate limit exceeded, with error.data.retryAfterSec.

HTTP/1.1 429 Too Many Requests
Retry-After: 17
RateLimit-Limit: 30
RateLimit-Remaining: 0
RateLimit-Reset: 17
RateLimit-Policy: 30;w=60, 2000;w=86400

{ "error": "Rate limit exceeded", "retryAfterSec": 17, "livemode": true }
  • Wait at least Retry-After seconds before calling again.
  • Back off exponentially on repeated 429 and 5xx answers, with jitter (a random part in each wait), so many clients do not retry in step.
  • Watch RateLimit-Remaining and slow down before it reaches 0.
  • Cache what rarely changes: GET /business, /services and /staff.
  • Use webhooks to learn about changes instead of polling for them.
async function call(url, init, attempt = 0) {
  const res = await fetch(url, init);
  if ((res.status === 429 || res.status >= 500) && attempt < 5) {
    const retryAfter = Number(res.headers.get("Retry-After")) || 2 ** attempt;
    const jitter = Math.random() * 1000;
    await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter));
    return call(url, init, attempt + 1);
  }
  return res;
}