Errors and limits
Rate limits
The two request budgets of a key, the 429 answer, and how to stay within them.
Every API key has two request budgets a minute. This guide covers the budgets, the answer to a call over them, and how to stay within them.
Two budgets#
The budgets are counted in fixed one-minute windows:
| Calls | Limit per key |
|---|---|
| Reading venues, menus, tables, orders, bookable times and reservations; pricing a basket | About 120 a minute |
| Placing an order; taking or cancelling a reservation; sending a test webhook | About 30 a minute |
- The two budgets are separate: reading the menu does not use up the orders and reservations a key may send.
- A retried order or reservation counts as a call.
- All the calls from one IP address also share a ceiling of about 500 a minute with everything else on that address.
Why the limits are approximate#
Cibusy runs on several servers, and each counts the calls it receives for itself. Under load a key may therefore get somewhat more than 120 or 30 calls through in a minute. Plan for the figures above rather than for what may get through, and treat a 429 as the signal to slow down.
The 429 answer#
A call over the budget is not queued. It is answered 429 RATE_LIMIT_EXCEEDED with a Retry-After: 60 header, and the next window opens within the minute.
{
"success": false,
"userMessage": "Too many requests for this API key. Wait a minute and try again.",
"developerMessage": "Rate limit exceeded for policy. Please retry after 60 seconds.",
"errorCode": "RATE_LIMIT_EXCEEDED",
"retryAfter": 60,
"timestamp": "2026-10-01T09:30:00.123Z",
"traceId": "0HNF3N1G9B0TO:00000003"
}successbooleanAlways false.
userMessagestringA sentence for the person using your site, in the language of the Accept-Language header.
developerMessagestringFor you and your logs, in English. Its wording may change; do not parse it.
errorCodestringAlways RATE_LIMIT_EXCEEDED.
retryAfterintegerSeconds to wait. The same as the Retry-After header.
timestampstring · date-timeWhen the answer was written, in UTC.
traceIdstringIdentifies the request in Cibusy's logs. Quote it when you write to support.
Wait the number of seconds in the Retry-After header (or in retryAfter), then try again. Trying again without waiting only brings the next 429.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function callCibusy(url, options = {}) {
for (let attempt = 1; ; attempt++) {
const response = await fetch(url, {
...options,
headers: { ...options.headers, 'X-Api-Key': process.env.CIBUSY_API_KEY },
});
if (response.status !== 429 || attempt === 3) return response;
const seconds = Number(response.headers.get('Retry-After')) || 60;
await wait(seconds * 1000);
}
}Waiting is right in a background job. While answering a guest's request, rather than keep them waiting for a minute, tell them to try again shortly: that is what userMessage is written for.
Staying within the budget#
- Keep a copy of the menu. Do not read the menu for every visitor; check your copy now and then with
If-None-Match. Reading the menu shows how. - Follow orders and reservations with webhooks. Rather than asking about the open ones over and over, use a webhook, and read only an order or a reservation you are unsure about.
- Do not re-read what does not change. Tables and a venue's details change seldom. Reading the venue's
orderingstate when the order screen opens is enough; the answer to placing an order says whether it was accepted anyway. - Give each integration a key of its own. The budget is per key: if your website and an ordering kiosk use separate keys, one cannot use up the other's budget.