Developer docs

QR codes and table sessions

Resolve a printed QR token to its table, see what the guest can order and has on the tab, place orders, and close the tab into an invoice — no API key needed.

Every table, lane or board can carry a printed QR code. Scanning it opens a page with a token in the URL; that token is the credential for everything on this page — it identifies the venue and the resource, it can be revoked and reprinted from the console, and it needs no API key on top. A table session is the guest's open tab on that resource: it starts with the first order and ends when the tab is closed.

Endpoints

GET /api/qr/{token}

What the QR code points at — enough to show "Table 7 at The Neon Tap" before the guest does anything.

Response 200 OK

Field Type Meaning
venueSlug, venueName string The venue, for the other calls and for display
resourceCode, resourceName string The resource (resourceCode is its id on the venue's resources)
resourceType string One of the resource types
{
  "venueSlug": "demo-sportsbar",
  "venueName": "The Neon Tap",
  "resourceCode": "table-4a",
  "resourceName": "Table 4",
  "resourceType": "RestaurantTable"
}

Errors — 404 the token is unknown, revoked, or its venue is no longer entitled to QR codes.

curl

curl "https://api.bookdineplay.com/api/qr/<token>"

JavaScript

const token = new URL(location.href).searchParams.get('t');
const table = await (await fetch(`https://api.bookdineplay.com/api/qr/${encodeURIComponent(token)}`)).json();
document.title = `${table.resourceName} · ${table.venueName}`;

C#

var table = await client.ResolveQrTokenAsync(token, cancellationToken);

GET /api/qr/{token}/session

The guest's view of the table: the menus they may order from here and the running tab. Before the first order the session is not open yet — isOpen is false and tab is empty — so this is also how a page decides whether to show "Order" or "Your tab".

Response 200 OK

Field Type Meaning
resourceCode, resourceName, resourceType string The table
isOpen boolean Whether a tab is open
currency string Currency of every amount below
menus[] object[] Orderable menus for this resource: id, name, kind (Food or Drinks), groups[] → items[] with name, description, price, currency, taxRate (nullable), imageUrl (nullable). A billiard table may only get the drinks menu; a restaurant table both
tab[] object[] Aggregated lines across the session's orders: itemName, quantity, unitPrice, lineTotal
runningTotal decimal Sum of the tab
paymentPending, paymentUrl boolean, string or null An online payment of this tab is live, and the checkout it opened — see POST …/payment
voucherCredits object[] or null Gift vouchers staff applied to this tab at the table: label and amount. Null when there are none
amountDue decimal or null runningTotal less voucherCredits, never below zero — what is left to pay. Null when no voucher was applied, so runningTotal is what is due
{
  "resourceCode": "table-4a",
  "resourceName": "Table 4",
  "resourceType": "RestaurantTable",
  "isOpen": false,
  "currency": "EUR",
  "menus": [
    {
      "id": "kitchen",
      "name": "Kitchen",
      "kind": "Food",
      "groups": [
        {
          "name": "Small plates",
          "description": null,
          "items": [
            {
              "name": "Loaded nachos",
              "description": "Melted cheese, jalapeños, lime crema",
              "price": 9.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/nachos.webp"
            },
            {
              "name": "Buffalo wings",
              "description": "Blue-cheese dip, celery",
              "price": 11.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/wings.webp"
            },
            {
              "name": "Padrón peppers",
              "description": "Sea salt, olive oil",
              "price": 7.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/padron-peppers.webp"
            }
          ]
        },
        {
          "name": "Mains",
          "description": null,
          "items": [
            {
              "name": "Smash burger",
              "description": "Double patty, house sauce, fries",
              "price": 14.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/smash-burger.webp"
            },
            {
              "name": "BBQ ribs",
              "description": "Slow-cooked, slaw, fries",
              "price": 19.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/bbq-ribs.webp"
            },
            {
              "name": "Halloumi flatbread",
              "description": "Grilled halloumi, harissa, greens",
              "price": 13.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/halloumi-flatbread.webp"
            }
          ]
        },
        {
          "name": "Sweet",
          "description": null,
          "items": [
            {
              "name": "Churros",
              "description": "Cinnamon sugar, chocolate",
              "price": 7.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/churros.webp"
            }
          ]
        }
      ]
    },
    {
      "id": "bar",
      "name": "Bar",
      "kind": "Drinks",
      "groups": [
        {
          "name": "Draft beer",
          "description": null,
          "items": [
            {
              "name": "House lager 0.3L",
              "description": "Crisp pilsner",
              "price": 4.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/lager.webp"
            },
            {
              "name": "Local IPA 0.5L",
              "description": "Hoppy, citrus finish",
              "price": 6.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/ipa.webp"
            }
          ]
        },
        {
          "name": "Cocktails",
          "description": null,
          "items": [
            {
              "name": "Old Fashioned",
              "description": "Bourbon, bitters, orange",
              "price": 11.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/old-fashioned.webp"
            },
            {
              "name": "Spicy margarita",
              "description": "Tequila, chilli, lime",
              "price": 10.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/margarita.webp"
            }
          ]
        },
        {
          "name": "Zero proof",
          "description": null,
          "items": [
            {
              "name": "Craft soda",
              "description": "House-made, ask for today's flavour",
              "price": 4.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/craft-soda.webp"
            }
          ]
        }
      ]
    }
  ],
  "tab": [],
  "runningTotal": 0
}

Lists in the example are shortened to three entries.

Errors — 404 token not active.

curl

curl "https://api.bookdineplay.com/api/qr/<token>/session"

JavaScript

const session = await (await fetch(`https://api.bookdineplay.com/api/qr/${encodeURIComponent(token)}/session`)).json();
const drinks = session.menus.find(m => m.kind === 'Drinks');

C#

var session = await client.GetTableSessionAsync(token, cancellationToken);

POST /api/qr/{token}/orders

Place an order: one or more lines, each naming a menu by id and an item by name as they appear in the session's menus. The first order opens the session; every order snapshots the price at the time of ordering onto the tab. The response is the updated session.

Request body (Content-Type: application/json)

Field Required Meaning
lines[] yes, at least one menuId (a menu id from the session), itemName (an item name in that menu), quantity (positive)

Response 200 OK — the same shape as GET …/session, now with the new lines on tab.

{
  "resourceCode": "table-4a",
  "resourceName": "Table 4",
  "resourceType": "RestaurantTable",
  "isOpen": true,
  "currency": "EUR",
  "menus": [
    {
      "id": "kitchen",
      "name": "Kitchen",
      "kind": "Food",
      "groups": [
        {
          "name": "Small plates",
          "description": null,
          "items": [
            {
              "name": "Loaded nachos",
              "description": "Melted cheese, jalapeños, lime crema",
              "price": 9.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/nachos.webp"
            },
            {
              "name": "Buffalo wings",
              "description": "Blue-cheese dip, celery",
              "price": 11.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/wings.webp"
            },
            {
              "name": "Padrón peppers",
              "description": "Sea salt, olive oil",
              "price": 7.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/padron-peppers.webp"
            }
          ]
        },
        {
          "name": "Mains",
          "description": null,
          "items": [
            {
              "name": "Smash burger",
              "description": "Double patty, house sauce, fries",
              "price": 14.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/smash-burger.webp"
            },
            {
              "name": "BBQ ribs",
              "description": "Slow-cooked, slaw, fries",
              "price": 19.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/bbq-ribs.webp"
            },
            {
              "name": "Halloumi flatbread",
              "description": "Grilled halloumi, harissa, greens",
              "price": 13.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/halloumi-flatbread.webp"
            }
          ]
        },
        {
          "name": "Sweet",
          "description": null,
          "items": [
            {
              "name": "Churros",
              "description": "Cinnamon sugar, chocolate",
              "price": 7.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/churros.webp"
            }
          ]
        }
      ]
    },
    {
      "id": "bar",
      "name": "Bar",
      "kind": "Drinks",
      "groups": [
        {
          "name": "Draft beer",
          "description": null,
          "items": [
            {
              "name": "House lager 0.3L",
              "description": "Crisp pilsner",
              "price": 4.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/lager.webp"
            },
            {
              "name": "Local IPA 0.5L",
              "description": "Hoppy, citrus finish",
              "price": 6.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/ipa.webp"
            }
          ]
        },
        {
          "name": "Cocktails",
          "description": null,
          "items": [
            {
              "name": "Old Fashioned",
              "description": "Bourbon, bitters, orange",
              "price": 11.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/old-fashioned.webp"
            },
            {
              "name": "Spicy margarita",
              "description": "Tequila, chilli, lime",
              "price": 10.50,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/margarita.webp"
            }
          ]
        },
        {
          "name": "Zero proof",
          "description": null,
          "items": [
            {
              "name": "Craft soda",
              "description": "House-made, ask for today's flavour",
              "price": 4.00,
              "currency": "EUR",
              "taxRate": 0.19,
              "imageUrl": "_content/BookDinePlay.App.Shared/menu/craft-soda.webp"
            }
          ]
        }
      ]
    }
  ],
  "tab": [
    {
      "itemName": "Loaded nachos",
      "quantity": 2,
      "unitPrice": 9.50,
      "lineTotal": 19.00
    },
    {
      "itemName": "House lager 0.3L",
      "quantity": 1,
      "unitPrice": 4.50,
      "lineTotal": 4.50
    }
  ],
  "runningTotal": 23.50
}

Errors

Status When
400 lines is missing or empty (errors.lines); or a line names a menu or item that is not orderable at this resource — nothing is placed
404 Token not active
409 The tab was closed between the guest's read and this order. Re-read the session; the next order opens a new tab

curl

curl -X POST "https://api.bookdineplay.com/api/qr/<token>/orders" \
  -H "Content-Type: application/json" \
  -d '{ "lines": [ { "menuId": "kitchen", "itemName": "Smash burger", "quantity": 2 }, { "menuId": "bar", "itemName": "House lager", "quantity": 1 } ] }'

JavaScript

const updated = await (await fetch(`https://api.bookdineplay.com/api/qr/${encodeURIComponent(token)}/orders`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ lines: [{ menuId: 'kitchen', itemName: 'Smash burger', quantity: 2 }] })
})).json();
console.log(updated.runningTotal, updated.currency);

C#

var updated = await client.PlaceOrderAsync(token, new PlaceOrderRequest([
    new OrderLineRequest("kitchen", "Smash burger", 2),
    new OrderLineRequest("bar", "House lager", 1)
]), cancellationToken);

The item names in the samples are illustrative — use the names from the session's menus.

POST /api/qr/{token}/close

Close the tab. The response is the invoice: every line, the subtotal, the tax included in it, and the total. The session ends; the guest's next order starts a fresh one.

Response 200 OK

Field Type Meaning
lines[] object[] itemName, quantity, lineTotal
subtotal, taxTotal, total decimal Prices are tax-inclusive: total is what the guest pays, taxTotal the tax contained in it
currency string
issuedAt ISO 8601 instant When the tab was closed
{
  "lines": [
    {
      "itemName": "Loaded nachos",
      "quantity": 2,
      "lineTotal": 19.00
    },
    {
      "itemName": "House lager 0.3L",
      "quantity": 1,
      "lineTotal": 4.50
    }
  ],
  "subtotal": 19.75,
  "taxTotal": 3.75,
  "total": 23.50,
  "currency": "EUR",
  "issuedAt": "2026-09-15T12:55:15.3387817+00:00"
}

Errors — 404 token not active, or no open session to close.

curl

curl -X POST "https://api.bookdineplay.com/api/qr/<token>/close"

JavaScript

const invoice = await (await fetch(`https://api.bookdineplay.com/api/qr/${encodeURIComponent(token)}/close`, { method: 'POST' })).json();
console.log(invoice.total, invoice.currency);

C#

var invoice = await client.CloseTableSessionAsync(token, cancellationToken);

POST /api/qr/{token}/payment

Let the guest pay the tab by card instead of at the bar. The tab is frozen at its current total for 31 minutes and a Stripe Checkout session is opened on the venue's own Stripe account; send the guest to checkoutUrl. While the freeze is live, POST …/orders is refused with 409 — the amount being collected is already fixed — and GET …/session reports paymentPending: true with the same paymentUrl, so a second device at the table sees the payment in progress rather than starting another one.

Calling it again while a payment is live returns the same checkout, not a second one. The tab itself is the claim, so the guest's phone needs no Idempotency-Key header. If the guest abandons Checkout, the freeze lapses on its own and the tab takes orders again.

When the payment completes, the tab is closed automatically — there is no need to call POST …/close as well.

This call needs no API key, like the rest of this page. Unlike them it does require the venue to be on a plan that includes online payments, to have payments switched on, and to have finished Stripe payout onboarding; a guest can always pay at the bar instead.

A gift voucher. The body is optional. Send { "voucherCode": "…" } and an amount or percentage voucher of this venue pays what it can; the card pays the rest. The tab total is what is still due after any voucher staff already applied, and a percentage voucher is a discount on the whole bill, capped at what is due. The voucher's value is held together with the tab's freeze and taken when the payment lands; if the guest gives up, both lapse. The card part is at least €0.50 (Paying with a voucher). A voucher that covers everything needs no card, so none of the card conditions above apply: the tab is settled and closed at once, paidInFull is true, and checkoutUrl is the guest's tab page. A refund in the venue's Stripe dashboard of a tab paid partly by voucher puts the voucher part back on the voucher.

Response 200 OK

Field Type Meaning
checkoutUrl string Stripe Checkout URL — redirect the guest here
amount decimal The frozen tab total, tax included
currency string
expiresAt ISO 8601 instant When the freeze lapses and the checkout stops being payable
voucherAmount decimal or null What the voucher pays; null without one. amount is then what the card pays
paidInFull boolean The voucher paid everything: no Stripe session was opened and the tab is already closed
{
  "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3",
  "amount": 23.50,
  "currency": "EUR",
  "expiresAt": "2026-09-15T13:26:15.3387817+00:00"
}

A voucher that paid the whole tab:

{
  "checkoutUrl": "https://app.bookdineplay.com/t/table-4a-token?paid=1",
  "amount": 0.00,
  "currency": "EUR",
  "expiresAt": "2026-10-09T13:26:15.3387817+00:00",
  "voucherAmount": 23.50,
  "paidInFull": true
}

Errors

Status When
404 Token not active
409 Nothing on the tab to pay; the tab is already paid; a payment is already live but its checkout could not be handed back; or the venue cannot take card payments for tabs right now
404, 409 With a voucher: one of the gift voucher problems, or 409 with reason below-card-minimum when the tab is €0.50 or less and the voucher does not cover it

curl

curl -X POST "https://api.bookdineplay.com/api/qr/<token>/payment" \
  -H "Content-Type: application/json" \
  -d '{ "voucherCode": "K7QM-2XRP-9DTA-HV3N-8W4F" }'

JavaScript

const payment = await (await fetch(`https://api.bookdineplay.com/api/qr/${encodeURIComponent(token)}/payment`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ voucherCode }) // or no body at all to pay by card only
})).json();
location.href = payment.checkoutUrl; // the tab page when paidInFull

C#

var payment = await client.StartTabPaymentAsync(token, voucherCode, cancellationToken); // voucherCode may be null

Next steps

  • Menus and Resources — the display menus and which resources allow ordering.
  • .NET SDK — the five calls as typed methods; PlaceOrderAsync and StartTabPaymentAsync return null on a 400/404 and throw BookDinePlayApiException on a 409.