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.