Vai al contenuto
v26.3

Eigenen Webshop an DiKAS anbinden

Diese Seite richtet sich an Webagenturen und Entwickler, die für einen Betrieb einen eigenen Webshop (zum Beispiel mit Node.js und React) mit der DiKAS-Kasse verbinden. Sie zeigt die einzelnen Möglichkeiten, von der Speisekarte über die Bestellung bis zum Artikel-Update, und welche Sie wofür wählen.

Alle Aufrufe gehen an die Kasse des Betriebs (https://<betrieb>.<ihre-domain>) und verwenden ausschließlich die aktuelle Schnittstelle unter /api/v1/…. Die ältere Schnittstelle unter /rest/… ist für neue Anbindungen nicht vorgesehen.

Nachschlagen

Alle Aufrufe mit Anmeldung, Feldern, Fehlercodes und Beispielen stehen in der API-Referenz zur Webshop-Anbindung; sie gibt es auch als OpenAPI-Datei für Postman oder einen Client-Generator.

Voraussetzungen im Betrieb

  • Die Lizenz enthält das Modul Online-Bestellung (für Lieferung zusätzlich Lieferservice).
  • Unter Admin → Einstellungen → Betrieb ist Online-Bestellung aktiviert (Einrichtung des Shops: Online-Shop). Sonst antworten die Aufrufe mit 404 SWITCH_DISABLED.
  • Es gibt mindestens eine Warengruppe mit Artikeln, siehe Artikel anlegen.

Welche Möglichkeit passt?

Ich möchte … Möglichkeit Anmeldung
keinen eigenen Code schreiben Der Online-Shop von DiKAS keine
die Speisekarte aus DiKAS in meinem Shop anzeigen Karte lesen keine, öffentlich
die Karte in meinem Shop pflegen und nach DiKAS übertragen Artikel schreiben Benutzer-Anmeldung
Bestellungen aus meinem Shop an die Kasse übergeben Bestellung übergeben keine; Bestell-Geheimnis je Bestellung
dem Gast „angenommen“, „fertig“ anzeigen Status zurückmelden Bestell-Geheimnis oder Partner-Schlüssel
Online bezahlen lassen Bezahlen je Zahlart

Der Ablauf einer Bestellung, die aus einem eigenen Shop kommt:

  1. Der Shop zeigt die Karte (aus DiKAS gelesen oder vom Shop geführt).
  2. Der Gast bestellt im Shop; der Shop übergibt die Bestellung an die Kasse.
  3. Die Kasse legt einen Auftrag im Lieferservice an und druckt ihn bzw. zeigt ihn auf dem Küchenmonitor. Der Betrieb nimmt an oder lehnt ab (oder die Kasse nimmt automatisch an).
  4. Der Shop erfährt den Stand: durch Abfragen, über einen Webhook oder über den Ereignis-Feed.

Zugänge im Überblick

Zugang Wofür Wo ausgestellt
ohne Anmeldung Karte lesen, Bestellung abgeben, Status einer Bestellung mit Bestell-Geheimnis –
Partner-Schlüssel (X-Dikas-Api-Key: dk1_…) Server-zu-Server: offene und heutige Bestellungen lesen (orders:read), annehmen/ablehnen/Status setzen (orders:manage), Tische lesen (tables:read), Ereignis-Feed lesen (events:read) Einstellungen → System → Partner-Schlüssel (Administrator)
Benutzer-Anmeldung (Bearer) Artikel und Gruppen pflegen, alles, was ein Kassenbenutzer darf POST /api/v1/auth/login, Benutzer mit nur den nötigen Rechten

Ein Partner-Schlüssel gehört zu einem Betrieb und zu einem technischen Benutzer (aktiv, weder Administrator noch Manager). Er hat ein Ablaufdatum (Standard 365 Tage, höchstens 400), lässt sich erneuern (das alte Geheimnis gilt noch 24 Stunden) und sofort widerrufen. Er wird einmal angezeigt, speichern Sie ihn sofort. Gesendet wird er in der Kopfzeile X-Dikas-Api-Key, nicht als Authorization: Bearer. Alles, was nicht zu seinen Bereichen gehört, lehnt die Kasse ab, auch Schlüssel, Benutzer und Einstellungen.

So kommen Sie an den Zugang

Die Beispiele verwenden curl und jq; API ist die Adresse der Kasse mit /api/v1. Jede Antwort der Kasse hat dieselbe Hülle: success, data, errorCode, errorMessage.

1. Benutzer-Anmeldung (Bearer-Token). Anmelden mit Benutzername und Passwort:

API=https://mein-betrieb.example.de/api/v1
curl -sS -X POST "$API/auth/login" -H 'Content-Type: application/json' \
  -d "{\"username\":\"$BENUTZER\",\"password\":\"$PASSWORT\"}"

Die Antwort enthält den Token im Feld data.accessToken:

{
  "success": true,
  "data": {
    "success": true,
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "refreshToken": "xV3k…",
    "expiresAt": "2026-11-10T23:08:00Z",
    "requiresTotp": false
  },
  "errorCode": null,
  "errorMessage": null
}

Den Token nehmen Sie mit jq heraus und senden ihn bei jedem weiteren Aufruf im Kopf Authorization: Bearer …:

TOKEN=$(curl -sS -X POST "$API/auth/login" -H 'Content-Type: application/json' \
  -d "{\"username\":\"$BENUTZER\",\"password\":\"$PASSWORT\"}" | jq -r .data.accessToken)
curl -sS "$API/articles" -H "Authorization: Bearer $TOKEN"

Der Token läuft zum Zeitpunkt expiresAt ab; mit dem refreshToken holen Sie einen neuen, siehe REST API. Ist requiresTotp wahr, hat der Benutzer die Zwei-Faktor-Anmeldung eingeschaltet. Für einen Server-Zugang legen Sie dafür einen eigenen technischen Benutzer ohne diese Pflicht an.

2. Partner-Schlüssel (Server zu Server). Ein Administrator stellt ihn unter Einstellungen → System → Partner-Schlüssel aus, oder per Schnittstelle (Anmeldung als Administrator mit Token wie oben, apiUserId ist die Kennung des technischen Benutzers aus GET /api/v1/staff):

curl -sS -X POST "$API/partner-api-keys" -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Webshop","scopes":["orders:read","orders:manage","events:read"],"apiUserId":"usr_9bf8f71a2db5","expiresInDays":365}'
{
  "success": true,
  "data": {
    "key": "dk1_bae9d9de99992c14.Q2vP…",
    "id": "pak_bae9d9de99992c14",
    "keyId": "bae9d9de99992c14",
    "name": "Webshop",
    "scopes": ["orders:read", "orders:manage", "events:read"],
    "apiUserId": "usr_9bf8f71a2db5",
    "expiresAt": "2027-11-10T17:48:22Z",
    "status": "active"
  }
}

Das Feld data.key ist der Partner-Schlüssel. Er steht nur in dieser einen Antwort: Speichern Sie ihn sofort, später ist er nicht mehr abrufbar (nur erneuern). Gesendet wird er in der Kopfzeile X-Dikas-Api-Key, nicht als Bearer:

PARTNER_SCHLUESSEL='dk1_bae9d9de99992c14.Q2vP…'
curl -sS "$API/online-checkout/today" -H "X-Dikas-Api-Key: $PARTNER_SCHLUESSEL" | jq -c '.data[] | {id, orderNumber, status, isPaid}'
{"id":"olord_4c1c808725b8","orderNumber":"ON-20261110-006","status":1,"isPaid":false}

Ein falscher, abgelaufener oder widerrufener Schlüssel antwortet mit 401 („Ungültiger Schlüssel“). Der Schlüssel öffnet nur die Bereiche, die Sie ihm gegeben haben.

3. Bestell-Geheimnis. Dritter Zugang ohne Anmeldung: Jede Bestellung bekommt beim Anlegen ein eigenes Geheimnis, siehe Bestellung übergeben.

A. Der Online-Shop von DiKAS

Wenn Sie keinen eigenen Shop programmieren müssen: DiKAS bringt einen fertigen Online-Shop mit (Karte, Warenkorb, Abholung und Lieferung, Zahlung, Bestellstatus). Er wird in der Verwaltung eingerichtet, siehe Online-Shop. Für viele Betriebe ist das der schnellste Weg. Die folgenden Möglichkeiten sind für Betriebe, die einen eigenen Shop behalten wollen.

B. Speisekarte und Öffnungsstatus lesen

Die Karte ist öffentlich und enthält nur Artikel, die online sichtbar sind (ausgeblendete Gruppen und Artikel fehlen), mit Preisen, Optionen, Allergenen und Bildern. Ein Artikel mit soldOut: true steht auf der Karte, wird aber nicht angenommen.

curl -sS "$API/online-menu/menu" | jq '.data.groups[] | {name, artikel: [.articles[] | {id, name, price, soldOut}]}'
curl -sS "$API/online-menu/status"        | jq -c '.data | {isOpen, currentCloseTime}'
curl -sS "$API/online-menu/wait-estimate" | jq -c '.data | {pickupMinutes, deliveryMinutes, load}'
{"name":"Pizza","artikel":[{"id":"art_917c19d4a18a","name":"Pizza Margherita","price":9.5,"soldOut":false}]}
{"isOpen":true,"currentCloseTime":"22:00"}
{"pickupMinutes":15,"deliveryMinutes":60,"load":"low"}

Die Karte darf nie hinter den Preisen der Kasse herlaufen

Beim Bestellen rechnet die Kasse mit ihren Preisen. Zeigt Ihr Shop einen alten Preis, stimmt die Summe im Warenkorb nicht mit der Rechnung überein. Laden Sie die Karte deshalb bei jedem Aufruf der Startseite oder höchstens wenige Minuten zwischengespeichert.

C. Artikel und Preise aus dem Shop pflegen

Pflegt der Shop die Karte, übertragen Sie Änderungen per Benutzer-Anmeldung an DiKAS. Legen Sie vorher fest, wer führend ist: Pflegt der Betrieb dieselben Artikel auch in der Kasse, laufen Preise auseinander.

Warengruppen sind hierarchisch nummeriert: eine Hauptgruppe hat 3 Ziffern (100), jede Untergruppe hängt zwei Ziffern an (10001). Artikel erscheinen nur in Untergruppen.

curl -sS -X POST "$API/articles/groups" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"groupId":"10001","name":"Pizza","sortOrder":1,"isActive":true}'

GRUPPE=$(curl -sS "$API/articles/groups" -H "Authorization: Bearer $TOKEN" \
  | jq -r '.data[] | select(.groupId=="10001") | .id')          # artgrp_…
curl -sS -X POST "$API/articles" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "{
  \"articleNumber\":\"1001\", \"name\":\"Pizza Margherita\", \"description\":\"Tomate, Mozzarella\",
  \"groupId\":\"$GRUPPE\", \"price\":9.5, \"taxRate\":7, \"isActive\":true }" | jq -c '.data | {id, name, price}'

Leere Karte trotz Artikeln?

Das Feld groupId am Artikel muss die Kennung der Gruppe tragen (artgrp_…), nicht die Gruppennummer. Artikel in einer Hauptgruppe erscheinen nie.

Ändern Sie Artikel als JSON Patch (Content-Type: application/json-patch+json), nur die Felder, die sich ändern:

curl -sS -X PATCH "$API/articles/art_917c19d4a18a" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json-patch+json' \
  -d '[{"op":"replace","path":"/price","value":10.5},{"op":"replace","path":"/manualSoldOut","value":true}]' \
  | jq -c '.data | {price, manualSoldOut}'
{"price":10.5,"manualSoldOut":true}
Was Sie wollen Feld Wirkung im Shop
Preis ändern /price neuer Preis in der Karte, die Kasse rechnet damit
Heute nicht verfügbar /manualSoldOut = true Artikel bleibt als „derzeit aus“ sichtbar, Bestellung wird mit ARTICLE_SOLD_OUT abgewiesen
Aus der Karte nehmen, an der Kasse lassen /hideFromMenu = true nicht im Shop, aber an der Kasse buchbar
Ganz abschalten /isActive = false weder Shop noch Kasse
Name, Beschreibung /name, /description in der Karte

Senden Sie Änderungen gebündelt und nicht im Sekundentakt. Optionen (Extras, Größen) und Allergene pflegt der Betrieb in der Kasse; der Shop liest sie aus der Karte.

D. Bestellung an die Kasse übergeben

POST /api/v1/online-checkout/create braucht keine Anmeldung. Die Antwort enthält das Bestell-Geheimnis (guestSecret), mit dem der Shop später Status lesen, bezahlen oder stornieren darf. Senden Sie bei jeder Bestellung eine Kopfzeile Idempotency-Key mit neuem Wert: Wiederholt der Shop den Aufruf nach einem Netzfehler, bekommt er dieselbe Bestellung zurück statt einer zweiten.

curl -sS -X POST "$API/online-checkout/create" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(cat /proc/sys/kernel/random/uuid)" -d '{
  "orderType": 0, "customerName": "Erika Muster", "customerEmail": "erika@example.com", "customerPhone": "0123 456789",
  "items": [ { "articleId": "art_917c19d4a18a", "quantity": 1, "notes": "ohne Zwiebeln" } ],
  "paymentMethod": "cash" }' | jq '.data | {id, orderNumber, status, grandTotal, isPaid, guestSecret}'
{
  "id": "olord_648e7d3cfa87",
  "orderNumber": "ON-20261110-001",
  "status": 1,
  "grandTotal": 9.5,
  "isPaid": false,
  "guestSecret": "oNaV4kT0pQ7sH2mXz9LwB1cEyD6uRf3J"
}

Das Bestell-Geheimnis (guestSecret) steht nur in dieser Antwort. Speichern Sie es zusammen mit der id bei Ihrer Bestellung. Damit lesen Sie später den Status (Kopfzeile X-Order-Secret); ohne das Geheimnis antwortet die Kasse auf jede Abfrage dieser Bestellung mit 404, genauso wie bei einer unbekannten Bestellung:

curl -sS "$API/online-checkout/olord_648e7d3cfa87" -H "X-Order-Secret: oNaV4kT0pQ7sH2mXz9LwB1cEyD6uRf3J" \
  | jq -c '.data | {id, orderNumber, status, isPaid}'
{"id":"olord_648e7d3cfa87","orderNumber":"ON-20261110-001","status":1,"isPaid":false}
Feld Bedeutung
orderType 0 Abholung, 1 Lieferung (braucht street, houseNumber, zipCode, city und das Modul Lieferservice)
items[] articleId, quantity, notes, selectedOptions (Name der gewählten Optionen; den Preis bestimmt die Kasse, ein mitgesendeter Preis wird ignoriert)
paymentMethod cash, stripe oder paypal, siehe Bezahlen
requestedTime, kitchenNotes Wunschzeit, Hinweis für die Küche
externalReference Ihre eigene Bestellnummer (höchstens 100 Zeichen), nur mit Benutzer-Anmeldung; dieselbe Nummer mit demselben Inhalt liefert die vorhandene Bestellung zurück
tipAmount Trinkgeld, nur mit Online-Zahlung

Vorab prüfen Sie online-menu/status und online-menu/wait-estimate und zeigen dem Gast die Wartezeit. Häufige Antworten:

errorCode Bedeutung
SHOP_CLOSED außerhalb der Öffnungszeiten
SHOP_OVERLOADED der Betrieb nimmt wegen Küchenlast gerade nicht an
ARTICLE_SOLD_OUT, ARTICLE_POS_ONLY Artikel ist aus bzw. nur an der Kasse verkäuflich
MIN_ORDER_AMOUNT, DELIVERY_AREA_NOT_COVERED Mindestbestellwert nicht erreicht / Adresse außerhalb der Liefergebiete
PREPAYMENT_REQUIRED der Betrieb verlangt Online-Zahlung
ONLINE_ORDER_DISABLED, ONLINEORDER_MODULE_REQUIRED Online-Bestellung ist im Betrieb aus bzw. nicht lizenziert

E. Den Stand der Bestellung zurückbekommen

Ihr Shop soll dem Gast „angenommen“, „in Zubereitung“, „fertig“ anzeigen. Dafür gibt es drei Wege, die Sie kombinieren können.

Weg So geht es Wann sinnvoll
Abfragen GET /api/v1/online-checkout/{id} mit Kopfzeile X-Order-Secret (eine Bestellung); GET /api/v1/online-checkout/today mit Partner-Schlüssel (orders:read) für alle heutigen Der Gast wartet auf einer Statusseite: alle 10 bis 15 Sekunden abfragen
Webhook Die Kasse ruft eine https-Adresse Ihres Servers auf, signiert und mit Wiederholung über Stunden Sie wollen den Gast benachrichtigen, ohne ständig zu fragen
Ereignis-Feed GET /api/v1/events?after=<cursor> mit Partner-Schlüssel (events:read) Rückfall, wenn Ihr Empfänger ausfiel, oder wenn Sie keine eingehende Adresse betreiben

Das Feld status der Bestellung:

status Bedeutung status Bedeutung
1 eingegangen 6 bereit
2 Zahlung läuft 7 unterwegs (Lieferung)
3 bezahlt 8 abgeschlossen
4 angenommen 9 storniert
5 in Zubereitung 10 abgelehnt

Welche Zwischenstufen (5, 6) vorkommen, hängt von den Schritten ab, die der Betrieb im Lieferservice eingeschaltet hat.

Beispiel mit Partner-Schlüssel: Der Betrieb (oder Ihr Server mit orders:manage) nimmt die Bestellung an, danach liefert der Feed die Ereignisse:

curl -sS -X POST "$API/online-checkout/olord_648e7d3cfa87/confirm" -H "X-Dikas-Api-Key: $PARTNER_SCHLUESSEL" | jq -c '.data | {orderNumber, status}'
curl -sS "$API/events?limit=10" -H "X-Dikas-Api-Key: $PARTNER_SCHLUESSEL" | jq -c '.data.events[] | {type, seq, data}'
{"orderNumber":"ON-20261110-001","status":4}
{"type":"order.created","seq":1,"data":{"orderId":"olord_648e7d3cfa87","orderNumber":"ON-20261110-001","orderType":0}}
{"type":"order.status_changed","seq":2,"data":{"orderId":"olord_648e7d3cfa87","orderNumber":"ON-20261110-001","status":4,"previousStatus":1,"isPaid":false}}

Webhook einrichten

Ein Administrator des Betriebs legt das Ziel an (https, Port 443, höchstens 3 je Betrieb); die Antwort nennt das Signatur-Geheimnis (whsec_…) einmal:

curl -sS -X POST "$API/webhook-targets" -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Webshop","url":"https://shop.example.de/dikas/ereignisse"}' | jq '.data | {id, secret}'
Ereignis (type) Wann Inhalt (data)
order.created Bestellung angelegt orderId, orderNumber, orderType, externalRef
order.status_changed Status hat sich geändert orderId, orderNumber, status, previousStatus, isPaid
payment.received Bestellung ist bezahlt (online oder bar an der Kasse) orderId, amountPaid, paymentMethod
order.cancelled abgelehnt oder storniert orderId, orderNumber, reason
ticket.ready Abholnummer fertig ticketId, number, status

Jedes Ereignis hat die Hülle id, type, occurredAt (UTC), tenant, seq, apiVersion, data. Die Ereignisse enthalten bewusst keine Namen, Adressen oder Positionen, nur Kennungen und Status; Einzelheiten holen Sie mit Ihrem Schlüssel (orders:read) nach.

Zustellung: mindestens einmal, nicht in strenger Reihenfolge über Ereignisse hinweg. Entdoppeln Sie über id und ordnen Sie über seq (je Bestellung aufsteigend und lückenlos; eine Lücke heißt, dass ein Ereignis fehlt). Antworten Sie mit 2xx, sobald Sie das Ereignis gespeichert haben; bei allem anderen wiederholt die Kasse. 410 schaltet das Ziel ab.

Signatur prüfen: Kopfzeile X-Dikas-Signature: t=<unix>,v1=<hex>. Die signierte Nachricht ist t, ein Punkt und die unveränderten Rohbytes des Rumpfs; Schlüssel ist das Geheimnis als Text (mit whsec_), Verfahren HMAC-SHA256, Hex in Kleinbuchstaben. In Node.js (Express: den Rumpf als Rohbytes lesen, nicht erst parsen):

import crypto from 'node:crypto';

export function signaturGueltig(kopf, rohRumpf, geheimnis, maxAlterSek = 300) {
  const t = Number(kopf.match(/t=(\d+)/)?.[1]);
  const signaturen = [...kopf.matchAll(/v1=([0-9a-f]+)/g)].map(m => m[1]);
  if (!t || Math.abs(Date.now() / 1000 - t) > maxAlterSek) return false;   // zu alt: ablehnen
  const soll = crypto.createHmac('sha256', geheimnis).update(`${t}.`).update(rohRumpf).digest('hex');
  return signaturen.some(s => s.length === soll.length &&
    crypto.timingSafeEqual(Buffer.from(s), Buffer.from(soll)));          // während einer Rotation mehrere v1
}

Testvektor zum Nachrechnen: Geheimnis whsec_dikas-test-vector-0001, t = 1767312000, Rumpf genau

{"id":"olord_ab12.3","type":"order.status_changed","occurredAt":"2026-01-02T00:00:00Z","tenant":"demo","seq":3,"apiVersion":"2026-11-01","data":{"orderId":"olord_ab12","orderNumber":42,"status":6,"previousStatus":5,"isPaid":false}}

ergibt v1=aa93e991e022bd8dd251f140eb934ae6692cbfa2fb44d8347620c21eb4294649.

Ereignis-Feed

Ohne eigene öffentliche Adresse holen Sie dieselben Ereignisse ab:

curl -sS "$API/events?after=$ZEIGER&limit=100" -H "X-Dikas-Api-Key: $PARTNER_SCHLUESSEL" | jq -c '{n:(.data.events|length), nextCursor:.data.nextCursor, hasMore:.data.hasMore}'

Speichern Sie nextCursor und rufen Sie damit erneut auf; ohne after liefert der Feed die letzten 24 Stunden. Aufbewahrt werden die Ereignisse 14 Tage.

F. Bezahlen

Die Zahlart legen Sie bei create fest:

paymentMethod Ablauf
cash Der Gast zahlt bei Abholung oder Übergabe; kassiert wird in der Kasse mit Beleg
stripe Karte über das Stripe-Konto des Betriebs: init-payment liefert clientSecret und publishableKey für Stripe.js im Browser des Gastes, danach verify-payment
paypal init-payment mit returnUrl liefert eine redirectUrl zu PayPal; nach der Rückkehr verify-payment. PayPal muss im Betrieb eingeschaltet sein

init-payment und verify-payment rufen Sie mit dem Bestell-Geheimnis (X-Order-Secret) auf. Die Zahlung wird serverseitig bei Stripe bzw. PayPal geprüft, erst dann gilt die Bestellung als bezahlt.

Zahlt Ihr Shop selbst?

Wickelt Ihr Shop die Zahlung über ein eigenes Konto ab, kennt die Kasse sie nicht: Die Bestellung kommt als nicht bezahlt an und erscheint weder im Kassenabschluss noch im DATEV-Export des Betriebs. Lassen Sie die Online-Zahlung deshalb über die Kasse laufen (stripe oder paypal wie oben).

Checkliste vor dem Livegang

  • Modul und Schalter „Online-Bestellung“ aktiv, Öffnungszeiten und Liefergebiete gepflegt
  • Wer führt die Karte (Kasse oder Shop) ist entschieden; Probe-Bestellung mit jedem Artikeltyp
  • Idempotency-Key bei jedem create; Fehlercodes im Shop in lesbare Hinweise übersetzt
  • Partner-Schlüssel mit notiertem Ablaufdatum; technischer Benutzer ohne Administratorrechte
  • Status-Rückmeldung gewählt (Abfrage, Webhook oder Feed) und mit einer Probebestellung durchgespielt; Signatur geprüft
  • Zahlung über die Kasse getestet
  • Probebestellungen im Testsystem storniert

Weiter