Sign in to see your venue's slug and publishable key in every sample.
Your account has no venue yet, so the samples keep their placeholders. Sign out
Signed in as · . Create a publishable key in the console and reload to see it here. Sign out
Signed in as · . The samples show your venue's publishable key. Sign out
Developer docs
Errors
Every problem type the API emits for a refused key, what caused it, what fixes it, and the problems that carry no reason code.
When the API refuses a request because of the key, the problem body's type is https://bookdineplay.com/docs/api/errors/<reason> and <reason> is one of the six sections on this page. Follow the URL and you land here. Twenty-five more type URLs land here as well: the plan problem, the two idempotency problems (shared by reservation create, event registration, ticket checkout and voucher checkout), the three event-registration and refund problems, the two reservation-cancellation problems, the twelve gift voucher problems, and the five billing/account/payout problems further down. Every other refusal — validation, a missing venue, a slot that is gone — is listed at the end.
| Reason | Status | In one line |
|---|---|---|
| missing-api-key | 401 | No key on a /api/venues/** request |
| invalid-api-key | 401 | The key is unknown or has been revoked |
| venue-mismatch | 403 | The key belongs to a different venue |
| origin-not-allowed | 403 | Publishable key, Origin not on its allowlist |
| secret-key-from-browser | 403 | Secret key sent with an Origin header |
| publishable-key-without-origin | 403 | Publishable key sent without an Origin header |
missing-api-key
401. The request reached a /api/venues/** endpoint with neither an X-BookDinePlay-Key nor an Authorization: Bearer header. Most often the header was set on the wrong client instance, or a proxy stripped it. Fix: send the key on every request — see Sending a key.
{
"type": "https://bookdineplay.com/docs/api/errors/missing-api-key",
"title": "Unauthorized",
"status": 401,
"detail": "An API key is required. Send it in the 'X-BookDinePlay-Key' header or an 'Authorization: Bearer' header.",
"traceId": "0HNOJ52B2L9BB"
}invalid-api-key
401. A key was sent, but no active key matches it: it was mistyped, revoked in the console under Venue → API keys, or rotated and the old value is still deployed. Fix: paste the current key from the console; when rotating, deploy the new key before revoking the old one.
venue-mismatch
403. The key is valid but scoped to another venue than the {venueSlug} in the URL. Keys are per venue; a site that embeds several venues needs one key per venue (the WordPress plugin's key attribute exists for exactly that). Fix: use the key created under the venue you are calling.
origin-not-allowed
403. A publishable key was sent from an origin that is not on the key's allowlist. Typical causes: the site moved to a new domain, www versus the bare domain, http versus https, or a staging host. Fix: add the exact origin (scheme, host, and port if any) under Venue → API keys, or add a :* port wildcard for local development — see Origins.
{
"type": "https://bookdineplay.com/docs/api/errors/origin-not-allowed",
"title": "Forbidden",
"status": 403,
"detail": "This origin is not on the API key's list of allowed origins.",
"traceId": "0HNOJ52B2L9BD"
}secret-key-from-browser
403. A secret key (bdp_sk_…) arrived together with an Origin header, which only browsers send. A secret key in a browser is a published secret, so the request is refused outright rather than served. Fix: revoke that key now, create a publishable key for the browser, and keep secret keys on the server.
publishable-key-without-origin
403. A publishable key (bdp_pk_…) arrived without an Origin header. Publishable keys are protected by their allowlist, and without an origin there is nothing to check against. This is what a server-side call with a publishable key looks like. Fix: use a secret key on the server; if the call really comes from a browser, the Origin header is added automatically — a missing one means the request was not made by a browser.
Plan and entitlement problems
Not key problems: the tenant's plan does not include what the request needs.
feature-not-in-plan
403. Seen on the operator API (creating or editing a game-type resource, opening the floor plan, managing employees, or managing events on a plan without them), on POST /reservations for a resource type the tenant's plan does not offer, on the public POST /events/{eventSlug}/registrations route when the venue's plan does not include Events — the events list answers [] and the detail 404 on such a plan, so only a guest who still holds an event URL reaches this problem — and on POST /payment-intents when the plan does not include online payments or deposits. The paid ticket-checkout route never answers this one: a plan without ticket sales folds into ticket-sales-not-enabled instead, so a guest never learns which of the three causes applies. The booking widget itself never triggers feature-not-in-plan, because availability already omits resource types a Starter plan does not allow. requiredPlan names the tier (Pro or Premium) that unlocks the feature. Fix: upgrade the tenant's plan — in the console under Your plan, or via PUT /api/admin/tenants/{id}.
{
"type": "https://bookdineplay.com/docs/api/errors/feature-not-in-plan",
"title": "Feature not in plan",
"status": 403,
"detail": "This feature requires the Pro plan or higher.",
"requiredPlan": "Pro",
"traceId": "0HNOJ52B2L9BE"
}Which tier unlocks what, so you can tell the operator what to upgrade to. requiredPlan always names one of these:
| Capability | Unlocked by |
|---|---|
| Game resources — billiards, darts, shuffleboard, bowling lanes, event space | Pro |
| Table and game ordering, floor plans, employees, guest messaging | Pro |
| Venue events — publishing, free registration, named tickets, door check-in | Pro |
| Online payments and deposits | Premium |
| Online ticket sales for paid events | Premium |
| More than one venue under the same account | Premium |
What each tier costs is on the pricing page.
Event registration problems
Not key problems either: the event's registration window, the state of online ticket sales, or a Paid registration's refund policy refuses a valid request. The first two come from POST /api/venues/{venueSlug}/events/{eventSlug}/registrations and the ticket-checkout route; the third comes from the guest's own cancel route.
registration-closed
409. The registration window is shut. reason names why, and the event detail reports the same value in registration.reason before you even try:
reason |
Meaning | What to show |
|---|---|---|
closed |
The venue's closesAt has passed |
"Registration has closed" |
sold-out |
No tickets are left; the problem adds remaining: 0 |
"Sold out" — and re-read the detail, because a cancellation frees seats |
past |
The event has already started | "This event has already started" |
not-open |
The event takes no registrations (mode None) or was cancelled |
The event's own status |
A registration can be refused sold-out even though the detail you read a moment ago showed seats: another guest took the last ones in between. Treat the problem as the truth and the detail as a hint. The JavaScript client lifts reason and remaining onto the error (Errors); the .NET SDK exposes them as Reason and Remaining on BookDinePlayApiException (.NET SDK).
{
"type": "https://bookdineplay.com/docs/api/errors/registration-closed",
"title": "Registration closed",
"status": 409,
"detail": "Registration for this event has closed.",
"reason": "closed",
"traceId": "0HNOK3A31C71T"
}ticket-sales-not-enabled
409. The event is Paid and the venue cannot sell tickets online right now. It is the single answer for three different causes — the tenant's plan does not include ticket sales, the venue's plan does include it but its Stripe Connect payout account is not Enabled yet, or the event itself is not Paid — deliberately folded into one problem so a guest never learns which one it is; the venue sees the precise reason in the console instead (the Billing → Payouts card, or the event editor's own note). On registration this only applies while the event's window is open — a paid event whose window is shut answers registration-closed first, the same as a free event; the checkout route answers it regardless of the window. Fix: nothing on your side — show a generic "not available right now" message, as the events widget does, and let the venue's other channels sell the tickets.
{
"type": "https://bookdineplay.com/docs/api/errors/ticket-sales-not-enabled",
"title": "Ticket sales not enabled",
"status": 409,
"detail": "Online ticket sales are not enabled for this venue yet.",
"traceId": "0HNOK3A31C71U"
}refund-not-available
409. A guest tried to cancel a Paid registration through their own cancel link, but the event's refund policy does not allow an online refund right now — the venue set "no online refunds", or the refund window (hoursBeforeStart before the event starts) has already passed. The registration is left untouched: nothing is cancelled and no charge is touched, so the guest can still ask the venue directly. GET /api/registrations/{cancelToken} reports the same thing in advance as canCancel: false with a cancelNote explaining why, so an integration never has to attempt the cancel just to find out. Fix: nothing on your side — show the note and point the guest at the venue; only the venue (from the event's attendee list) or BookDinePlay support can refund from here.
{
"type": "https://bookdineplay.com/docs/api/errors/refund-not-available",
"title": "Refund not available",
"status": 409,
"detail": "This registration cannot be refunded online. Please contact the venue directly.",
"traceId": "0HNOK3A31C71X"
}reservation-cancellation-not-allowed
409. A guest tried to cancel a booking through their own cancel link, but the venue's cancellation policy does not allow an online cancel right now — the venue switched guest self-cancellation off entirely, or the cut-off (hoursBeforeStart before the booking starts) has already passed. The reservation is left untouched: nothing is cancelled. GET /api/reservations/{cancelToken} reports the same thing in advance as canCancel: false with a cancelNote explaining why, so an integration never has to attempt the cancel just to find out. Fix: nothing on your side — show the note and point the guest at the venue directly.
{
"type": "https://bookdineplay.com/docs/api/errors/reservation-cancellation-not-allowed",
"title": "Reservation cancellation not allowed",
"status": 409,
"detail": "This booking can no longer be cancelled online. Please contact the venue directly.",
"traceId": "0HNOK3A31C71Y"
}reservation-deposit-not-refundable
409. A guest tried to cancel a booking through their own cancel link, but the booking's deposit was taken online and the venue has chosen that a deposit is sorted out by talking to them rather than through a cancel link. Cancelling would free the slot with nobody having decided what happens to the money, so it is refused. Deliberately a different type from reservation-cancellation-not-allowed: a venue's site needs to say something different here ("we need to sort your deposit out, please call us") than it says for an ordinary cut-off refusal ("too close to your booking"). The reservation is left untouched. GET /api/reservations/{cancelToken} reports the same thing in advance as canCancel: false with a cancelNote explaining why. Fix: nothing on your side — show the note and point the guest at the venue directly. This is the venue's own setting: an operator who switches their cancellation policy to refund (or to keep) a deposit on a guest cancel stops producing this problem entirely.
{
"type": "https://bookdineplay.com/docs/api/errors/reservation-deposit-not-refundable",
"title": "Reservation deposit not refundable",
"status": 409,
"detail": "This booking has a deposit that cannot be refunded online yet. Please contact the venue directly.",
"traceId": "0HNOK3A31C71Z"
}reservation-refund-failed
502. The opposite case to the one above: the venue DOES refund deposits when a guest cancels, and the refund itself did not go through at the payment provider. Nothing was cancelled and nothing was recorded — the refund runs before the cancellation precisely so a failure leaves the guest's booking intact rather than freeing their slot while their money stays put. A 502 rather than a 409 is the point: this is not the venue's settled policy but a transient upstream failure, so it is genuinely worth retrying and your UI must not tell the guest their venue does not allow this. Fix: invite the guest to try again in a moment; if it keeps failing, the venue's payment account needs attention and the guest should contact them.
{
"type": "https://bookdineplay.com/docs/api/errors/reservation-refund-failed",
"title": "Reservation refund failed",
"status": 502,
"detail": "We couldn't refund your deposit just now, so your booking was left as it is. Please try again in a moment.",
"traceId": "0HNOK3A31C720"
}Idempotency problems
Not a key problem or a plan problem: four routes accept an optional Idempotency-Key header —
POST /api/venues/{venueSlug}/reservations,
POST /api/venues/{venueSlug}/events/{eventSlug}/registrations,
the ticket-checkout route
and the voucher checkout
— and this section is the two ways that header itself can be refused, or make you wait.
idempotency-key-reused
409. The Idempotency-Key you sent was already used — within the last 24 hours, on the same route, for this venue, by your own account — for a request whose body hashes differently from this one. Nothing was created by this call: a matching body would have replayed the original booking, registration or checkout instead of reaching this refusal at all, so this only fires on a genuine mismatch. The usual cause is a key that outlived the attempt it was minted for — reused across two different requests, or a form re-submitted with edited details under the same key by mistake. On the ticket-checkout route the first attempt's Checkout session is left exactly as it was: a mismatching caller is not its owner, so this refusal never touches it. Fix: mint a fresh key per attempt (see Retrying safely with Idempotency-Key), and only ever resend the identical body under a key you have already used.
{
"type": "https://bookdineplay.com/docs/api/errors/idempotency-key-reused",
"title": "Idempotency key reused",
"status": 409,
"detail": "This Idempotency-Key was already used for a different request.",
"traceId": "0HNOK3A31C723"
}checkout-in-progress
409. Ticket checkout only. A live Idempotency-Key claim exists for this request, but the Checkout session it is for has not finished being created yet — the seat hold is reserved before Stripe is ever called, so a byte-identical retry that lands in the brief gap while Stripe is still responding cannot yet be handed the checkout URL. Realistically only a genuinely concurrent resend (two requests in flight at once under the same key) can observe this; a sequential retry — the common case, made after the first attempt has actually returned — never does. Fix: wait a moment and retry with the same key; it will then either replay the completed checkout or, if the first attempt's Stripe call itself failed, start a fresh one.
{
"type": "https://bookdineplay.com/docs/api/errors/checkout-in-progress",
"title": "Checkout in progress",
"status": 409,
"detail": "A checkout session for this request is still being created. Please try again in a moment.",
"traceId": "0HNOK3A31C725"
}Billing, account and payout problems
Not about the request at all: the venue's BookDinePlay account, or its connected Stripe payout account, is what refuses.
tenant-suspended
403. The venue's account is not being served. Either its subscription lapsed (unpaid or cancelled), BookDinePlay suspended it, or a self-serve sign-up never finished paying — all of which answer the same problem, because none of them is something an integration or a guest can act on, and telling them apart would publish the venue's billing state to anyone who can reach the API. Every /api/venues/** route answers it, reads included; the per-credential routes a guest already holds a link for (GET /api/tickets/{code}, POST /api/event-registrations/{cancelToken}/cancel, GET /api/qr/{token} and the voucher lookup) keep working, so a printed QR code or an e-mailed ticket is never stranded by the venue's card expiring. Fix: nothing on your side — hide the booking widget and show the venue's phone number; the venue settles it in Billing and the route starts answering again within seconds.
{
"type": "https://bookdineplay.com/docs/api/errors/tenant-suspended",
"title": "Tenant suspended",
"status": 403,
"detail": "This venue isn't taking bookings right now. Please contact the venue directly.",
"traceId": "0HNOK3A31C71V"
}billing-managed-by-stripe
409. Only on PUT /api/admin/tenants/{id}: the tenant's plan and its status are owned by its Stripe subscription, so the admin surface will not hand-edit either — both are re-derived from the subscription on every sync and an edit here would be silently reverted by the next webhook. Sending the values the tenant already has is not a change, so editing an unrelated field still works. Fix: change the subscription (the operator's Change plan does exactly that), or switch the tenant to manual billing in the same request by sending billing: "Manual" — which stops the subscription governing the plan but does not cancel it at Stripe; the response's notice says so.
{
"type": "https://bookdineplay.com/docs/api/errors/billing-managed-by-stripe",
"title": "Billing managed by Stripe",
"status": 409,
"detail": "This tenant's plan and account status are managed by its Stripe subscription. Change the subscription, or switch the tenant to manual billing first.",
"traceId": "0HNOK3A31C71W"
}payouts-not-connected
409. The tenant's plan and the venue's own switches allow online payments, but the venue's Stripe Connect payout account is not Enabled yet — it never started onboarding, is mid-onboarding, or Stripe restricted it after a verification issue. Returned by POST /api/venues/{venueSlug}/payment-intents for a reservation deposit. The guest ticket-checkout route never answers this one directly: it folds the same cause into ticket-sales-not-enabled instead, so a guest never learns which of the three reasons applies — only the operator does, in the console's Billing → Payouts card. Fix: nothing on your side — skip the deposit step and treat the reservation as confirmed by the venue; the venue connects (or finishes connecting) its payout account in the console.
{
"type": "https://bookdineplay.com/docs/api/errors/payouts-not-connected",
"title": "Payouts not connected",
"status": 409,
"detail": "This venue has not connected a payout account yet.",
"traceId": "0HNOK3A31C71Y"
}connect-not-enabled
409. Returned by POST /api/operator/venue/payouts/onboarding-link when Stripe refuses to create the venue's Connect Standard account because the platform's own Stripe account has not signed up for Connect yet — nothing the venue's operator did, and nothing they can fix. A retry alone does not help either: the operator API mints a fresh idempotency key per attempt, but Stripe still refuses the same underlying prerequisite until the platform account enables Connect. Fix: nothing on the venue's side — this is a BookDinePlay platform configuration issue; contact support.
{
"type": "https://bookdineplay.com/docs/api/errors/connect-not-enabled",
"title": "Connect not enabled",
"status": 409,
"detail": "We can't connect Stripe accounts yet. This is on us — please try again later or contact support.",
"traceId": "0HNOK3A31C721"
}connect-rejected
409. Returned by the same onboarding-link route when Stripe accepts that Connect itself is enabled but refuses this venue's own account details — in practice the company's country, VAT id, or website URL shape. Unlike connect-not-enabled, this one the operator can act on directly. Fix: check the company profile under Billing → Company (country, VAT id, website), correct it, and press Connect again.
{
"type": "https://bookdineplay.com/docs/api/errors/connect-rejected",
"title": "Connect rejected",
"status": 409,
"detail": "Our payment provider could not accept these venue details. Please check the company's country, VAT ID and website, then try again.",
"traceId": "0HNOK3A31C722"
}Gift voucher problems
Every route that takes a voucher code answers with these: the voucher routes, the tab, deposit and ticket payments that take a voucherCode, and the console's voucher pages. A problem never carries the code. Several only ever reach the console; they are listed so that every type URL lands somewhere.
voucher-not-found
404. The code does not lead to a voucher that can be used here: mistyped, unknown, a purchase that was never paid, or a voucher of another venue. All of these give the same answer on purpose, so codes cannot be probed. Fix: ask the guest to check the code. The SDKs catch a typo before sending, because the last character is a check symbol.
voucher-not-redeemable
409. The voucher exists but cannot pay anything any more. reason says why: Expired, Voided or UsedUp, and detail says it in a sentence. Fix: show detail; the guest pays by other means.
{
"type": "https://bookdineplay.com/docs/api/errors/voucher-not-redeemable",
"title": "Voucher not redeemable",
"status": 409,
"detail": "This voucher has expired.",
"reason": "Expired",
"traceId": "0HNOK3A31C723"
}voucher-insufficient-balance
409. More was asked of the voucher than it has available; value held by a payment in progress counts as gone. At the table, staff asked for more than an amount voucher has left. Online, the payments take only what the voucher has and charge the rest to the card, so this means everything left is held by another payment in progress. Fix: redeem a smaller amount, or retry once the other payment has finished or lapsed.
voucher-not-applicable
409. This kind of voucher cannot pay this, or its currency is not the venue's. A deposit and tickets take only amount vouchers; percentage and item vouchers pay a bill. Fix: tell the guest the voucher is for the table, and continue without it.
voucher-busy
409. Another payment or till was using the same voucher at the same moment. Nothing was taken. Fix: retry the same request after a moment.
voucher-validity-not-set
409. The venue has not chosen how long its vouchers are valid, so it cannot issue or sell any yet. Fix: nothing on your side; the venue sets the validity in the console's voucher settings.
voucher-validity-too-short
400. A paid voucher must be valid for at least a year from its issue. The venue's validity rule, a hand-issued voucher or an expiry change would give it less. Fix: the venue chooses a longer validity.
voucher-sale-not-available
409. Vouchers cannot be bought online at this venue right now: online sale is off, the product is not sold online, payments are switched off, or the payout account cannot take money. The offers report the same in onlineSaleAvailable before you try. Fix: show the offers' "not available" state.
voucher-not-refundable
409. Console only: a voucher sale can be refunded only while the voucher is unused, sold online and not held by a payment in progress. Fix: settle a used voucher with the guest directly.
voucher-refund-failed
502. Console only: Stripe did not complete the refund of a voucher sale. Nothing was changed and the voucher is still valid. Fix: retry, or refund in Stripe directly.
voucher-undo-not-allowed
409. Console only: a redemption at the table can be undone once, and only within the undo window unless staff give a reason. Online redemptions are undone by refunding the payment instead.
voucher-hold-live
409. Console only: a payment in progress holds value on this voucher, so it cannot be voided or its sale refunded right now. Fix: retry once the guest has finished or abandoned the payment.
Platform-admin problems
Answered only by the platform-admin surface under /api/admin/** — not part of the public widget/SDK API, but part of the same reference (the same reason billing-managed-by-stripe above is documented here).
operator-already-exists
409. Returned by POST /api/admin/tenants/{id}/operators when the Auth0 subject or the e-mail address in the request already belongs to another operator row. A subject must resolve to exactly one operator (a duplicate has cost a platform admin their own console access before), and two rows sharing an e-mail is the same ambiguity from the other direction. Fix: reuse the existing row instead of creating a second one, or supply a different subject/e-mail if this genuinely is a different person.
{
"type": "https://bookdineplay.com/docs/api/errors/operator-already-exists",
"title": "Operator already exists",
"status": 409,
"detail": "An operator for this Auth0 subject already exists.",
"traceId": "0HNOK3A31C724"
}not-a-demo-venue
403. Returned by POST /api/admin/demo-venues/{venueSlug}/reset when venueSlug does not name one of BookDinePlay's own demo venues. This endpoint clears a venue's transactional data — reservations, table sessions, event registrations and tickets, guest conversations, and QR tokens — for release-validation passes; it is refused outright for anything else so it can never reach a real customer's data. 403, not 404: the slug may well name a real venue, and the point is that this route will never act on it.
{
"type": "https://bookdineplay.com/docs/api/errors/not-a-demo-venue",
"title": "Not a demo venue",
"status": 403,
"detail": "Only a demo venue's transactional data can be reset through this endpoint.",
"traceId": "0HNOK3A31C724"
}Problems without a reason code
These carry the generic RFC 9110 type; detail says what happened.
| Status | When | What to do |
|---|---|---|
| 400 | A field failed validation; errors lists the fields |
Fix the input; the field names are the request's property names |
| 400 | date is not yyyy-MM-dd, or a reservation's time or duration is not one the venue offers |
Send the values from the availability response unchanged |
| 404 | No venue with that slug; no reservation with that reference; the QR token or table session is not active; no published event with that slug; an unknown ticket code or cancel token | Check the slug; a revoked QR code stays 404 — print the new one; a draft event is 404 until the venue publishes it |
| 409 | The slot was taken between availability and reservation; the tab was just closed; the venue has not enabled online payments; a cancel link is used after a ticket of that registration was checked in | Re-query availability and offer the next slot; start a new session; skip the deposit step; tell the guest to ask the venue |
| 400 | A deposit payment's amount or currency is not exactly the booking's deposit |
Send the reservation's depositAmount and currency unchanged |
| 409 | A deposit payment for a booking that no longer waits for one — cancelled, confirmed by the venue, or finished (reason deposit-not-payable), or whose deposit is already paid (reason deposit-already-paid) |
Show the booking as it is; there is nothing to pay |
| 409 | A voucher would leave a card payment under Stripe's minimum (€0.50 in EUR) on a tab, a deposit or tickets (reason below-card-minimum) |
Pay without the voucher, or at the venue |
| 5xx | Ours | Retry with exponential backoff; if it persists, send the traceId to support |
Next steps
- Authentication — the same refusals from the key's point of view.
- API reference — the problem body format.
- Events — where the registration, checkout and refund problems come from.