---
title: "Rate limits"
description: "How many calls the Bookatu API takes per key, per business and per address; the RateLimit headers on every answer; the standard and raised tiers; and what to do on a 429."
url: "https://bookatu.com/en-nz/developers/rate-limits"
language: "en"
section: "Developers"
---

# 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:

| Header | What it says |
| --- | --- |
| `RateLimit-Limit` | The limit of the window closest to running out. |
| `RateLimit-Remaining` | Calls left in that window. |
| `RateLimit-Reset` | Seconds until that window starts again. |
| `RateLimit-Policy` | Every 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-After` | On every `429`: seconds to wait before trying again. |

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

```http
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

| What | Counted per | Limit |
| --- | --- | --- |
| Calls with a live key or connected app, Solo plan (standard tier) | Each key, and the business's keys together | 30 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 together | 60 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 together | 180 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 together | 600 a minute, 250,000 a day |
| Calls with a live key on the raised tier | That key, and its business's keys together while it calls | 4 times the plan's numbers above |
| Public calls with no key (business, services, staff, availability, book), Solo plan | Each address, at each business | 60 a minute, 5,000 a day |
| Public calls with no key (business, services, staff, availability, book), Studio plan | Each address, at each business | 120 a minute, 20,000 a day |
| Public calls with no key (business, services, staff, availability, book), Pro plan | Each address, at each business | 300 a minute, 100,000 a day |
| Public calls with no key (business, services, staff, availability, book), Scale plan | Each address, at each business | 600 a minute, 500,000 a day |
| Bookings through POST /book and POST /visits/book | Each address, at each business | 5 per 10 minutes, 15 a day |
| Bookings through the keyless doors, every address together | Each business | 40 a day |
| The booking-management calls under /booking/{id} | Each address | 30 per 5 minutes for the two reads, 10 per 10 minutes each for reschedule and cancel |
| GET /api/v1/discover | Each address | 60 per 5 minutes |
| MCP writes: create_booking and create_visit; the catalog writes together | Each key or connected app | 30 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 API | Each address | 1,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 tier | Each test key, and the sandbox's keys together | 25 a minute, 1,000 a day |
| Public calls with no key at a sandbox | Each address, at each sandbox | 30 a minute, 1,000 a day |
| GET /sandbox/outbox | Each sandbox | 30 a minute |
| GET /sandbox/payments | Each sandbox | 30 a minute |
| POST /sandbox/simulate/{event} | Each sandbox | 10 a minute, 500 a day |
| Making a sandbox (POST /api/developer/sandboxes); resetting one | Each address; each person | 10 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
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.

```js
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;
}
```
