The KoSIT validator as an API: check what horstoeko, Mustang and co. generate

Generating invoices with a free library or your own code? Then the one thing missing is the official check — and that is a Java program with a configuration you update yourself at every XRechnung release. NormAPI runs exactly that tool as an HTTP endpoint: free, no sign-up, and every error comes with a plain-language explanation.

Last updated: 16 October 2026 · rule set v2026-08-31

Self-hosted or as an API

The KoSIT validator is the checking tool of Germany’s Coordination Office for IT Standards, and its XRechnung configuration is the official rule set. Both are free. What costs something is running them:

Self-hostedNormAPI
RuntimeA Java runtime plus the validator JAR, as a command line, in daemon mode or in a Docker image of your ownOne HTTPS call, nothing to install
Rule setYou download the configuration and swap it at every XRechnung releaseWe keep it current; every response names the version it checked against (now v2026-08-31)
ZUGFeRD PDFExtract the XML from the PDF yourself firstSend the PDF as it is; the embedded invoice is what gets checked
Factur-X BASIC, EXTENDEDThe official configuration ships no scenario for themA scenario of our own against the EN 16931 core, rather than passing through unchecked
ResultAn XML report, HTML on request, with the rule textJSON: the verdict, and per finding the rule code, severity, location and, in plain language, what the rule requires and how to fix it
CostYour server, your upkeepFree; 30 requests a minute without an account

PHP with horstoeko/zugferd

horstoeko/zugferd builds the invoice, and it ships two classes for checking it. ZugferdXsdValidator checks against the XML schema only — the roughly 300 business rules such as BR-DE-15 or BR-CO-16 do not run. ZugferdKositValidator wraps the KoSIT validator: it downloads the validator and its scenarios and needs a complete Java installation on the server, and the project itself calls the feature experimental.

Instead, the output of getContent() goes to the API as it is:

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

$document = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_XRECHNUNG_3);
// … build the invoice as usual …
$xml = $document->getContent();
// A hybrid PDF instead: (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: en'],
]);
$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";
    }
}

A hybrid PDF goes the same way, with Content-Type: application/pdf. The file type is detected from the content anyway, not from the header.

Java, Mustang and others

Mustang (Java), node-zugferd (TypeScript), factur-x and drafthorse (Python) all end with a file or a byte buffer. The call is the same — here with the JDK’s own HttpClient, no further dependency:

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

For TypeScript there is the client @normapi/client, with no dependencies; the API documentation shows it.

In CI

The check pays off most where a change to the generator would otherwise go unnoticed: as a build step, against a sample invoice from your own code. When a rule fails, the log shows the code and the fix, and the build stops before the invoice reaches a customer.

Shell
# Fails the build when the invoice is not acceptable
curl -sS https://api.normapi.de/v1/validate \
  -H 'Content-Type: application/xml' \
  -H 'Accept-Language: en' \
  --data-binary @build/invoice.xml > report.json

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

What comes back

  • acceptable is the verdict. An invalid invoice is still an HTTP 200: the status says whether the request could be processed, not whether the invoice is any good.
  • findings[]: per finding the rule code, severity, location and rule text, plus an explanation of what the rule requires and how to fix it. Every code leads into the error reference.
  • rulesetVersion names the version checked against. It belongs in every report you keep; what changes between versions is in the rule set changelog.

Limits and cost

  • Validation is free and stays free. Without an account the limit is 30 requests a minute; with a key of your own, also free, a validation never counts against an allowance.
  • Up to 5 MB per document. The file is checked in memory and discarded; we store no invoice content.
  • The check sees format and business rules, not substance: no validator can tell whether the tax rate is right or the description is true.