Zuletzt aktualisiert: Juli 2026 · Für PHP 8.1+ · Lesezeit: ca. 8 Minuten

Ab Januar 2027 müssen deutsche Unternehmen mit mehr als 800.000 Euro Umsatz ihre B2B-Rechnungen als E-Rechnung ausstellen — ab 2028 alle. Wenn Ihre Rechnungen aus einer PHP-Anwendung kommen (einem Shop, einem selbst gebauten Backoffice, einem Laravel- oder Symfony-Projekt), stehen Sie vor der Frage: Wie wird aus meinen Rechnungsdaten normkonformes XRechnung-XML?

Die ehrliche Antwort: Selbst bauen lohnt sich fast nie. Die EN 16931 umfasst weit über hundert Geschäftsregeln, dazu kommen die deutschen BR-DE-Zusatzregeln, und die Spezifikation bekommt regelmäßig neue Versionen. Dieser Artikel zeigt den API-Weg: Sie schicken JSON, Sie bekommen validiertes XML. Kein XML-Schreiben, kein Schematron, keine Spezifikations-PDFs.

Was Sie brauchen

Einen API-Key (kostenlos, 50 Credits inklusive, keine Kreditkarte) — mehr nicht: fünf Zeilen cURL reichen, wie Sie gleich sehen. Ein offizielles Composer-SDK (fakturwire/sdk) erscheint in Kürze auf Packagist; dieser Artikel kommt bewusst ohne Abhängigkeiten aus.

Die Rechnung als JSON

Der Kern des Ganzen ist ein schlichtes JSON-Objekt. Die Feldnamen folgen den Geschäftsbegriffen der EN 16931 — buyerReference ist BT-10, also die berüchtigte Leitweg-ID bzw. Käuferreferenz. Beträge sind bewusst Strings, damit unterwegs keine Float-Rundung passiert:

$invoice = [
    'number'    => 'RE-2026-0815',
    'issueDate' => '2026-07-20',
    'dueDate'   => '2026-08-19',
    'currency'  => 'EUR',
    'buyerReference' => 'BESTELLUNG-4711',   // BT-10 — Pflicht in XRechnung!
    'seller' => [
        'name'  => 'Beispiel GmbH',
        'vatId' => 'DE811128135',
        'electronicAddress' => ['schemeId' => '9930', 'value' => 'DE811128135'],
        'address' => [
            'streetName' => 'Hauptstraße 5', 'city' => 'Berlin',
            'postalZone' => '10115', 'countryCode' => 'DE',
        ],
        'contact' => [   // BR-DE-2: Name, Telefon UND E-Mail sind Pflicht
            'name' => 'Erika Muster', 'phone' => '+49 30 1234567',
            'email' => 'rechnung@beispiel.de',
        ],
    ],
    'buyer' => [
        'name'  => 'Kunde AG',
        'vatId' => 'DE129273398',
        'electronicAddress' => ['schemeId' => '9930', 'value' => 'DE129273398'],
        'address' => [
            'streetName' => 'Marktplatz 1', 'city' => 'München',
            'postalZone' => '80331', 'countryCode' => 'DE',
        ],
    ],
    'payment' => [   // BR-DE-1: ohne Zahlungsangaben keine XRechnung
        'meansCode' => '58',   // SEPA-Überweisung
        'iban'      => 'DE02120300000000202051',
        'reference' => 'RE-2026-0815',
        'terms'     => 'Zahlbar innerhalb von 30 Tagen',
    ],
    'lines' => [[
        'id' => '1', 'name' => 'Beratungsleistung Juli',
        'quantity' => '10', 'unitCode' => 'HUR',   // Stunden
        'unitPrice' => '120.00',
        'vat' => ['categoryCode' => 'S', 'rate' => '19'],
    ]],
];

Was hier fehlt, fällt auf: Summen und Steueraufstellung. Die müssen Sie nicht liefern — sie werden serverseitig berechnet und gegen die BR-CO-Regeln kreuzgeprüft. Damit ist die häufigste Fehlerquelle (Rundungsdifferenzen zwischen Positions- und Endsummen) von vornherein weg.

Der Aufruf

Fünf Zeilen mit der curl-Extension — keine Abhängigkeit, kein Framework-Zwang (mit Guzzle sieht es entsprechend aus):

$ch = curl_init('https://fakturwire.com/v1/convert');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'authorization: Bearer ' . getenv('FW_API_KEY'),
        'content-type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'target'  => 'xrechnung-ubl',
        'invoice' => $invoice,
    ]),
]);
$response = json_decode(curl_exec($ch), true);
$status   = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status === 200) {
    file_put_contents('RE-2026-0815.xml', $response['xml']);
} elseif ($status === 422) {
    // Die Rechnung verletzt Regeln — nichts wurde berechnet.
    foreach ($response['findings'] as $f) {
        error_log("[{$f['rule']}] {$f['message']}");   // z. B. BR-DE-15 …
    }
}

Der wichtigste Teil ist der 422-Zweig. Verletzt die Rechnung eine Regel, kommt kein kaputtes XML zurück, sondern eine Liste von Befunden — mit Regel-ID, Klartext-Erklärung und dem betroffenen Feld. Die Konvertierung wird in dem Fall auch nicht berechnet. Bauen Sie den Zweig fest in Ihre Rechnungsstrecke ein: erzeugen → bei 422 Befunde loggen und korrigieren → erneut senden. So verlässt kein non-konformes Dokument je Ihr System.

Brauchen Sie statt XRechnung ein hybrides Format (PDF mit eingebettetem XML, für menschliche Leser), setzen Sie 'target' => 'facturx-en16931'. Für Rechnungen ins EU-Ausland über das Peppol-Netz: 'peppol-bis-3'.

Die Fehler, die Sie sehen werden

Aus der Praxis: Beim ersten Durchlauf mit echten Bestandsdaten scheitern die meisten Rechnungen an denselben vier Regeln. BR-DE-15 — die Käuferreferenz fehlt, weil das Feld im Altsystem nie gepflegt wurde. BR-DE-2 — kein Ansprechpartner mit Telefon und E-Mail im Stammsatz. BR-CO-15 — Summen aus dem Altsystem, die um einen Cent von der normgerechten Berechnung abweichen. Und BR-E-10, sobald Reverse-Charge-Rechnungen ohne Befreiungsgrund ankommen.

Der Rat daraus: Jagen Sie vor dem Livegang einen repräsentativen Stapel echter Rechnungen durch POST /v1/validate (kostet 1 Credit pro Prüfung) und bereinigen Sie die Stammdaten einmal gründlich. Das ist ein Nachmittag Arbeit — im Januar wäre es ein Notfall.

In Laravel

Im Alltag landet der Aufruf meist in einem Job oder Event-Listener — etwa nach InvoiceFinalized. Zwei Hinweise dazu: Legen Sie den API-Key in die config/services.php (nicht ins Repo), und speichern Sie das zurückgegebene XML unverändert — es ist Ihr aufbewahrungspflichtiges Original im Sinne der GoBD, nicht das PDF.


Testen ohne Code: Der kostenlose XRechnung-Validator prüft jede Rechnung im Browser gegen alle EN-16931- und BR-DE-Regeln. Und wenn es ernst wird: API-Key holen — 50 Credits gratis, Abrechnung pro Rechnung, kein Abo.