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"
}

Errors404 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
{
  "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.

Errors404 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"
}

Errors404 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);

Next steps

  • Venue — the display menus and which resources allow ordering.
  • .NET SDK — the four calls as typed methods; PlaceOrderAsync returns null on a 400/404 rather than throwing.