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 betrieben | NormAPI | |
|---|---|---|
| Laufzeit | Java-Laufzeit plus Validator-JAR, als Kommandozeile, im Daemon-Modus oder in einem eigenen Docker-Image | Ein HTTPS-Aufruf, nichts zu installieren |
| Regelwerk | Die Konfiguration laden Sie selbst und tauschen sie bei jedem XRechnung-Release | Wir ziehen sie nach; jede Antwort nennt die geprüfte Fassung (derzeit v2026-08-31) |
| ZUGFeRD-PDF | Die XML vorher selbst aus dem PDF lösen | Das PDF direkt senden; geprüft wird die eingebettete Rechnung |
| Factur-X BASIC, EXTENDED | Die offizielle Konfiguration enthält dafür kein Szenario | Ein eigenes Szenario gegen den EN-16931-Kern, statt ungeprüft durchzulaufen |
| Ergebnis | Ein XML-Bericht, auf Wunsch HTML, mit dem Regeltext | JSON: das Urteil, je Fund Regelcode, Schweregrad, Fundstelle und im Klartext, was die Regel verlangt und wie Sie es beheben |
| Kosten | Ihr Server, Ihre Wartung | Kostenlos; 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
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:
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.
# 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/nullWas 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.