Kasse — API und MCP
Überblick
Dieser Bereich stellt 31 Operationen bereit. Jede davon ist zugleich ein MCP-Werkzeug.
Der Bereich hängt am Modul-Schalter module.pos. Ist das Modul aus, antworten diese Operationen mit 404.
Operationen
| Methode | Pfad | Zweck | Recht | Parameter | |||||
|---|---|---|---|---|---|---|---|---|---|
GET | /api/v1/pos/exports | Liste der PosExport | pos.export.manage | page, brandId, registerId, order[createdAt] | |||||
POST | /api/v1/pos/exports | DSFinV-K-Datensatz erzeugen — Payload {periodFrom, periodTo, registerUlid?} (Tagesdaten, einschließlich). Exportiert wird je Kassenabschluss, nicht je Kalendertag — noch offene Schichten werden übersprungen und in skippedSessions benannt. Ergebnis ist ein ZIP mit index.xml, der DTD und den CSV-Dateien der DSFinV-K 2.3; die Datei-ULID steht in fileUlid (Download über /v1/platform/files/{ulid}/download). | pos.export.manage | + Rumpf | |||||
GET | /api/v1/pos/exports/{ulid} | Eine PosExport lesen | pos.export.manage | ulid | |||||
GET | /api/v1/pos/recording-systems | Elektronische Aufzeichnungssysteme (§ 146a Abs. 4 AO) — Je Kasse die zu meldenden Angaben — Art und Seriennummer des Systems, Art, Seriennummer und BSI-Zertifizierungs-ID der Sicherheitseinrichtung, Anschaffungs- und Ausserbetriebnahmedatum — plus missing mit den noch fehlenden Feldern. Enthält bewusst AUCH stillgelegte Kassen: die Ausserbetriebnahme ist selbst meldepflichtig. Die Übertragung an ELSTER ist nicht Teil des Systems; diese Liste ist die Vorlage dafür. | pos.register.view | page, brandId, incomplete, order[registerKey] | |||||
GET | /api/v1/pos/registers | Liste der PosRegister | pos.register.view | page, brandId, active, warehouseId, q, order[name], order[registerKey] | |||||
POST | /api/v1/pos/registers | Kasse einrichten — Pflicht sind registerKey (DSFinV-K Z_KASSE_ID, systemweit eindeutig und danach unveränderlich) und name. warehouseId ist die Filiale, brandId die Firmierung auf dem Beleg. Ohne tseMode: fiskaly samt tseTssId/tseClientId wird JEDER Bon dieser Kasse als unsigniert geführt und trägt den Pflichthinweis. | pos.register.manage | + Rumpf | |||||
GET | /api/v1/pos/registers/{ulid} | Eine PosRegister lesen | pos.register.view | ulid | |||||
PATCH | /api/v1/pos/registers/{ulid} | Kasse ändern — Alles außer registerKey — die Kassen-ID ist die Kennung, unter der die Finanzverwaltung diese Kasse über Jahre wiederfindet. | pos.register.manage | ulid, + Rumpf | |||||
GET | /api/v1/pos/registers/{ulid}/customer-display | Kundendisplay einer Kasse lesen — Der Stand, den die Kasse zuletzt ans Display gegeben hat: state (`idle\ | cart\ | payment\ | paid), lines [{name, qty, unitPriceAmount, grossAmount}] (brutto, Cent), grossAmount, notices, changeAmount, updatedAt. Ohne Stand (oder nach 12 h) der Ruhezustand idle. Gedacht für die Browser-Ansicht auf einem zweiten Gerät am Tresen. Lesen darf die Kasse selbst (pos.sale.execute) oder ein reines Anzeigegerät mit dem minimalen Recht pos.display.view` — das kann weder senden noch kassieren. Fremde Marke → 404. | pos.sale.execute · pos.display.view | ulid | ||
POST | /api/v1/pos/registers/{ulid}/customer-display | Laufenden Bon ans Kundendisplay geben — Payload {state?, lines?, grossAmount?, notices?, changeAmount?} — state `idle\ | cart\ | payment\ | paid (Vorgabe cart, unbekannt → 422 errors.pos.displayStateUnknown), lines [{name, qty, unitPriceAmount, grossAmount}] in BRUTTO-Cent (höchstens 100), notices fertige Hinweissätze (höchstens 5), changeAmount Rückgeld nach Barzahlung. Ohne grossAmount gilt die Summe der Zeilen. Geht an die Browser-Ansicht und — falls angeschlossen — an ein Hardware-Display am Arbeitsplatz. Ein Display, das klemmt, ist KEIN Fehler: shown: false`, der Vorgang läuft weiter. | pos.sale.execute | ulid, + Rumpf | ||
POST | /api/v1/pos/registers/{ulid}/open-drawer | Kassenlade öffnen — Payload {reason?} (manual, cash_movement). Schickt den Impuls an die Lade am Arbeitsplatz dieser Kasse. Antwortet mit drawerOpened; false heißt „kein Gerät oder Impuls nicht angenommen" und ist kein Fehler — eine Lade, die klemmt, darf keinen Vorgang scheitern lassen. | pos.session.manage | ulid, + Rumpf | |||||
GET | /api/v1/pos/registers/{ulid}/pickup | Click & Collect: Auftrag zur Abholung an dieser Kasse aufrufen — Die OFFENEN Positionen eines Auftrags mit genau den Brutto-Beträgen, die der Bon kassieren wird (grossAmount je Zeile und gesamt), dazu tracking (serial\ | batch, sonst leer) und bei Chargenpflicht batchSuggestions (FEFO). Abschluss über POST /v1/pos/sales mit salesOrderUlid und pickupUnits [{orderItemUlid, batchRef?, serialRefs?}]. Fehler: fehlende Angabe → 400 errors.pos.pickupOrderReferenceRequired; unbekannt oder andere Marke → 404 errors.pos.pickupOrderNotFound; Nummer in mehreren Marken → 409 errors.pos.pickupOrderAmbiguous; storniert → 409; Fremdwährung, nichts offen, Konfigurationsartikel → 422. | pos.sale.execute | ulid, order | ||||
GET | /api/v1/pos/registers/{ulid}/price | Verkaufspreis eines Artikels an dieser Kasse — Der Brutto-Stückpreis inklusive Kundengruppen-, Staffel- und Aktionspreisen (mit cartQty auch Staffeln nach Bon-Summe), dazu Steuersatz und die angewandte Preisregel. Gedacht für ein Kassengerät, das den Warenkorb lokal führt: der Offline-Schnappschuss liefert nur den Regalpreis (priceBasis: shelf), dieser Abruf den Preis, der wirklich gilt. Unbekannter Code → 422. Dazu tracking (serial\ | batch, sonst leer) und bei Chargenpflicht batchSuggestions (FEFO): die Kasse verlangt damit den Scan je Stück bzw. bietet die Charge an (R47-002). | pos.sale.execute | ulid, code, productId, qty, cartQty, customerId | ||||
GET | /api/v1/pos/registers/{ulid}/snapshot | Offline-Schnappschuss der Kasse — Alles, was ein Kassengerät braucht, um OHNE Verbindung weiterzuverkaufen: Kasse, offene Schicht und die verkaufsfähigen Artikel mit Brutto-Regalpreis und Steuersatz. priceBasis: shelf sagt ausdrücklich, dass es KEINE Kundenpreise sind — die hängen an Daten, die das Gerät offline nicht hat. Je Artikel tracking (serial \ | batch \ | null): die Nachweisart — offline verlangt die Kasse die Seriennummer schon beim Kassieren, statt beim Nachsenden mit 422 zu scheitern. capabilities nennt, was offline erlaubt ist; Gutschein, Click&Collect und Kundensuche stehen bewusst auf false, damit die Oberfläche sie gar nicht erst anbietet. Ein gedeckelter Lauf meldet truncated und nextAfterId, statt still die Hälfte des Sortiments zu unterschlagen. | pos.sale.execute | ulid, since, after | |||
POST | /api/v1/pos/registers/{ulid}/terminal-charge | Betrag am Kartenterminal anfordern — Payload {amount, reference} — amount in Cent und POSITIV (eine Rückzahlung ist ein Storno, kein negativer Betrag), reference ist der Bezug des Kassenvorgangs (Bon-ULID). Antwortet mit terminalReference (Belegnummer des Terminals, gehört als reference an die Zahlungszeile), terminalAmount, terminalMaskedPan und terminalCardScheme. Ohne angebundenes Terminal 422 errors.pos.terminalUnavailable; Abbruch oder Ablehnung 422 errors.pos.terminalDeclined — in beiden Fällen ist KEIN Geld geflossen und die Zahlung darf nicht auf den Bon. | pos.sale.execute | ulid, + Rumpf | |||||
POST | /api/v1/pos/registers/{ulid}/terminal-refund | Kartenzahlung am Terminal stornieren — Payload {terminalReference, amount}. Braucht die Belegnummer des TERMINALS (nicht die Bonnummer): das Gerät führt sein eigenes Journal, und die Rücknahme muss dort ankommen. Führt das Terminal sie nicht aus, antwortet die Kasse mit 422 errors.pos.terminalRefundFailed — die Kartenzahlung ist dann weiterhin gültig, und der Gegenbon darf sie nicht als erstattet ausweisen. | pos.sale.execute | ulid, + Rumpf | |||||
GET | /api/v1/pos/sales | Liste der PosSale | pos.sale.view | page, brandId, registerId, sessionUlid, status, kind, customerId, unsigned, from, to, periodFrom, periodTo, q, order[number], order[finishedAt], order[grossAmount] | |||||
POST | /api/v1/pos/sales | Bon anlegen (und optional in einem Zug abschließen) — Payload {registerUlid, customerId?, clientRef?, salesOrderUlid?, items?: [...], payments?: [...]}. - clientRef ist der Offline-Anker: derselbe Wert liefert denselben Bon zurück (200 statt 201) statt einen zweiten anzulegen. - salesOrderUlid übernimmt die offenen Positionen eines Auftrags mit DESSEN Preisen (Click&Collect). - pickupUnits (nur mit salesOrderUlid): [{orderItemUlid, batchRef?, serialRefs?}] — Charge bzw. Seriennummern der abgeholten Auftragspositionen, geprüft und gebucht wie an einer Bonposition (Seriennummern genau eine je Stück, sonst 422 errors.pos.serialCountMismatch). Eine Position, die an diesem Auftrag nicht offen ist → 422 errors.pos.pickupUnitsUnknownItem; ohne Auftrag → 422 errors.pos.pickupUnitsWithoutOrder. - items: `[{code?\ | productId?, name?, qty?, unitPriceAmount?, discountAmount?, taxRate?, taxCategory?, itemType?}]. code löst über Artikelnummer ODER GTIN auf; unitPriceAmount ist immer BRUTTO. - **batchRef/serialRefs** je Position: die am Tresen erfasste Charge bzw. die gescannten Seriennummern (eine je Stück). Ein seriengeführter Artikel braucht genau so viele Nummern wie die Menge, sonst 422 errors.pos.serialCountMismatch; ohne Charge wählt FEFO und die gewählte Charge steht danach am Bon. Der Gegenbon legt dieselben Einheiten wieder ein. - **payments**: [{type: cash\ | card\ | voucher\ | invoice\ | online, amount?, givenAmount?, reference?}]. Sind sie da, wird der Bon sofort abgeschlossen; amount weggelassen heißt „der Rest". - **capturedAt` (ISO-8601): die Zeit, zu der das GERÄT den Bon erfasst hat — nur beim Nachreichen aus dem Offline-Puffer. Der Beleg trägt dann diese Zeit statt der Sync-Zeit (sonst sähen zwanzig Bons einer Netzstörung aus, als wären sie in derselben Minute entstanden); der Server klemmt sie gegen Schichtbeginn und Jetzt. Ein so nachgereichter Bon wird als dokumentierter TSE-Ausfall** geführt — nachträglich zu signieren wäre ein Signaturzähler, der nicht zum Verkauf gehört. | pos.sale.execute | + Rumpf |
GET | /api/v1/pos/sales/{ulid} | Eine PosSale lesen | pos.sale.view | ulid | |||||
POST | /api/v1/pos/sales/{ulid}/cancel | Offenen Bon abbrechen — Der Kunde geht ohne zu kaufen. Nichts gebucht, keine Nummer verbraucht — deshalb kein Storno und kein Gegenbon. Ein abgeschlossener Bon lässt sich so nicht abbrechen (409). | pos.sale.execute | ulid, + Rumpf | |||||
POST | /api/v1/pos/sales/{ulid}/email-receipt | Beleg per E-Mail zustellen — Payload {email?}. Schickt denselben Beleg, den der Drucker ausgibt, als PDF-Anhang — die Belegausgabepflicht (§ 146a Abs. 2 AO) verlangt einen ANGEBOTENEN Beleg, kein Papier. Ohne email entscheidet der Hauptkontakt des Kunden am Bon; ohne beides 422. Nur für abgeschlossene Bons (409). Der Versand läuft über den einen Kommunikations-Pfad des Hauses: Thread-Beitrag am Bon + Zustell-Datensatz, nie ein Ticket. Je Bon und Adresse genau einmal (Wiedervorlage schickt nicht doppelt). Die Antwort trägt receiptEmailed; ein Fehlschlag NACH den Prüfungen ist false, kein Fehler — der Verkauf steht, der Versand ist wiederholbar. | pos.sale.execute | ulid, + Rumpf | |||||
POST | /api/v1/pos/sales/{ulid}/finish | Abschließen — Nummer, Bestand, Geld, Signatur — Payload {payments: [{type, amount?, givenAmount?, reference?}]}. Die Zahlungen müssen den Bon EXAKT decken (ein zu viel hingelegter Schein ist Rückgeld, keine höhere Zahlung). Zieht die lückenlose Bonnummer, bucht den Warenausgang in EINEM Lauf und fragt zuletzt die TSE — ein Signatur-Ausfall bricht den Abschluss NICHT ab, er wird auf dem Bon dokumentiert und auf dem Beleg kenntlich gemacht (§ 146a AO / AEAO). | pos.sale.execute | ulid, + Rumpf | |||||
POST | /api/v1/pos/sales/{ulid}/items | Position hinzufügen (Scan) — Payload `{code?\ | productId?, name?, qty?, unitPriceAmount?, discountAmount?, taxRate?, taxCategory?, itemType?, batchRef?, serialRefs?}. Ohne Artikelbezug ist name Pflicht (Freitext-Position). Auf einem Verkaufsbon sind negative Mengen unzulässig — die Rücknahme ist ein Gegenbon. batchRef ist die gewählte Charge, serialRefs` die gescannten Seriennummern (genau eine je Stück, sonst 422). | pos.sale.execute | ulid, + Rumpf | ||||
POST | /api/v1/pos/sales/{ulid}/print | Beleg drucken bzw. Beleg-HTML holen — Payload {copies?}. Schickt den Beleg an den Arbeitsplatz-Agenten, sofern erreichbar; die Antwort enthält immer receiptHtml für die Druckvorschau — der Druck ist nie eine Bedingung, ein Papierstau darf einen abgeschlossenen Bon nicht in Frage stellen. | pos.sale.execute | ulid, + Rumpf | |||||
POST | /api/v1/pos/sales/{ulid}/remove-item | Position entfernen — Payload {itemUlid}. Nur solange der Bon offen ist — danach ist er ein Beleg. | pos.sale.execute | ulid, + Rumpf | |||||
POST | /api/v1/pos/sales/{ulid}/void | Zurücknehmen — Gegenbon erzeugen — Payload {refundPayments?: [{type, amount?, reference?}]}; ohne Angabe wird die ursprüngliche Zahlung gespiegelt. Erzeugt einen zweiten Bon mit negativen Mengen, eigener Nummer und eigener Signatur; das Original bleibt unverändert. Gutschein-Guthaben wird ZUERST zurückgegeben, dann der Rest ausgezahlt. Antwort ist der Gegenbon. - Vorzeichen: der Gegenbon ist NEGATIV, eine Rückzahlung deshalb ebenfalls — amount ist ein negativer Cent-Betrag (25,94 € zurück = -2594). Ein positiver Betrag wird mit 422 errors.pos.refundPaymentSign abgewiesen, bevor irgendetwas entsteht. - Betrag: die Summe der refundPayments muss den Gegenbon EXAKT decken (= negatives Brutto des Originals); sonst 422 errors.pos.paymentMismatch. amount weggelassen heißt „der Rest". - Alles oder nichts: Gegenbon, Positionen, Gutschein-Freigabe, Nummer, Bestand und die Verknüpfung am Original stehen in EINER Transaktion. Scheitert der Vorgang, bleibt nichts zurück und der Aufruf lässt sich unverändert wiederholen. | pos.sale.void | ulid, + Rumpf | |||||
GET | /api/v1/pos/sessions | Liste der PosSession | pos.sale.view | page, brandId, registerId, status, from, to, periodFrom, periodTo, order[zNumber], order[openedAt] | |||||
POST | /api/v1/pos/sessions | Schicht öffnen — Payload {registerUlid, openingCashAmount?}. Eine Kasse hat höchstens EINE offene Schicht (409). Die Z-Nummer läuft je Kasse fortlaufend und wird unter einer Sperre auf der Kassenzeile gezogen. | pos.session.manage | + Rumpf | |||||
GET | /api/v1/pos/sessions/{ulid} | Schicht mit Summen — Eine GESCHLOSSENE Schicht liefert den eingefrorenen Abschluss-Schnappschuss (Z-Bericht), eine OFFENE den Zwischenstand aus derselben Rechnung (X-Bericht). Zwei Rechnungen für dieselbe Frage wären zwei Antworten. | pos.sale.view | ulid | |||||
POST | /api/v1/pos/sessions/{ulid}/cash-movement | Bargeld einlegen oder entnehmen — Payload `{type: deposit\ | withdrawal\ | payout, amount, reason?}. amount` ist immer POSITIV — die Richtung steckt im Typ, damit eine Entnahme nicht versehentlich als Einlage gebucht wird. | pos.session.manage | ulid, + Rumpf | |||
POST | /api/v1/pos/sessions/{ulid}/close | Kassensturz — zählen und abschließen — Payload {countedCashAmount, note?}. Rechnet den Soll-Bestand (Anfangsbestand + Bareinnahmen + Einlagen − Entnahmen), speichert die Differenz ohne sie auszugleichen und friert die Abschluss-Summen ein. Ein offener Bon blockiert den Abschluss (409); ab der eingestellten Schwelle verlangt eine Differenz eine Begründung (422). | pos.session.manage | ulid, + Rumpf |
Zweckangaben, die nicht dem Standardmuster folgen, stammen unverändert aus der Schnittstellen-Beschreibung im Code.