Skip to content

Errors

Every error, from the API and from the CLI, has a machine code, a human message, the guilty field when there is one, a documentation URL, and, from the CLI, a fix: one sentence that says what to do next.

{ "ok": false, "environment": "test",
"error": {
"code": "slot_unavailable",
"message": "The requested slot is no longer available. 1 unit requested, 0 available.",
"param": "start",
"doc_url": "https://bookrail.dev/docs/errors#slot_unavailable",
"fix": "The capacity is gone. Run `bookrail availability --service ... --explain` to see what took it.",
"request_id": "req_..."
} }
CodeMeaning
0Success
1User or configuration error. Re-running the same command fails the same way.
2Authentication or permission.
3Network, timeout, rate limit or server fault. Retrying later may work.
4Conflict: the state changed underneath. Retrying may work right now.

invalid_request (400), authentication (401), permission (403), not_found (404), conflict (409), rate_limit (429), policy_violation (422), payment_required (402), internal (500).

CodeTypeWhen
missing_api_key, invalid_api_key, revoked_api_keyauthenticationNo key, a malformed one, or one that was revoked.
live_key_without_liveauthentication (CLI)A sk_live_ key is configured and --live was not typed. Nothing was sent.
parameter_missing, parameter_invalid, invalid_bodyinvalid_requestThe request body.
resource_missingnot_foundAn id that does not exist in this project and environment.
slot_unavailableconflictThe capacity is gone. The message carries the units requested and available.
hold_expired, hold_not_activeconflictThe hold died or was already used.
serialization_failureconflictNothing was written. Run the command again.
idempotency_key_in_progressconflictAnother request with the same key is still running.
idempotency_key_reusedinvalid_requestThe same key was used for a different request.
start_not_on_gridpolicy_violationThe instant is not on the slot grid of the service.
min_notice_violated, outside_booking_windowpolicy_violationThe booking window of the service.
customer_limit_reachedpolicy_violationmaxActiveBookingsPerCustomer is reached.
duration_not_offeredinvalid_requestNot one of the service’s durations.
resource_not_eligibleinvalid_requestA forced resource is not a candidate of any requirement.
invalid_transitionconflictThe booking’s state does not allow that action.
no_show_too_early, complete_too_earlypolicy_violationToo early for that transition.
max_reschedules_reachedpolicy_violationThe policy’s limit.
range_too_large, invalid_range, timezone_missinginvalid_requestAvailability requests.
invalid_webhook_urlinvalid_requestNot public, or http on live.
rate_limitedrate_limitThe key called faster than its ceiling. See below.
signup_rate_limitedrate_limitToo many sign up requests for one address, or from one caller.
terms_not_acceptedinvalid_requestA sign up, or a checkout from the dashboard, without both ticks of the terms: the Terms of Service with the Data Processing Agreement, and the specific approval of the clauses in their Section 17. From a terminal, npx bookrail@latest signup, or --accept-terms --approve-clauses without one.
plan_limit_reachedpayment_requiredLive only, free plan: the confirmed live bookings of the month plus the open pending ones have reached what the plan includes, or this payment, with the month’s paid volume and the open payments, would go past the included volume (param: payment.mode). Nothing was taken. The fix sends to the dashboard, where Pro is bought with a card. Like every 4xx it stays stored on its Idempotency-Key for 24 hours: after the upgrade, retry with a new key.

Two codes that people expect and that do not exist: capacity_exceeded (a quantity above capacity is slot_unavailable, whose message is more precise) and schedule_conflict (a calendar change that invalidates a future booking is not an error: it emits a booking.orphaned event and leaves the booking alone).

Every API key has a ceiling: 20 requests a second with bursts of 40 on a sk_test_ key, and on a sk_live_ one the ceiling of the account’s plan (20 with bursts of 40 on free, 100 and 500 on pro, 500 and 2 500 on scale). Every response carries the budget, so a client can pace itself instead of discovering the ceiling by hitting it:

HeaderMeaning
RateLimit-LimitThe burst: the most this key may have in flight at one instant.
RateLimit-RemainingHow many more requests would be accepted right now.
RateLimit-ResetWhole seconds until RateLimit-Remaining is back at RateLimit-Limit.
RateLimit-PolicyOnly ever unavailable, which means no limit could be applied and the request was served anyway.
Bookrail-Plan-UsageLive keys only: <confirmed>/<included>, the account’s confirmed live bookings this month over the ones its plan includes, as they stood when the request started.

Over the ceiling the answer is 429 rate_limited with Retry-After in whole seconds, at least 1, and a fix. The CLI exits 3 for it, the family’s code, because waiting and running the command again is the thing that works. The SDK (@bookrail/node) retries a 429 by itself, after the Retry-After the server sent.

The bucket is the key, not the project: two keys of one project have two budgets, and a script hammering one of them does not throttle the other. A refusal costs nothing else, in particular it does not consume an Idempotency-Key: the same key on the retry runs the request for real.

The sign up endpoints are the exception, because they have no key: they are limited by address, and by how many messages one mailbox may be sent. That limit is signup_rate_limited, and the reverse proxy in front of them answers with the same envelope when it refuses a burst of its own. bookrail signup treats such a refusal as a pause rather than a failure and waits for the Retry-After it was given, up to fifteen seconds: the number is written by whatever refused the request, which is not always this API, and a proxy asking for a minute should not park a sign up whose link is already valid.

CodeMeaning
config_not_foundNo bookrail.config.* in the working directory.
invalid_configThe file parsed but does not validate. The message lists each position.
config_unreadableThe file could not be evaluated. Usually a TypeScript config on a Node without a TypeScript loader.
confirmation_requiredThe command would delete something and --yes was not given.
ambiguous_config_idTwo remote objects carry the same metadata.config_id.
missing_inputA required value was not given and there was no terminal to ask on.
network_error, timeoutThe API could not be reached.