Errors and error codes
The one shape every failure arrives in, the type for each status, and the codes to branch on rather than the message.
Updated 8 October 2026 About 5 minutes View as Markdown
Every failure arrives in one shape, whichever part of the API it came from, so a client has one thing to parse.
{
"error": {
"type": "invalid_request",
"code": "naive_datetime",
"message": "Send a time with an offset, like 2026-06-10T10:00:00+02:00.",
"param": "starts_at"
}
}
All four keys are always there. type is the broad class, and decides whether
to try again. code is the specific reason, and is what to branch on.
message is written for a person and may be reworded at any time, so never
branch on it. param names the field that was wrong, or is null.
Types
| Status | Type |
|---|---|
400 |
invalid_request |
401 |
authentication_error |
403 |
permission_error |
404 |
not_found |
405 |
invalid_request |
406 |
invalid_request |
409 |
conflict |
415 |
invalid_request |
422 |
invalid_request |
429 |
rate_limit |
5xx |
api_error |
rate_limit and api_error are worth trying again: the first after the
seconds in Retry-After, the second after a short wait. Send a retried write
with the same Idempotency-Key, so it is answered rather than acted on twice.
The SDK's isRetryable() says which is which.
Codes about the request as a whole
| Code | Means |
|---|---|
not_authenticated |
No key was sent: no Authorization header, or one that is not Bearer |
invalid_key |
The key is malformed, revoked, or not a key at all |
secret_key_required |
The call needs a secret key and was made with a publishable one |
live_key_required |
A test key asked for something only a live key may do: publishing, unpublishing or restoring a page, or moving one that is on the website; changing bookings setup customers are offered, the opening hours or the live rules. Also a test key's checkout reaching an account that takes real money, which a working platform never lets happen |
origin_not_allowed |
A publishable key was used from a website not on its list |
subscription_inactive |
The account's plan has ended, so the API answers nothing |
quota_exhausted |
The month's allowance of requests is used up |
throttled |
Too many calls in a short time |
idempotency_key_required |
A write that needs an Idempotency-Key was sent without one |
idempotency_conflict |
A request with the same key is still being worked on |
idempotency_key_reused |
The same key was sent with a different request |
Codes about one field
When one field is wrong, param names it and code says how: required,
invalid, blank, null or max_length, and each part of the API adds its
own, such as naive_datetime when a time was sent without an offset. A code
about the request as a whole, such as slot_unavailable, has a param of
null.
Codes each part adds
Each part answers with codes of its own, about what it does, in the same shape. A part the project has not switched on answers none of them.
Content
| Code | Means |
|---|---|
browser_reads_not_allowed |
The project keeps its content off the browser, so pages are read from your server with a secret key |
limit_reached |
The website holds as many pages or section types, or a section type as many fields, as it may. Remove one to add another |
page_is_live |
The page is on the website. Unpublish it first, then delete it |
section_type_in_use |
A page holds a section of this type. Take it off every page first, then remove it |
Bookings
| Code | Means |
|---|---|
slot_unavailable |
The time is not free: taken a moment before, or outside the hours, the notice or the grid. Free times can be up to a minute old, so stop offering this one rather than asking again straight away |
naive_datetime |
A time was sent without an offset |
unknown_service |
No bookable service has that id |
unknown_staff |
Nobody bookable has that id, or they do not do that service |
phone_required |
The business asks for a phone number, and none was sent |
too_many_bookings |
This customer already holds as many bookings that day as the business allows |
invalid_date |
A date is not written YYYY-MM-DD, or the window ends before it starts |
window_too_wide |
Free times were asked for over more days than one request covers |
return_url_not_allowed |
A publishable key named a return address that is not one of its websites, or not https for a live key |
booking_not_active |
The booking no longer holds its time: cancelled, expired or over |
cancel_by_phone |
The business takes cancellations by phone, not online |
too_late_to_cancel |
The cancellation window has passed, so the customer contacts the business |
reschedule_not_allowed |
The business does not let customers move their own booking |
limit_reached |
The website holds as many services, people, opening hours rows or dated closures as it may. Remove one to add another |
Payments
| Code | Means |
|---|---|
payments_not_connected |
No payment account is connected in this key's mode, so nothing can be charged. The ordinary state of a project that has not connected one yet |
invalid_currency |
The currency is not one a project can be priced in, the same list its settings screen offers: counted in hundredths, like CHF or EUR |
invalid_filter |
A filter on the list is not one the API can read, such as an id that is not one |
too_much_metadata |
A checkout carried more metadata keys than it takes |
Inbox
| Code | Means |
|---|---|
bot_check_failed |
The website asks for Cloudflare's bot check and the token was missing, spent or refused. Ask the visitor to complete the check again, which makes a new token |
daily_limit_reached |
The website has received as many messages today as one website may. Nothing was kept, and it starts again at midnight on the website's clock |
invalid_folder |
A folder other than inbox, archived or spam was asked for |
Not found means not yours, too
A thing that does not exist, one that belongs to another project, and a part
the project has not switched on all answer the same 404, with the code
not_found. The API is not a way to find out what another account holds.