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

  1. Anlegen — du schickst Kunde und Positionen, der Server rechnet Summen und USt.
  2. Ausstellen — die Rechnungsnummer wird atomar vergeben, das XML gerendert und gegen die EN-16931-Geschäftsregeln validiert.
  3. 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
<?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 berechnet

2) 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
<?php

$issued = $http->patch("/invoices/{$id}/finalize");
// → Status "issued", Nummer vergeben, XRechnung-XML (CII) gerendert + validiert

3) XML abholen

PHP
<?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
<?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 float für Geld — schick "1250.00", nicht 1250.0. Der Server rechnet in ganzzahligen Minor-Units. Warum das Validierungsfehler vermeidet, steht unter KoSIT-Validierung erklärt.
  • Idempotenz. Ein stabiler Idempotency-Key pro 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 →

Häufige Fragen

Wie erstelle ich eine XRechnung mit PHP?
Du kannst das XML mit einer Bibliothek selbst bauen oder die Erzeugung an eine API delegieren. Der API-Weg ist drei HTTP-Calls: POST zum Anlegen der Rechnung (der Server rechnet die Summen), PATCH zum Ausstellen (Nummer wird vergeben, XML gerendert und validiert) und GET zum Abholen des XRechnung-XML. In PHP genügen dafür Guzzle oder die curl-Erweiterung.
Sollte ich XRechnung-XML in PHP selbst generieren?
Nur, wenn du bereit bist, Codelisten, EN-16931-Geschäftsregeln und die jährlichen Profilstände selbst nachzuziehen. Eine API kapselt diese Pflege: Deine PHP-Integration schickt die Geschäftsdaten und bleibt stabil, während der Dienst die Formatstände aktualisiert. Für die meisten SaaS- und Shop-Anbindungen ist das der geringere Wartungsaufwand.
Wie vermeide ich doppelte Rechnungen bei Retries in PHP?
Setze beim Anlegen einen Idempotency-Key-Header mit einem stabilen, pro Rechnung eindeutigen Wert. Ein wiederholter POST mit demselben Key ist dann ein Replay statt eines Duplikats. Beträge und Mengen schickst du als DECIMAL-Strings (z. B. "1250.00"), damit die Float-Arithmetik von PHP die Summen nicht verfälscht.