# Menüheld API > Menüheld ist ein Multi-Tenant-Bestell-/Kassen-/Kiosk-System für die Gastronomie. > Die öffentliche Produkt-API ist versioniert, idempotent und AI-native gedacht: > ein Gastronom (oder dessen KI) kann damit Speisekarte, Bestellungen und > Reservierungen lesen und – später – konfigurieren. Status: v1 (stabil, additiv erweiterbar). Lese- UND Schreib-Scopes sind freigeschaltet, ebenso Webhooks und ein MCP-Server (POST /api/mcp). Maschinen-lesbare Spec: GET /api/v1/openapi.json (OpenAPI 3.1, inkl. Webhooks). ## Authentifizierung Alle Aufrufe brauchen einen API-Key als Bearer-Token: Authorization: Bearer mh_live_XXXXXXXXXXXXXXXXXXXXXXXX - Keys werden pro Restaurant im Admin unter „API-Zugang" erzeugt (einmalig sichtbar). - Keys sind scoped (Prinzip least-privilege) und jederzeit widerrufbar. - Fehlender/ungültiger Key → 401. Fehlender Scope → 403. ## Scopes - `menu:read` – Speisekarte lesen - `menu:write` – Speisekarte ändern (Name/Beschreibung/Preis/Verfügbarkeit) - `orders:read` – Bestellungen lesen - `orders:write` – Bestellungen annehmen/ablehnen - `reservations:read` – Reservierungen lesen - `reservations:write` – Reservierungen bestätigen/ablehnen/abschließen - `settings:read` – Stammdaten lesen (Name, Telefon, Farbe, Öffnungszeiten) - `settings:write` – Stammdaten ändern (Free-Zone, keine Fiskal-Felder) Fiskal-Scopes (Steuerkategorie/TSE) sind bewusst NICHT per API frei schreibbar und verlangen später eine menschliche Bestätigung. ## Konventionen (v1.1) — worauf sich Clients verlassen können - **Fehler:** RFC 9457, Content-Type `application/problem+json`. Form: `{ type, title, status, code, detail?, errors?, requestId }`. Der `code` ist stabil (z. B. `insufficient_scope`, `not_found`, `invalid_input`, `rate_limited`, `idempotency_conflict`), der `title`/`detail`-Text darf sich ändern. - **Request-ID:** Jede Antwort trägt `X-Request-Id`. Ein selbst mitgeschickter `X-Request-Id` (8–128 Zeichen `[A-Za-z0-9._-]`) wird gespiegelt — bei Support einfach diese ID nennen. - **Rate-Limit:** Header `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (Sekunden) auf jeder Antwort. Reads 120/min, Writes 60/min pro Key. Bei Überschreitung `429` + `Retry-After`. - **Idempotenz:** Schreib-Requests (POST/PATCH/DELETE) akzeptieren einen `Idempotency-Key`-Header. Gleicher Key + gleicher Request → gespeicherte Antwort wird zurückgespielt (`Idempotency-Replayed: true`). Gleicher Key + abweichender Body → `409 idempotency_conflict`. So sind Retries nach Netzfehlern sicher. - **Kompatibilität:** v1 wird nur additiv erweitert (neue Felder, Endpoints, Webhook-Typen, optionale Scopes). Clients MÜSSEN unbekannte Felder ignorieren und unbekannte Enum-Werte tolerant behandeln. ## Webhooks — Envelope + Header Zustellung mindestens einmal (Duplikate möglich, Reihenfolge nicht garantiert). Body: `{ id: "evt_…", type, apiVersion, event, data, restaurantId, createdAt }`. Header: `X-Menuheld-Event`, `X-Menuheld-Event-Id` (stabil über alle Zustellversuche → Dedupe), `X-Menuheld-Delivery` (pro Versuch), `X-Menuheld-Attempt`, `X-Menuheld-Timestamp`, `X-Menuheld-Signature: sha256=…` (HMAC über `timestamp.body` mit dem Webhook-Secret). Signatur prüfen, dann `2xx` schnell antworten und asynchron verarbeiten. ## Endpoints ### GET /api/v1/restaurant (Scope: settings:read) Stammdaten: `{ version, restaurant: { name, phone, accentColor, logoUrl, openingHours } }`. `openingHours`: `{ "mon": [{ "open":"11:00","close":"22:00" }], … }` (Tage mon–sun). ### PATCH /api/v1/restaurant (Scope: settings:write) Stammdaten ändern (partiell): `{ name?, phone?, accentColor?("#RRGGBB"), openingHours? }`. Zeiten im Format `HH:MM`. Keine Fiskal-Felder. → `{ ok: true }`. ### GET /api/v1/menu (Scope: menu:read) Gibt die Speisekarte zurück: Kategorien → Artikel → Optionsgruppen. Beispiel: curl https://.menuheld.de/api/v1/menu \ -H "Authorization: Bearer mh_live_XXXX" Antwort (gekürzt): { "version": "v1", "restaurant": { "name": "Lahori Dhaba" }, "categories": [ { "id": "…", "name": "Vorspeisen", "items": [ { "id": "…", "name": "Samosa", "priceCents": 450, "taxCategory": "food", "optionGroups": [ { "name": "Schärfe", "options": [ { "id": "…", "name": "Mild", "priceCentsDelta": 0 } ] } ] } ] } ] } ### POST /api/v1/menu/categories (Scope: menu:write) Neue Kategorie. Body: `{ "name": "Vorspeisen", "emoji"?: "🥗" }` → `{ ok, id }`. ### POST /api/v1/menu/items (Scope: menu:write) Neuer Artikel. Body: `{ "categoryId", "name", "priceCents", "taxCategory"?:"food"|"drink", "description"?, "isAvailable"? }` → `{ ok, id }`. `taxCategory` nur bei Anlage wählbar. ### DELETE /api/v1/menu/items/{id} (Scope: menu:write) Artikel löschen → `{ ok: true }` (404 wenn nicht vorhanden). ### PATCH /api/v1/menu/items/{id} (Scope: menu:write) Ändert einen Artikel der kundenseitigen Speisekarte. Body (partiell, nur gesetzte Felder ändern): `{ "name"?, "description"?, "priceCents"?, "isAvailable"? }`. Beispiel — Preis auf 12,50 € setzen und ausverkauft markieren: curl -X PATCH https://.menuheld.de/api/v1/menu/items/ \ -H "Authorization: Bearer mh_live_XXXX" -H "Content-Type: application/json" \ -d '{ "priceCents": 1250, "isAvailable": false }' Steuerkategorie (food/drink) ist NICHT änderbar (Fiskal). Antwort: `{ "ok": true }`. ### GET /api/v1/orders (Scope: orders:read) `?scope=active|today|archive` (Standard active). Zahlungs-Gate greift (nur sichtbare Bestellungen). Antwort: `{ version, scope, orders: [ { id, orderNumber, status, type, customerName, totalCents, createdAt, requestedFor } ] }`. ### GET /api/v1/orders/{id} (Scope: orders:read) Vollständige Bestellung inkl. Positionen (`items`), Kunde, Lieferadresse, Summen. ### POST /api/v1/orders/{id}/accept (Scope: orders:write) Bestellung annehmen. Body optional `{ "prepMinutes": 30 }` (Zubereitungszeit). → `{ ok: true }`. ### POST /api/v1/orders/{id}/reject (Scope: orders:write) Bestellung ablehnen. Body `{ "reason": "…" }`. → `{ ok: true }`. ### GET /api/v1/reservations (Scope: reservations:read) `?day=YYYY-MM-DD` (ein Tag) oder `?scope=active|all`. Antwort: `{ version, reservations: [ { id, guestName, guestPhone, partySize, reservedFor, status, comment, createdAt } ] }`. ### GET /api/v1/reservations/{id} (Scope: reservations:read) Eine Reservierung als Detail. ### POST /api/v1/reservations/{id}/{accept|reject|complete|no-show} (Scope: reservations:write) Reservierung bestätigen / ablehnen (Body `{ "reason"? }`) / als erschienen abschließen / als No-Show markieren. → `{ ok: true }`. ## Webhooks Menüheld schickt bei Ereignissen einen signierten `POST` an deine im Admin hinterlegte URL. Events: - `order.created` - `order.status_changed` (Feld `status`) - `reservation.created` - `reservation.status_changed` (Feld `status`) Body: `{ "event": "...", "data": { "id": "...", ... }, "restaurantId": "...", "timestamp": "ISO" }` Die Payload ist schlank — Details bei Bedarf über die API nachladen. Signatur prüfen: Header `X-Menuheld-Signature: sha256=` = HMAC-SHA256 mit deinem Webhook-Secret über den String `.`. Weitere Header: `X-Menuheld-Event`, `X-Menuheld-Delivery` (eindeutige ID). Ein 2xx quittiert; sonst ein Retry, nach dauerhaften Fehlern wird der Webhook automatisch deaktiviert. ## MCP (für KI-Clients) Remote-MCP-Server (Streamable HTTP, JSON-RPC 2.0) unter **POST /api/mcp**, auth per Bearer-API-Key. Spielt die API als Tools aus (get_menu, update_menu_item, create_menu_item, accept_order, reject_order, list_reservations, update_restaurant …). Sichtbare/aufrufbare Tools richten sich nach den Scopes des Keys. Anbinden (Beispiel-Config): { "mcpServers": { "menuheld": { "url": "https://.menuheld.de/api/mcp", "headers": { "Authorization": "Bearer mh_live_XXXX" } } } } ## Konventionen - Alle Beträge in **Cent** (Integer), nie Kommazahlen. - Fehlerform: `{ "error": "" }` mit passendem HTTP-Status. - Rate-Limit pro Key (Standard 120 Anfragen/Minute) → 429 bei Überschreitung.