Errors
Every error response is application/problem+json (RFC 9457):
{ "type": "about:blank", "title": "Not Found", "status": 404, "detail": "No such resource for this tenant.", "code": "resource_not_found"}statusalways matches the HTTP status code.detailis human-readable and safe to log — it’s written for you to debug with, not for you to show a guest verbatim.codeis a stable, machine-readable string. Match your error handling oncode, never ondetailtext, which can change wording without notice.
A deliberate 404 policy
Section titled “A deliberate 404 policy”If you get an unexpected 404, read the code first. It tells you which
check failed. See Authentication errors below. Then
check, in this order: the resource ID, whether you’re using the key kind
(sk_/pk_) the route expects, whether your key holds the scope the route
needs, and — for a pk_ key — whether the request’s Origin header is on
that key’s allowlist, and for an sk_ key whether your server’s IP address
is on that key’s IP list (if it has one).
Authentication errors
Section titled “Authentication errors”Every route that takes an API key can return these.
code | Status | Meaning |
|---|---|---|
missing_authorization | 401 | The Authorization: Bearer sk_… or pk_… header is missing or malformed. |
unknown_api_key | 404 | The key does not exist, or it was revoked. |
key_kind_not_allowed | 404 | This route does not accept this kind of key. For example, a pk_ key on a route that needs sk_. |
insufficient_scope | 404 | The key does not hold the scope this route needs. detail names the scope. See Secret key scopes. |
origin_not_allowed | 404 | A pk_ request came from an origin that is not on the key’s list. |
ip_not_allowed | 404 | An sk_ request came from an IP address that is not on the key’s list. |
rate_limited | 429 | The key went over a rate limit. See Rate limits. |
key_auth_not_configured | 404 | A safety net. A route with no allowed key kind refuses every key. Routes for dashboard staff only (for example /v1/api-keys) answer this to API keys. |
Common codes by endpoint
Section titled “Common codes by endpoint”| Endpoint | code | Status |
|---|---|---|
GET /v1/availability | invalid_window (from is not before to) | 400 |
GET /v1/availability, POST /v1/quotes | resource_not_found | 404 |
GET /v1/availability, POST /v1/quotes | validation_failed (the window is longer than 92 nights for availability, 366 for quotes) | 400 |
GET /v1/resources/{id}/calendar | invalid_window, range_too_large (more than 92 nights) | 400 |
GET /v1/resources/{id}/slots | not_hourly (the resource is not an hourly space), invalid_duration (not one of the space’s durations) | 400 |
GET /v1/resources/{id}/slots | resource_not_found | 404 |
POST /v1/quotes | rate_plan_required, invalid_window, invalid_duration (hourly space: the length is not an allowed duration) | 400 |
POST /v1/quotes | rate_plan_not_found | 404 |
POST /v1/quotes | unavailable (carries reasons) | 409 |
POST /v1/quotes | no_pricing_rule (carries missingDates), fx_rate_unavailable | 422 |
POST /v1/holds | idempotency_key_required, invalid_quote_token, quote_expired, guest_required (a pk_ hold on an hourly space has no guest) | 400 |
POST /v1/holds | resource_locked (carries Retry-After), hold_conflict (the time is held or booked, or too close to another booking because of a buffer) | 409 |
POST /v1/holds | rate_limited (hold-creation rate), active_hold_limit_reached (too many active holds — see Rate limits) | 429 |
POST /v1/holds/{id}/otp | hold_not_found | 404 |
POST /v1/holds/{id}/otp | hold_not_held (the hold is expired, confirmed, or cancelled) | 409 |
POST /v1/holds/{id}/otp/verify | bad_code (a wrong, expired, or used-up code — all three look the same on purpose) | 400 |
| guest-OTP routes | rate_limited | 429 |
POST /v1/holds/{id}/confirm | invalid_guest_token, wrong_reservation | 400 |
POST /v1/holds/{id}/confirm | hold_not_found | 404 |
POST /v1/holds/{id}/confirm | not_confirmable, payment_required (no succeeded payment yet for this hold — see Holds and confirmation) | 409 |
POST /v1/holds/{id}/payment-intent | idempotency_key_required | 400 |
POST /v1/holds/{id}/payment-intent | reservation_not_found | 404 |
POST /v1/holds/{id}/payment-intent | reservation_not_holdable, stripe_not_configured, already_paid | 409 |
POST /v1/holds/{id}/checkout-session | idempotency_key_required, invalid_redirect_url | 400 |
POST /v1/holds/{id}/checkout-session | reservation_not_found | 404 |
POST /v1/holds/{id}/checkout-session | reservation_not_holdable, stripe_not_configured, already_paid, payment_already_initiated | 409 |
GET /v1/bookings, GET /v1/payments | invalid_cursor | 400 |
GET/PATCH/DELETE /v1/bookings/{id} | booking_not_found | 404 |
PATCH/DELETE /v1/bookings/{id} | illegal_transition | 409 |
POST /v1/bookings/{id}/confirm | booking_not_found | 404 |
POST /v1/bookings/{id}/confirm | not_confirmable (the booking is not held, its hold expired, or an online payment already confirmed it; detail names the state) | 409 |
POST /v1/bookings | idempotency_key_required, invalid_window, rate_plan_required | 400 |
POST /v1/bookings | resource_not_found, rate_plan_not_found | 404 |
POST /v1/bookings | unavailable (carries reasons), resource_locked, booking_conflict, idempotency_key_conflict, stripe_not_configured, already_paid, payment_already_initiated, payment_setup_failed | 409 |
POST /v1/bookings | no_pricing_rule (carries missingDates), fx_rate_unavailable | 422 |
POST /v1/bookings/{id}/refunds | booking_not_found | 404 |
POST /v1/bookings/{id}/refunds | no_succeeded_payment, stripe_not_configured, refund_exceeds_payment, stripe_refund_failed, nothing_to_refund | 409 |
PUT /v1/resources/{id}/rates, PUT /v1/resources/{id}/restrictions | no_room_type (the resource has no room type) | 409 |
PUT /v1/resources/{id}/restrictions | unknown_rate_plan_code | 400 |
POST /v1/rates/bulk | resource_rate_plan_mismatch | 400 |
POST /v1/rates/bulk | resource_not_found, rate_plan_not_found | 404 |
POST /v1/resources, PATCH /v1/resources/{id} | invalid_resource (the fields do not fit the resource’s vertical), invalid_timezone (not an IANA zone name) | 400 |
POST /v1/resources | room_type_not_found | 404 |
POST /v1/room-types | duplicate_code | 409 |
GET/PUT …/slot-rules, GET/PUT …/hourly-pricing, GET/PUT …/opening-hours | not_hourly (the resource is not an hourly space) | 400 |
PUT …/opening-hours | invalid_hours (not in order, overlapping, or not in 5-minute steps) | 400 |
GET/PUT …/slot-rules, GET/PUT …/hourly-pricing, GET/PUT …/opening-hours | resource_not_found | 404 |
GET/POST /v1/webhook-endpoints, DELETE /v1/webhook-endpoints/{id} | webhook_endpoint_not_found | 404 |
PATCH /v1/api-keys/{id} (dashboard session) | allowlist_kind_mismatch (origins sent for a secret key, or IPs for a publishable key) | 400 |
PATCH /v1/api-keys/{id}, DELETE /v1/api-keys/{id} (dashboard session) | api_key_not_found | 404 |
POST /v1/webhooks/stripe | invalid_signature (this route is for Stripe, not for you) | 400 |
Features that are not available yet
Section titled “Features that are not available yet”Two parts of the API are switched off on the current deployment. Their
routes still exist, so they answer with 503 and a stable code. They
never move data.
| Routes | code | Status |
|---|---|---|
Channel connections, channel conflicts, iCal feed tokens, GET /v1/feeds/{token}.ics | channel_unavailable | 503 |
POST /v1/quotes/rental, GET/PUT /v1/rate-plans/{id}/rental-tiers | rental_unavailable | 503 |
Do not retry a 503 with one of these codes. It will not succeed until the
feature is switched on. See the Changelog.
Errors any route can return
Section titled “Errors any route can return”code | Status | When |
|---|---|---|
validation_failed (carries errors) | 400 | The request body, query or path does not match the route’s schema. errors is a list of { path, message }. |
rate_limited | 429 | Your API key or IP address went over a rate limit. Always sent with Retry-After. See Rate limits. |
payload_too_large | 413 | The request body is over 64 KB (256 KB for the Stripe webhook). |
Every 429 carries a Retry-After header (seconds). Wait at least that
long before you retry.
Dashboard sign-in errors
Section titled “Dashboard sign-in errors”These codes belong to the operator dashboard’s session routes
(/v1/auth/*, /v1/users, /v1/tenant, /v1/audit). They are not for
sk_ or pk_ integrations, so you will only see them if you call those
routes with a dashboard session.
code | Status | When |
|---|---|---|
invalid_credentials | 401 | The password is wrong, the email is unknown, the email has no active workspace, or the account is locked. All look the same on purpose. A tenantId or tenantSlug that the person does not belong to is ignored. It does not cause this error. |
invalid_workspace_selection | 401 | GET /v1/auth/workspace-choices or POST /v1/auth/select-workspace: the be_ws_select cookie is missing, expired (it lasts 5 minutes), or not valid, or the tenantId is not one of the person’s active workspaces. One code for all of these. Log in again. |
missing_session, invalid_session | 401 | The session cookie is missing, expired, or not valid. |
missing_refresh_token, invalid_refresh_token, reuse_detected | 401 | The refresh cookie is missing, not valid, or was already used. |
invalid_reset_token | 400 | The password-reset token is not valid, or it expired. |
rate_limited | 429 | Too many sign-in or password-reset requests. See Rate limits. |
user_not_found, tenant_not_found | 404 | No such staff account or workspace. |
email_already_invited | 409 | This email already has a staff account in this workspace. |
invites_unavailable | 503 | POST /v1/users or POST /v1/users/invite: the server cannot build a working link for the invite email, so it saved nothing. This is a server setting. Retrying does not help. Tell the platform operator. |
invalid_cursor | 400 | The cursor for the audit list is not valid. |
Full per-endpoint error lists live in the API Reference, generated straight from the checked-in OpenAPI spec.
Next steps
Section titled “Next steps”- Rate limits — every limit, the
429codes andRetry-After. - Authentication — how key kind and scope affect the 404 policy above.