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 steht offen in der API-Referenz, einen Test-Key (sk_test_…) legst du dir selbst im Kontobereich an; hier kommen beide 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;

// Base-URL mit /v1 und abschließendem Slash, Pfade unten OHNE führenden Slash —
// sonst ersetzt Guzzle den Pfad der base_uri und das /v1 fällt weg (HTTP 404).
$http = new Client([
    'base_uri' => rtrim(getenv('FORTLAUF_API_URL'), '/') . '/',
    '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 — dort steht auch die Base-URL. Den Test-Key legst du dir selbst im Kontobereich an (Schritt für Schritt im Quickstart). 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.