Eine gültige XRechnung aus PHP heraus erzeugst du am robustesten nicht durch eigenes XML-Zusammenbauen, sondern über eine E-Rechnung API: Du schickst strukturierte Rechnungsdaten (Kunde, Positionen, Steuersätze) per HTTP, der Dienst rechnet die Summen, vergibt die Nummer und rendert das EN-16931-konforme XML. Der Flow sind drei Calls — anlegen, ausstellen, XML abholen — die sich mit Guzzle oder der curl-Erweiterung in wenigen Zeilen abbilden lassen.
XRechnung ist reines XML nach EN 16931 — kein Sichtbeleg, nur strukturierte Daten. In PHP hast du zwei Wege: das XML mit einer Bibliothek selbst bauen (und Codelisten, Geschäftsregeln und Profilstände jährlich nachpflegen) oder die Erzeugung an eine E-Rechnung API delegieren. Dieser Beitrag zeigt den API-Weg — drei Calls, mit Guzzle.
Der Flow: drei Calls
- Anlegen — du schickst Kunde und Positionen, der Server rechnet Summen und USt.
- Ausstellen — die Rechnungsnummer wird atomar vergeben, das XML gerendert und gegen die EN-16931-Geschäftsregeln validiert.
- XML abholen — das reine XRechnung-XML (CII) zum Weiterverarbeiten oder Versenden.
Die Base-URL und einen Test-Key (sk_test_…) bekommst du mit dem Sandbox-Zugang; hier
kommen sie aus der Umgebung, damit sich der Code später ohne Änderung auf produktiv
umstellen lässt.
1) Rechnung anlegen
Beträge und Mengen gehen als DECIMAL-Strings über die Leitung — nie als PHP-Float,
sonst holt dich die Rundung ein. Der Idempotency-Key macht einen wiederholten
Anlege-Call zum Replay statt zum Duplikat.
<?php
use GuzzleHttp\Client;
$http = new Client([
'base_uri' => getenv('FORTLAUF_API_URL'), // Base-URL aus der Sandbox
'headers' => [
'Authorization' => 'Bearer ' . getenv('FORTLAUF_API_KEY'),
],
]);
$draft = $http->post('/invoices', [
'headers' => ['Idempotency-Key' => bin2hex(random_bytes(16))],
'json' => [
'currency' => 'EUR',
'documentTarget' => 'b2g',
'buyer' => [
'name' => 'Landkreis Eldenrode — Eigenbetrieb Abfallwirtschaft',
'addressLine1' => 'Am Anger 7',
'postalCode' => '06479',
'city' => 'Eldenrode',
'countryCode' => 'DE',
],
'buyerElectronicAddress' => '<LEITWEG-ID>',
'buyerReference' => '<LEITWEG-ID>', // Leitweg-ID (BT-10), nur B2G
'invoiceTypeCode' => '380',
'lineItems' => [
[
'description' => 'Softwarepflege Wiegesystem (2. Halbjahr 2026)',
'quantity' => '6',
'unitCode' => 'MON',
'unitPriceNet' => '480.00',
'taxRatePercent' => '19',
],
],
],
]);
$invoice = json_decode((string) $draft->getBody(), true);
$id = $invoice['id']; // Status "draft", alle Summen serverseitig berechnet2) Ausstellen
Das Finalisieren vergibt die Nummer atomar und rendert die XRechnung. Bricht die
Ausstellung ab, bleibt der Entwurf erhalten und behält seine reservierte Nummer — ein
zweiter Versuch stellt denselben Beleg unter derselben Nummer aus. Ist die Rechnung
bereits ausgestellt, antwortet ein wiederholter Aufruf mit 409; ein zweites Dokument
oder eine zweite Nummer entsteht nie.
<?php
$issued = $http->patch("/invoices/{$id}/finalize");
// → Status "issued", Nummer vergeben, XRechnung-XML (CII) gerendert + validiert3) XML abholen
<?php
$xml = (string) $http->get("/invoices/{$id}/xml")->getBody();
file_put_contents("rechnung-{$id}.xml", $xml); // reines XRechnung-XML (CII)Brauchst du zusätzlich einen menschenlesbaren Sichtbeleg, holst du dasselbe Dokument als
hybrides ZUGFeRD/Factur-X über den PDF-Pfad (GET /invoices/{id}/pdf) — siehe
ZUGFeRD/Factur-X per API erzeugen.
Ohne Guzzle: die curl-Erweiterung
Wenn du keine Abhängigkeit ziehen willst, geht der erste Call auch mit der eingebauten curl-Erweiterung:
<?php
$ch = curl_init(getenv('FORTLAUF_API_URL') . '/invoices');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('FORTLAUF_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => json_encode([
'currency' => 'EUR',
'documentTarget' => 'b2g',
'buyer' => [
'name' => 'Landkreis Eldenrode — Eigenbetrieb Abfallwirtschaft',
'addressLine1' => 'Am Anger 7',
'postalCode' => '06479',
'city' => 'Eldenrode',
'countryCode' => 'DE',
],
'buyerElectronicAddress' => '<LEITWEG-ID>',
'buyerReference' => '<LEITWEG-ID>',
'invoiceTypeCode' => '380',
'lineItems' => [[
'description' => 'Softwarepflege Wiegesystem (2. Halbjahr 2026)',
'quantity' => '6', 'unitCode' => 'MON',
'unitPriceNet' => '480.00', 'taxRatePercent' => '19',
]],
]),
]);
$invoice = json_decode(curl_exec($ch), true);Worauf es in PHP ankommt
- Beträge als Strings. Nie
floatfür Geld — schick"1250.00", nicht1250.0. Der Server rechnet in ganzzahligen Minor-Units. Warum das Validierungsfehler vermeidet, steht unter KoSIT-Validierung erklärt. - Idempotenz. Ein stabiler
Idempotency-Keypro Rechnung macht Retries sicher — gerade in Webhook- oder Queue-Workern, die einen Job wiederholen. - Kein Format selbst pflegen. Codelisten, Geschäftsregeln und der Übergang zu XRechnung 4.0 liegen hinter der API; deine PHP-Integration bleibt unverändert.
Der gezeigte Code ist illustrativ: Die vollständige Dokumentation steht dir in der API-Referenz zur Verfügung. Base-URL und Test-Key bekommst du mit dem Sandbox-Zugang im Entwickler-Bereich. Die Einordnung selbst bauen vs. einbinden steht unter E-Rechnung selbst bauen oder eine API einbinden?.
Selbst ausprobieren? Der Zugang ist kostenlos und ohne Karte — Produkt-Einstieg für Entwickler →
Oder den eigenen Abrechnungsfluss konkret durchsprechen: 30 Min, technisch, mit dem Gründer →