normbill/xrechnung

PHP client for the normbill e-invoicing API: generate, validate and parse XRechnung, ZUGFeRD/Factur-X and Peppol BIS Billing 3.0 invoices. Every validation error maps to the exact field in your request, with a plain-language fix.

v0.1.1 2026-07-17 22:09 UTC

This package is auto-updated.

Last update: 2026-07-17 22:10:57 UTC


README

XRechnung mit PHP erzeugen — in einem API-Aufruf, KoSIT-validiert. Rechnungsdaten als JSON-Array rein, konforme XRechnung (UBL oder CII), ZUGFeRD/Factur-X oder Peppol BIS 3.0 raus. Schlägt die Validierung fehl, bekommen Sie keinen kryptischen Schematron-Code, sondern das exakte Feld in Ihrem Request samt Klartext-Erklärung und konkretem Fix — z. B. invoice.buyer_reference statt [BR-DE-15] cbc:BuyerReference. Validierung ist im Rahmen der Fair-Use-Grenzen kostenlos; ein großzügiger Gratis-Tarif reicht für Integration, Tests und den Start. API-Key auf normbill.com.

PHP client for the normbill e-invoicing API. Plain PHP ≥ 8.1 with ext-curl — no framework, no XML libraries, no Java toolchain.

normbill vs. horstoeko/zugferd (honest comparison)

horstoeko/zugferd is an excellent open-source library — if you want to own XML generation, use it. The difference:

horstoeko generates the XML. normbill generates it, validates it against the official KoSIT validator (the legal reference implementation), tells you which field in your request to fix when a rule fails, and tracks spec version bumps (XRechnung 4.0 is expected late 2026) for you.

With a library, Schematron validation, error interpretation and every future XRechnung/ZUGFeRD version bump stay on your roadmap. With normbill they are the product.

Install

composer require normbill/xrechnung

Quickstart

use Normbill\Xrechnung\NormbillClient;

$normbill = new NormbillClient(getenv('NORMBILL_API_KEY'));

$result = $normbill->generate([
    'format'  => 'xrechnung-3.0',
    'invoice' => [
        'number'          => 'INV-2026-0001',
        'issue_date'      => '2026-06-15',
        'due_date'        => '2026-07-15',
        'buyer_reference' => '04011000-1234512345-06', // Leitweg-ID
        'seller' => [
            'name'    => 'Muster Lieferant GmbH',
            'vat_id'  => 'DE123456789',
            'address' => ['country' => 'DE', 'city' => 'Berlin', 'postal_code' => '10115'],
            'contact' => ['name' => 'Erika Muster', 'phone' => '+49 30 1234567', 'email' => 'billing@lieferant.example'],
        ],
        'buyer' => [
            'name'    => 'Beispiel Kunde AG',
            'address' => ['country' => 'DE', 'city' => 'Hamburg', 'postal_code' => '20095'],
            'contact' => ['email' => 'ap@kunde.example'],
        ],
        'payment' => ['iban' => 'DE89370400440532013000'],
        'lines'   => [
            ['name' => 'Beratungsleistung', 'qty' => 10, 'unit_price' => 100, 'vat' => 19],
        ],
    ],
]);

$xml    = $result['xml'];        // KoSIT-validated XRechnung 3.0 (UBL 2.1)
$report = $result['validation']; // full validation report

When validation fails, you know what to fix

use Normbill\Xrechnung\Exception\ValidationFailedException;

try {
    $normbill->generate($request);
} catch (ValidationFailedException $e) {
    foreach ($e->getIssues() as $issue) {
        // $issue['rule']    => 'BR-DE-15'
        // $issue['path']    => 'invoice.buyer_reference'   ← a field in YOUR request
        // $issue['message'] => 'The buyer reference (Leitweg-ID) is missing.'
        // $issue['fix']     => 'Set invoice.buyer_reference, e.g. "04011000-1234512345-06".'
        error_log(sprintf('%s @ %s: %s', $issue['rule'], $issue['path'], $issue['message']));
    }
}

API

Method Endpoint What it does
generate(array $request, ?string $idempotencyKey = null) POST /v1/invoices/generate Invoice JSON → validated e-invoice (XML; base64 PDF for hybrid formats)
validate(string $content, ?string $format = null) POST /v1/invoices/validate Existing XML → validation report with reverse-mapped fixes. Free within fair use.
parse(string $content) POST /v1/invoices/parse Inbound XRechnung/ZUGFeRD XML → invoice array (the receive side of the mandate)

validate() always returns the report — an invalid document is a result, not an exception. Check $report['valid'].

Formats

format Syntax Validation
xrechnung-3.0 UBL 2.1 official KoSIT validator
xrechnung-cii-3.0 CII (UN/CEFACT D16B) official KoSIT validator
peppol-bis-3.0 UBL 2.1 (BIS Billing 3.0) structural tier
zugferd-2.x / facturx-1.0 CII in hybrid PDF/A-3 structural tier, paid plans

Retry-safe generation (idempotency)

Pass an idempotency key and retries can never double-generate or double-bill — same key + same body replays the original result (honoured for 24 hours):

$normbill->generate($request, 'order-' . $orderId);

Quota warnings

From 80% usage every response carries an X-Quota-Warning header:

$quota = $normbill->getLastQuota();
// ['limit' => 50, 'remaining' => 9, 'warning' => '82% of monthly documents used']

Exceptions

All exceptions extend Normbill\Xrechnung\Exception\NormbillException:

Class When
ValidationFailedException 422 — getReport() / getIssues() (rule, path, message, fix)
AuthenticationException 401 — missing/invalid API key
RateLimitException 429 — quota or burst limit; getRetryAfter()
ApiException any other non-2xx; getStatus() / getProblem() (RFC 9457)
ConnectionException network failure / timeout

Privacy

normbill never persists invoice content — payloads are request-scoped, processed in the EU, and gone when the response is sent.

Tests

composer install
composer test

The test suite stubs the HTTP transport (no network, no API key needed). It is not executed in this repository's CI yet — run it locally or wire it into your pipeline.

Links

MIT © VIBOA UG (haftungsbeschränkt)