Entwicklerdokumentation

QR-Codes und Tischsitzungen

QR-Token zu seinem Tisch auflösen, sehen, was der Gast bestellen kann und auf der Rechnung hat, bestellen und die Rechnung schließen – ohne API-Schlüssel.

Jeder Tisch, jede Bahn, jede Scheibe kann einen gedruckten QR-Code tragen. Wer ihn scannt, öffnet eine Seite mit einem Token in der URL; dieses Token ist das Zugangsmerkmal für alles auf dieser Seite – es identifiziert Location und Ressource, lässt sich in der Konsole widerrufen und neu drucken und braucht keinen zusätzlichen API-Schlüssel. Eine Tischsitzung ist die offene Rechnung des Gastes an dieser Ressource: Sie beginnt mit der ersten Bestellung und endet, wenn die Rechnung geschlossen wird.

Endpunkte

GET /api/qr/{token}

Worauf der QR-Code zeigt – genug, um „Tisch 7 im The Neon Tap“ anzuzeigen, bevor der Gast etwas tut.

Antwort 200 OK

Feld Typ Bedeutung
venueSlug, venueName string Die Location, für die weiteren Aufrufe und zur Anzeige
resourceCode, resourceName string Die Ressource (resourceCode ist ihre id in den Ressourcen der Location)
resourceType string Einer der Ressourcentypen
{
  "venueSlug": "demo-sportsbar",
  "venueName": "The Neon Tap",
  "resourceCode": "table-4a",
  "resourceName": "Table 4",
  "resourceType": "RestaurantTable"
}

Fehler – 404 das Token ist unbekannt, widerrufen oder seine Location ist nicht mehr für QR-Codes berechtigt.

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

Die Sicht des Gastes auf den Tisch: die Speisekarten, aus denen er hier bestellen darf, und die laufende Rechnung. Vor der ersten Bestellung ist die Sitzung noch nicht offen – isOpen ist false und tab leer –, so entscheidet eine Seite auch, ob sie „Bestellen“ oder „Ihre Rechnung“ zeigt.

Antwort 200 OK

Feld Typ Bedeutung
resourceCode, resourceName, resourceType string Der Tisch
isOpen boolean Ob eine Rechnung offen ist
currency string Währung jedes Betrags darunter
menus[] object[] Bestellbare Karten für diese Ressource: id, name, kind (Food oder Drinks), groups[] → items[] mit name, description, price, currency, taxRate (nullable), imageUrl (nullable). Ein Billardtisch bekommt ggf. nur die Getränkekarte, ein Restauranttisch beide
tab[] object[] Zusammengefasste Positionen über alle Bestellungen der Sitzung: itemName, quantity, unitPrice, lineTotal
runningTotal decimal Summe der Rechnung
paymentPending, paymentUrl boolean, string oder null Eine Online-Zahlung dieser Rechnung läuft, und der Checkout, den sie geöffnet hat – siehe POST …/payment
voucherCredits object[] oder null Geschenkgutscheine, die das Personal am Tisch auf diese Rechnung angewendet hat: label und amount. Null, wenn es keine gibt
amountDue decimal oder null runningTotal abzüglich voucherCredits, nie unter null – was noch zu zahlen ist. Null, wenn kein Gutschein angewendet wurde; dann ist runningTotal fällig
{
  "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
}

Listen im Beispiel sind auf drei Einträge gekürzt.

Fehler – 404 Token nicht aktiv.

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

Eine Bestellung aufgeben: eine oder mehrere Positionen, jede mit einer Karte per id und einer Position per name, wie sie in den menus der Sitzung stehen. Die erste Bestellung öffnet die Sitzung; jede Bestellung hält den Preis zum Bestellzeitpunkt auf der Rechnung fest. Die Antwort ist die aktualisierte Sitzung.

Request-Body (Content-Type: application/json)

Feld Erforderlich Bedeutung
lines[] ja, mindestens eine menuId (eine Karten-id aus der Sitzung), itemName (ein Positions-name in dieser Karte), quantity (positiv)

Antwort 200 OK – dieselbe Struktur wie GET …/session, jetzt mit den neuen Positionen auf 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
}

Fehler

Status Wann
400 lines fehlt oder ist leer (errors.lines); oder eine Position nennt eine Karte oder Position, die an dieser Ressource nicht bestellbar ist – nichts wird aufgegeben
404 Token nicht aktiv
409 Die Rechnung wurde zwischen dem Lesen des Gastes und dieser Bestellung geschlossen. Sitzung neu lesen; die nächste Bestellung öffnet eine neue Rechnung

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

Die Positionsnamen in den Beispielen sind illustrativ – verwenden Sie die Namen aus den menus der Sitzung.

POST /api/qr/{token}/close

Die Rechnung schließen. Die Antwort ist die Rechnung: jede Position, die Zwischensumme, die darin enthaltene Steuer und die Gesamtsumme. Die Sitzung endet; die nächste Bestellung des Gastes beginnt eine neue.

Antwort 200 OK

Feld Typ Bedeutung
lines[] object[] itemName, quantity, lineTotal
subtotal, taxTotal, total decimal Preise verstehen sich inklusive Steuer: total ist, was der Gast zahlt, taxTotal die darin enthaltene Steuer
currency string
issuedAt ISO-8601-Zeitpunkt Wann die Rechnung geschlossen wurde
{
  "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"
}

Fehler – 404 Token nicht aktiv oder keine offene Sitzung zum Schließen.

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

Lässt den Gast die Rechnung per Karte bezahlen statt an der Bar. Die Rechnung wird 31 Minuten lang auf ihre aktuelle Summe eingefroren und eine Stripe-Checkout-Sitzung auf dem Stripe-Konto der Location eröffnet; schicken Sie den Gast zu checkoutUrl. Solange das Einfrieren gilt, wird POST …/orders mit 409 abgelehnt – die abzubuchende Summe steht bereits fest – und GET …/session meldet paymentPending: true mit derselben paymentUrl, sodass ein zweites Gerät am Tisch die laufende Zahlung sieht statt eine weitere zu starten.

Ein erneuter Aufruf während einer laufenden Zahlung gibt dieselbe Checkout-Sitzung zurück, keine zweite. Die Rechnung selbst ist der Anspruch, das Telefon des Gastes braucht also keinen Idempotency-Key-Header. Bricht der Gast den Checkout ab, läuft das Einfrieren von selbst aus und die Rechnung nimmt wieder Bestellungen an.

Ist die Zahlung abgeschlossen, wird die Rechnung automatisch geschlossen – POST …/close muss nicht zusätzlich aufgerufen werden.

Dieser Aufruf braucht wie der Rest dieser Seite keinen API-Schlüssel. Im Unterschied dazu setzt er aber voraus, dass der Tarif der Location Online-Zahlungen umfasst, Zahlungen eingeschaltet sind und das Stripe-Auszahlungs-Onboarding abgeschlossen ist; bezahlen an der Bar ist immer möglich.

Ein Geschenkgutschein. Der Body ist optional. Senden Sie { "voucherCode": "…" }, und ein Betrags- oder Prozentgutschein dieser Location bezahlt, was er kann; die Karte bezahlt den Rest. Die Rechnungssumme ist, was nach einem vom Personal bereits angewendeten Gutschein noch offen ist, und ein Prozentgutschein ist ein Rabatt auf die ganze Rechnung, begrenzt auf den offenen Betrag. Der Wert des Gutscheins wird zusammen mit dem Einfrieren der Rechnung reserviert und abgebucht, wenn die Zahlung eintrifft; bricht der Gast ab, verfällt beides. Der Kartenteil beträgt mindestens 0,50 € (Mit einem Gutschein bezahlen). Ein Gutschein, der alles deckt, braucht keine Karte, also gilt keine der Kartenbedingungen oben: Die Rechnung ist sofort bezahlt und geschlossen, paidInFull ist true, und checkoutUrl ist die Rechnungsseite des Gasts. Eine Erstattung im Stripe-Dashboard der Location für eine teilweise per Gutschein bezahlte Rechnung bucht den Gutscheinteil auf den Gutschein zurück.

Antwort 200 OK

Feld Typ Bedeutung
checkoutUrl string Stripe-Checkout-URL – leiten Sie den Gast hierhin weiter
amount decimal Die eingefrorene Rechnungssumme, inkl. Steuer
currency string
expiresAt ISO-8601-Zeitpunkt Wann das Einfrieren ausläuft und der Checkout nicht mehr bezahlbar ist
voucherAmount decimal oder null Was der Gutschein bezahlt; null ohne Gutschein. amount ist dann, was die Karte bezahlt
paidInFull boolean Der Gutschein hat alles bezahlt: Es wurde keine Stripe-Sitzung geöffnet, und die Rechnung ist schon geschlossen
{
  "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3",
  "amount": 23.50,
  "currency": "EUR",
  "expiresAt": "2026-09-15T13:26:15.3387817+00:00"
}

Ein Gutschein, der die ganze Rechnung bezahlt hat:

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

Fehler

Status Wann
404 Token nicht aktiv
409 Nichts auf der Rechnung zu bezahlen; die Rechnung ist bereits bezahlt; eine Zahlung läuft schon, ihr Checkout konnte aber nicht zurückgegeben werden; oder die Location kann Rechnungen gerade nicht per Karte annehmen
404, 409 Mit einem Gutschein: eines der Gutschein-Probleme oder 409 mit reason below-card-minimum, wenn die Rechnung 0,50 € oder weniger beträgt und der Gutschein sie nicht deckt

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 }) // oder ganz ohne Body, um nur per Karte zu zahlen
})).json();
location.href = payment.checkoutUrl; // bei paidInFull die Rechnungsseite

C#

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

Nächste Schritte

  • Speisekarten und Ressourcen – die Anzeige-Karten und welche Ressourcen Bestellungen erlauben.
  • .NET-SDK – die fünf Aufrufe als typisierte Methoden; PlaceOrderAsync und StartTabPaymentAsync geben bei 400/404 null zurück und werfen bei 409 BookDinePlayApiException.