Wir haben eine kleine Rechnung über 916,30 € als XRechnung an POST /api/v1/invoice/de/xrechnung/generate geschickt, einmal korrekt und dann mit gezielten Fehlern. Das Ergebnis:
| Variante | Was wir geändert haben | Antwort |
|---|---|---|
| korrekt | nichts | 200, gültig |
| Endsumme +0,01 | BT-112 = 916.31 statt 916.30 | 422, BR-CO-15 |
| Steuer +0,01 | BT-110 = 146.31, Endsumme passend dazu | 200, gültig |
| Steuer +0,99 | BT-110 = 147.29, Endsumme passend dazu | 200, gültig |
| Steuer +1,00 | BT-110 = 147.30, Endsumme passend dazu | 422, BR-CO-17 und BR-S-09 |
Ein Cent in der Summe reicht für die Ablehnung, 99 Cent in der Steuer nicht. Das ist kein Fehler unserer API, so prüft die Norm. Wer das nicht weiß, verlässt sich auf eine Prüfung, die genau den teureren Fehler durchlässt.
Die Rechnung
Zwei Positionen, 19 % Umsatzsteuer. Im Video klappt sie in die Felder der europäischen Norm EN 16931 um:
| Feld | Bedeutung | Wert |
|---|---|---|
BT-131 | Nettobetrag je Position | 480.00 (Montage), 290.00 (Material) |
BT-109 | Gesamtbetrag ohne Umsatzsteuer | 770.00 |
BT-110 | Umsatzsteuer gesamt | 146.30 |
BT-112 | Gesamtbetrag mit Umsatzsteuer | 916.30 |
BT-115 | Fälliger Zahlbetrag | 916.30 |
Dazu kommen Rechnungsnummer (BT-1), Datum (BT-2), Rechnungsart (BT-3), Währung (BT-5) und die Artikelnamen (BT-153). Getestet haben wir XRechnung in UBL-Syntax. Firmen, USt-IdNrn. und IBAN in unseren Anfragen sind Beispieldaten.
Warum der eine Cent scheitert
Die Summenregeln der Norm sind exakt. BR-CO-15 verlangt BT-112 = BT-109 + BT-110, ohne Spielraum. 770.00 + 146.30 ergibt 916.30, nicht 916.31. Die API antwortet mit HTTP 422 (gekürzt um filename, mimeType, hash, payloadHash und warnings):
1{2 "success": false,3 "format": "xrechnung",4 "data": "…",5 "complianceErrors": [6 {7 "code": "SPEC_RULE_FAILED",8 "rule": "BR-CO-15",9 "message": "[BR-CO-15]-Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).",10 "field": "/Q{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice[1]"11 }12 ]13}data enthält trotzdem das erzeugte XML. So siehst du, was die Prüfung abgelehnt hat. Verschicken solltest du es nicht.
Dasselbe gilt für die Nachbarregeln: BR-CO-13 (BT-109 = Summe der BT-131 − Abzüge + Zuschläge) und BR-CO-16 (BT-115 = BT-112 − bereits gezahlt + Rundung).
Warum 99 Cent durchgehen
Die Steuer prüfen andere Regeln, BR-CO-17 und für den Normalsatz BR-S-09. BR-CO-17 lautet im Text: Steuerbetrag je Kategorie = Bemessungsgrundlage × Steuersatz, gerundet auf zwei Nachkommastellen. Die offizielle Schematron der Norm prüft aber großzügiger. Sinngemäß steht dort:
1|Steuerbetrag| − 1 < gerundet(Bemessungsgrundlage × Satz) < |Steuerbetrag| + 1Der angegebene Steuerbetrag darf also um bis zu 99 Cent vom berechneten abweichen. Bei 770.00 × 19 % sind 146.30 richtig. Gemessen: 145.31 und 147.29 bestehen die Prüfung, 145.30 und 147.30 scheitern an BR-CO-17 und BR-S-09. Rundet ein System die Steuer je Position und addiert dann, weicht es je Position um höchstens einen halben Cent ab. Bei kleinen Rechnungen bleibt das weit innerhalb der Toleranz.
Gültig heißt hier nicht richtig. Wer mehr Umsatzsteuer ausweist, als er schuldet, schuldet nach § 14c Abs. 1 UStG auch den Mehrbetrag, bis er die Rechnung berichtigt. Die Validierung bewahrt dich davor nicht. Rechne die Steuer selbst, je Steuersatz aus der Bemessungsgrundlage, und behandle eine bestandene Prüfung nicht als Beleg, dass der Betrag stimmt:
1// je Steuersatz, nicht je Position2const net = round2(sum(linesWithRate.map((l) => l.netAmount)));3const tax = round2((net * rate) / 100);4const total = round2(net + tax);Was eine Anzahlung ändert
Ein zweiter Fall: Der Kunde hat 300 € angezahlt. Wir haben dieselbe Rechnung mit prepaidAmount: 300 geschickt. Ergebnis, gültig:
| Feld | Wert |
|---|---|
BT-109 Gesamtbetrag ohne Umsatzsteuer | 770.00 |
BT-110 Umsatzsteuer | 146.30 |
BT-112 Gesamtbetrag mit Umsatzsteuer | 916.30 |
BT-113 Bereits gezahlt | 300.00 |
BT-115 Fälliger Zahlbetrag | 616.30 |
Die Anzahlung senkt nur den Zahlbetrag. Bemessungsgrundlage und Steuer bleiben, denn der Kunde hat früher gezahlt, nicht weniger. Zieht man die 300 € stattdessen von der Nettosumme ab, lehnt die API schon vor der Erzeugung ab: Subtotal (470) does not match sum of items (770). Difference: 300. Die zweite Meldung derselben Antwort zeigt, dass auch die Bemessungsgrundlage eine Toleranz hat: BR-S-08 … requires 469 < sum < 471.
Zwei Arten von 422
Unser allererster Lauf scheiterte an etwas anderem. Die Käuferreferenz BT-10, die jede XRechnung nach BR-DE-15 braucht, stand in unserem JSON an der falschen Stelle: oben statt in countrySpecific. Bei öffentlichen Auftraggebern steht dort die Leitweg-ID, sonst etwa die Bestellnummer des Kunden. Die Antwort, ebenfalls HTTP 422, sah anders aus (gekürzt):
1{2 "success": false,3 "data": "",4 "errors": [5 {6 "code": "DE_REF_002",7 "rule": "BR-DE-15",8 "message": "Käuferreferenz (BT-10) ist für XRechnung Pflicht (BR-DE-15). Bitte eine Käuferreferenz oder Leitweg-ID (B2G) angeben.",9 "field": "countrySpecific.buyerReference"10 }11 ]12}Hier hat die API gar nicht erst erzeugt. errors zeigt mit field auf das Feld in deinem JSON. Bei complianceErrors wurde das Dokument erzeugt und danach geprüft, field ist ein XPath ins XML. Für die Fehlerbehandlung heißt das: Bei errors korrigierst du die Eingabe, bei complianceErrors meistens deine Rechenlogik.
Der zweite Lauf scheiterte an BR-DE-1: Eine XRechnung braucht Zahlungsangaben (BG-16), und wir hatten weder Bankverbindung noch Zahlungsart geschickt. Gültig wurde sie mit seller.bankAccount.iban und countrySpecific.paymentMeansCode: "58" (SEPA-Überweisung).
Selbst ausprobieren
Im Playground kannst du ohne eigenen Schlüssel eine Rechnung erzeugen und die Antwort ansehen. Die Feldnamen stehen in der Feldreferenz, den Einstieg mit eigenem Schlüssel beschreibt der Quickstart.

