Der KoSIT-Validator als API: prüfen, was horstoeko, Mustang & Co. erzeugen

Sie erzeugen Rechnungen mit einer freien Bibliothek oder eigenem Code? Dann fehlt nur noch die offizielle Prüfung — und die ist ein Java-Programm mit einer Konfiguration, die Sie bei jedem XRechnung-Release selbst nachziehen. NormAPI führt genau dieses Werkzeug als HTTP-Endpunkt aus: kostenlos, ohne Anmeldung, und jeder Fehler kommt mit einer Erklärung im Klartext.

Stand: 16. Oktober 2026 · Regelwerk v2026-08-31

Selbst betreiben oder als API

Der KoSIT-Validator ist das Prüfwerkzeug der Koordinierungsstelle für IT-Standards, und die XRechnung-Konfiguration dazu ist das offizielle Regelwerk. Beides ist frei. Was es kostet, ist der Betrieb:

Selbst betriebenNormAPI
LaufzeitJava-Laufzeit plus Validator-JAR, als Kommandozeile, im Daemon-Modus oder in einem eigenen Docker-ImageEin HTTPS-Aufruf, nichts zu installieren
RegelwerkDie Konfiguration laden Sie selbst und tauschen sie bei jedem XRechnung-ReleaseWir ziehen sie nach; jede Antwort nennt die geprüfte Fassung (derzeit v2026-08-31)
ZUGFeRD-PDFDie XML vorher selbst aus dem PDF lösenDas PDF direkt senden; geprüft wird die eingebettete Rechnung
Factur-X BASIC, EXTENDEDDie offizielle Konfiguration enthält dafür kein SzenarioEin eigenes Szenario gegen den EN-16931-Kern, statt ungeprüft durchzulaufen
ErgebnisEin XML-Bericht, auf Wunsch HTML, mit dem RegeltextJSON: das Urteil, je Fund Regelcode, Schweregrad, Fundstelle und im Klartext, was die Regel verlangt und wie Sie es beheben
KostenIhr Server, Ihre WartungKostenlos; ohne Konto 30 Anfragen pro Minute

PHP mit horstoeko/zugferd

horstoeko/zugferd erzeugt die Rechnung; für die Prüfung bringt es zwei Klassen mit. ZugferdXsdValidator prüft nur gegen das XML-Schema — die rund 300 Geschäftsregeln wie BR-DE-15 oder BR-CO-16 laufen dabei nicht. ZugferdKositValidator bindet den KoSIT-Validator an: Er lädt Validator und Szenarien herunter und braucht eine vollständige Java-Installation auf dem Server, und das Projekt selbst bezeichnet die Funktion als experimentell.

Stattdessen geht das Ergebnis von getContent() unverändert an die API:

PHP
<?php
use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdProfiles;

$document = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_XRECHNUNG_3);
// … Rechnung aufbauen wie gewohnt …
$xml = $document->getContent();
// Hybrid-PDF stattdessen: (new ZugferdDocumentPdfBuilder($document, $pdf))
//     ->generateDocument()->downloadString()

$ch = curl_init('https://api.normapi.de/v1/validate');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $xml,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/xml', 'Accept-Language: de'],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$result['acceptable']) {
    foreach ($result['findings'] as $f) {
        echo "{$f['severity']} {$f['code']}: " . ($f['explanation']['fix'] ?? $f['text']) . "\n";
    }
}

Ein Hybrid-PDF senden Sie genauso, mit Content-Type: application/pdf. Der Dateityp wird ohnehin am Inhalt erkannt, nicht am Header.

Java, Mustang und andere

Mustang (Java), node-zugferd (TypeScript), factur-x und drafthorse (Python) schreiben am Ende eine Datei oder einen Byte-Puffer. Der Aufruf ist derselbe — hier mit dem HttpClient aus dem JDK, ohne weitere Abhängigkeit:

Java
var request = HttpRequest.newBuilder(URI.create("https://api.normapi.de/v1/validate"))
    .header("Content-Type", "application/xml")
    .header("Accept-Language", "de")
    .POST(HttpRequest.BodyPublishers.ofFile(Path.of("rechnung.xml")))
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
// JSON: acceptable, scenario, rulesetVersion, findings[]

Für TypeScript gibt es den Client @normapi/client ohne Abhängigkeiten; die API-Dokumentation zeigt ihn.

In der CI

Am meisten bringt die Prüfung dort, wo eine Änderung am Generator sonst unbemerkt bliebe: als Schritt im Build, gegen eine Beispielrechnung aus Ihrem eigenen Code. Fällt eine Regel, zeigt das Log den Code und die Behebung, und der Build bricht ab, bevor die Rechnung bei einem Kunden landet.

Shell
# Bricht den Build ab, wenn die Rechnung nicht annehmbar ist
curl -sS https://api.normapi.de/v1/validate \
  -H 'Content-Type: application/xml' \
  -H 'Accept-Language: de' \
  --data-binary @build/rechnung.xml > report.json

jq -r '(.findings // [])[] | "\(.severity) \(.code): \(.explanation.fix // .text)"' report.json
jq -e '.acceptable' report.json > /dev/null

Was zurückkommt

  • acceptable ist das Urteil. Eine ungültige Rechnung ist trotzdem eine HTTP-200-Antwort: der Status sagt, ob die Anfrage verarbeitet werden konnte, nicht, ob die Rechnung taugt.
  • findings[]: je Fund Regelcode, Schweregrad, Fundstelle und Regeltext, dazu eine Erklärung, was die Regel verlangt und wie Sie den Fehler beheben — auf Deutsch mit Accept-Language: de. Jeder Code führt in die Fehlerreferenz.
  • rulesetVersion nennt die geprüfte Fassung. Sie gehört in jeden abgelegten Bericht; was sich zwischen den Fassungen ändert, steht im Regelwerk-Changelog.

Grenzen und Kosten

  • Die Prüfung ist kostenlos und bleibt es. Ohne Konto gelten 30 Anfragen pro Minute; mit einem eigenen, ebenfalls kostenlosen Schlüssel zählt eine Prüfung nie gegen ein Kontingent.
  • Bis 5 MB je Dokument. Die Datei wird im Arbeitsspeicher geprüft und danach verworfen; wir speichern keine Rechnungsinhalte.
  • Die Prüfung sieht Format und Geschäftsregeln, nicht den Inhalt: ob der Steuersatz stimmt oder die Leistung zutrifft, erkennt kein Validator.