Developer docs

Vouchers

Sell gift vouchers online, look one up by its code without a key, and let guests pay a tab, a deposit or tickets with one.

A venue sells gift vouchers through these routes, and guests spend them at the table or online. Three kinds exist: an amount (spent in parts until the balance is used up), a percentage off one bill, and one menu item. The venue sets what it sells, the price and how long a voucher stays valid; the console issues vouchers by hand on every plan, and selling them online needs Premium.

Two routes below need your key, like every /api/venues/** route: the offers and the checkout. The third, the lookup by code, takes no key: whoever holds a voucher's code holds the voucher. Paying with a voucher needs no route of its own — the tab, deposit and ticket payments take an optional voucher code (Paying with a voucher).

The code is a credential

A voucher code is 20 characters, shown in five groups (K7QM-2XRP-9DTA-HV3N-8W4F). The last character is a check symbol, so a typo is caught before any request; the SDKs check it for you. Anyone who has the code can see the voucher's value and spend it, exactly like a gift card. So:

  • Never log it, and never put it in a URL of your own — only the lookup below carries it in its path, and BookDinePlay strips it from its own logs and traces.
  • Send it in the request body everywhere else: the tab payment, the deposit payment and the ticket checkout all take it as voucherCode.
  • Do not show a code you did not receive from the guest. The code exists only once the voucher is paid; it reaches the buyer or the recipient by e-mail, never through the checkout response.
  • Expect one answer for "no". A mistyped, unknown, unpaid or other venue's code is the same 404 voucher-not-found, so codes cannot be probed.

The reference (GV-…) a checkout returns is not a credential: it identifies the purchase for support and can be logged.

Endpoints

GET /api/vouchers/{code}

Keyless. code is the voucher code in any form a guest might type or scan: the display form (K7QM-2XRP-9DTA-HV3N-8W4F), the 20 symbols without dashes, or either in lower case. A full guest-page URL is not accepted here: take its last segment. Send it percent-encoded as one path segment, and keep it out of your own logs — it is a bearer credential. Not plan-gated: a voucher issued while the venue's plan allowed it keeps working after a downgrade.

Response 200 OK

Field Type Meaning
venueSlug, venueName string The venue that issued the voucher
displayCode string The code in its display form
kind string Amount, Percentage or Item
status string Active, PartlyUsed, UsedUp, Expired or Voided
currency string ISO 4217 currency of every money field
balance number or null What an Amount voucher can still pay; null for the other kinds
faceValue number or null The value an Amount voucher was issued with
percentage, maxDiscount number or null A Percentage voucher's discount and its optional cap in money
itemName string or null An Item voucher's menu item
expiresOn string The last valid day, venue-local, yyyy-MM-dd
terms string or null The venue's voucher terms as they were at issue
recipientName, giftMessage string or null What the buyer wrote for the recipient
guestUrl string The guest page of this voucher
qrSvg string An SVG QR code of guestUrl

The answer never carries the voucher's history or the buyer's name or e-mail address. balance is what can be spent right now: value held by a payment in progress is not counted.

Errors. 404 with the problem type https://bookdineplay.com/docs/api/errors/voucher-not-found for every code that does not resolve to an issued voucher — mistyped, unknown, or a sale that was never paid. An expired or voided voucher is not an error: it answers 200 with that status. The answer is the same for all of them on purpose, so codes cannot be probed. 429 with a Retry-After header when one client asks more than 30 times a minute — this is the one rate-limited route of the API, because there is no key to count against (Rate limits).

curl

curl "https://api.bookdineplay.com/api/vouchers/K7QM-2XRP-9DTA-HV3N-8W4F"

JavaScript

const voucher = await client.voucher(code); // sends no key; null when unknown or mistyped (no request at all for a mistyped code)

C#

var voucher = await client.GetVoucherAsync(code, cancellationToken); // sends no key; null when unknown

GET /api/venues/{venueSlug}/vouchers/offers

What the venue sells online right now. Answers on every plan: when vouchers cannot be bought online, onlineSaleAvailable is false, offers is empty and unavailableReason says why. Titles, terms and the validity sentence follow the request's Accept-Language.

Response 200 OK

Field Type Meaning
venueSlug, venueName, currency string The venue and the currency of every price
onlineSaleAvailable boolean Whether a checkout can be started
unavailableReason string or null not-offered, validity-not-set, validity-too-short, payments-off, not-in-plan or payouts-not-connected
offers array One entry per product: productId, kind, title, price (null when the buyer picks the amount), customAmount, minAmount, maxAmount, itemName
terms string or null The venue's voucher terms
validityDescription string or null How long a voucher bought today is valid

Show a guest the same sentence for every unavailableReason, with at most not-offered getting its own: the other reasons are the venue's setup, not something a guest can act on. The voucher shop does exactly that.

curl

curl "https://api.bookdineplay.com/api/venues/your-venue/vouchers/offers" \
  -H "X-BookDinePlay-Key: bdp_pk_your_publishable_key" \
  -H "Origin: https://www.your-venue.example"

JavaScript

const offers = await client.voucherOffers();
if (!offers.onlineSaleAvailable) showNotAvailable(offers.unavailableReason);

C#

var offers = await client.GetVoucherOffersAsync("your-venue", cancellationToken); // null when the venue is unknown

POST /api/venues/{venueSlug}/vouchers/checkout

Buy one voucher: opens a Stripe Checkout session on the venue's own connected Stripe account and returns the URL to send the buyer to. Needs the Premium plan, online payments switched on and a payout account that can sell. The voucher's code does not exist yet: it is minted once the payment is confirmed and reaches the buyer, or the recipient, by e-mail. Send an Idempotency-Key header so a retried request replays the same checkout instead of opening a second one; see Idempotency problems. The .NET SDK and the voucher shop send one for you; with client.checkoutVoucher pass { idempotencyKey } yourself — without it the JavaScript client sends none and a retry can open a second checkout.

Request body (Content-Type: application/json)

Field Required Meaning
productId yes An offer's productId
amount custom amounts The value to buy, within the offer's minAmount and maxAmount
buyerName, buyerEmail yes Who pays
recipientName, recipientEmail, giftMessage no Who the voucher is for; the message is at most 500 characters
deliverOn no yyyy-MM-dd, venue-local, today up to a year ahead: the day the recipient's e-mail goes out. Needs recipientEmail
language no en or de; defaults to the request's Accept-Language, then the venue's own language
successUrl, cancelUrl no Where Checkout sends the buyer. Must be https and on this API key's origin allowlist or the platform's own guest app; omitted uses the built-in thank-you page

Response 201 Created: reference (GV-…, display only), checkoutUrl (send the buyer here) and expiresAt (when the Checkout session stops accepting payment).

A paid voucher is valid for at least a year from the day it is issued. A sale whose payment never arrives creates no voucher; it is deleted 30 days after its checkout expired.

Errors

Status When
400 A field fails validation (errors names it), a return URL is refused, or the venue's validity rule gives a paid voucher less than a year (voucher-validity-too-short)
400 errors["Idempotency-Key"]: the key is longer than 200 characters
403 feature-not-in-plan: online sale needs Premium
404 No venue with that slug
409 voucher-sale-not-available (online sale off, product not sold online, payments off, or no payout account that can sell), or voucher-validity-not-set
409 idempotency-key-reused or checkout-in-progress

curl

curl -X POST "https://api.bookdineplay.com/api/venues/your-venue/vouchers/checkout" \
  -H "X-BookDinePlay-Key: bdp_pk_your_publishable_key" \
  -H "Origin: https://www.your-venue.example" \
  -H "Idempotency-Key: 4f9c2a1e8b7d4c3f9e0a1b2c3d4e5f60" \
  -H "Content-Type: application/json" \
  -d '{ "productId": "3f2a1b4c5d6e4f708192a3b4c5d6e7f8", "buyerName": "Ben Buyer", "buyerEmail": "ben@example.com", "recipientName": "Ada" }'

JavaScript

const checkout = await client.checkoutVoucher(
  { productId, buyerName: 'Ben Buyer', buyerEmail: 'ben@example.com', recipientName: 'Ada' },
  { idempotencyKey: attemptKey } // one crypto.randomUUID() per attempt, reused when you retry it
);
window.location.assign(checkout.checkoutUrl);

C#

var checkout = await client.StartVoucherCheckoutAsync("your-venue",
    new StartVoucherCheckoutRequest { ProductId = productId, BuyerName = "Ben Buyer", BuyerEmail = "ben@example.com", RecipientName = "Ada" },
    idempotencyKey: attemptKey, cancellationToken);

Paying with a voucher

A guest spends a voucher online by adding its code to a payment they are making anyway. The three payments take an optional voucherCode in their body; leaving it out changes nothing.

Payment Field Kinds accepted What comes back
A table tab, /api/qr/{token}/payment voucherCode Amount, Percentage voucherAmount, paidInFull
A reservation deposit, /api/venues/{venueSlug}/payment-intents voucherCode Amount voucherAmount, cardAmount
Event tickets, /api/venues/{venueSlug}/events/{eventSlug}/tickets/checkout voucherCode Amount voucherAmount, amountDue

The same rules hold for all three:

  • The voucher pays first, the card pays the rest. The value is held while the guest is on Stripe's page, so it cannot be spent twice; if the guest gives up, the hold lapses on its own.
  • The card pays at least Stripe's minimum — €0.50 in EUR; the minimum is Stripe's own per currency. Stripe refuses smaller card payments. When the rest would be between €0.01 and €0.49, the voucher pays a little less so the card pays exactly €0.50, and the voucher keeps the difference: a €50 voucher on a €50.30 tab pays €49.80. When the whole amount is €0.50 or less and the voucher does not cover it, the voucher cannot be used for it: 409 with reason below-card-minimum, on all three payments.
  • A voucher that covers everything opens no payment page. The payment completes at once, and checkoutUrl is the success page — so a client that simply follows checkoutUrl works either way. Check paidInFull, cardAmount or amountDue to skip the redirect.
  • A refund gives the voucher part back to the voucher. Only the card part goes back to the card. An expired voucher is extended by 30 days from the refund, so the guest can still use what came back. A voucher the venue has voided stays void: the refund is recorded on it, but the venue settles that value with the guest directly.
  • A percentage voucher is a discount on the whole bill, applied before any amount voucher and capped at what is due. It is used once.
  • No plan gate. Paying with a voucher and refunding it work on every plan. A payment's own conditions still apply — a tab needs ordering, a deposit needs deposits, tickets need ticket sales — and the card part needs online payments, as without a voucher.
  • The same problems everywhere. A code is refused with one of the gift voucher problems, whichever payment it was given to.

Item vouchers are redeemed at the table by staff, who pick the line on the bill they pay for.

Next steps

  • JavaScript vouchers — the voucher shop, the balance check and the voucher fields in the widgets.
  • .NET SDK — the typed voucher calls.
  • Errors — every voucher problem and what to show.