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
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 |
{
"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);