invoicexml / zugferd-xrechnung
PHP client for ZUGFeRD and XRechnung e-invoices: create, validate, embed, render and extract EN 16931 invoices with the InvoiceXML API.
Package info
github.com/InvoiceXML/invoicexml-php-zugferd-xrechnung
pkg:composer/invoicexml/zugferd-xrechnung
Requires
- php: ^8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.5
- guzzlehttp/psr7: ^2.4
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A thin PHP client for the InvoiceXML API that creates, validates, embeds, renders and reads German e-invoices:
- ZUGFeRD: hybrid PDF/A-3 invoices with embedded EN 16931 CII XML, for B2B.
- XRechnung: XML invoices in UBL or CII syntax checked against the KoSIT rules, for the public sector.
You send an invoice as a PHP array, and the API computes totals and the VAT breakdown, builds the XML and the PDF/A-3, and validates the result before returning it. Your project needs no PDF library, no XML builder and no Schematron tooling.
Installation
composer require invoicexml/zugferd-xrechnung
Requires PHP 8.1 or newer. Guzzle is used by default, and any PSR-18 HTTP client works.
Quick start
use InvoiceXml\ZugferdXrechnung\Client; $client = new Client(getenv('INVOICEXML_API_KEY')); $pdf = $client->createZugferd([ 'invoiceNumber' => 'INV-2026-00045', 'issueDate' => '2026-05-17', 'currency' => 'EUR', 'seller' => ['name' => 'Acme Engineering GmbH', 'vatIdentifier' => 'DE123456789', 'postalAddress' => ['line1' => 'Hauptstraße 12', 'city' => 'Berlin', 'postCode' => '10115', 'country' => 'DE']], 'buyer' => ['name' => 'Globex SAS', 'postalAddress' => ['line1' => '15 rue de Rivoli', 'city' => 'Paris', 'postCode' => '75001', 'country' => 'FR']], 'paymentDetails' => ['paymentAccountIdentifier' => 'DE89370400440532013000'], 'lines' => [['quantity' => 10, 'priceDetails' => ['netPrice' => 150], 'vatInformation' => ['rate' => 19], 'item' => ['name' => 'Senior consulting']]], ]); $pdf->save('invoice.pdf');
The result is a ZUGFeRD PDF/A-3 in the EN 16931 profile, already validated against the official rules.
Why InvoiceXML
- Rule sets updated on their legal effective dates. New KoSIT and FeRD releases go live server-side on the day they take effect. You never update a package or redeploy to stay compliant.
- Validated before delivery. Every created or embedded invoice passes the full rule set before it is returned. Data that cannot produce a compliant invoice is refused with the violated rules, so nothing broken reaches a customer.
- Zero data retention. Documents are processed in memory and discarded when the response is returned. Your data is never used for AI training.
- Processing in Frankfurt. All processing runs in Frankfurt, Germany.
- Integration assistance. When a rule finding needs a human explanation, a person who knows the rule sets answers your support request.
Get an API key
Sign up at invoicexml.com to start a 30-day free trial with 100 credits. No credit card is needed. Plans are listed on the pricing page.
Usage
Invoices use the field names of the EN 16931 InvoiceDocument model from the OpenAPI specification. The API explorer documents every field. Every method that takes a file accepts a path on disk or a File object.
Create a ZUGFeRD PDF
$pdf = $client->createZugferd($invoice, [ 'language' => 'de', // labels of the rendered PDF: en, de or fr 'brandColor' => '#0F766E', 'logoUrl' => 'https://example.com/logo.png', ]); $pdf->save($pdf->filename); // for example invoice-zugferd-INV-2026-00045.pdf
To keep your own invoice layout, pass 'pdfUrl' => 'https://...' and the API embeds the XML into that PDF instead of rendering one.
Create an XRechnung
XRechnung needs the Leitweg-ID in buyerReference, a seller contact and electronic addresses for both parties.
$xml = $client->createXrechnung([ 'invoiceNumber' => 'RE-2026-3311', 'issueDate' => '2026-07-28', 'dueDate' => '2026-08-27', 'currency' => 'EUR', 'buyerReference' => '08111-00234-45', 'seller' => [ 'name' => 'Neckarblick IT GmbH', 'vatIdentifier' => 'DE315498762', 'postalAddress' => ['line1' => 'Königstraße 82', 'city' => 'Stuttgart', 'postCode' => '70173', 'country' => 'DE'], 'contact' => ['name' => 'Miriam Vogel', 'phone' => '+49 711 2203980', 'email' => 'rechnung@neckarblick-it.de'], 'electronicAddress' => ['identifier' => 'DE315498762', 'schemeId' => '0204'], ], 'buyer' => [ 'name' => 'Landeshauptstadt Stuttgart, Schulverwaltungsamt', 'postalAddress' => ['line1' => 'Hauptstätter Straße 79', 'city' => 'Stuttgart', 'postCode' => '70178', 'country' => 'DE'], 'electronicAddress' => ['identifier' => '08111-00234-45', 'schemeId' => '0204'], ], 'paymentDetails' => ['paymentMeansCode' => 58, 'paymentAccountIdentifier' => 'DE75512108001245126199'], 'lines' => [[ 'quantity' => 210, 'unitCode' => 'HUR', 'item' => ['name' => 'Betreuung Schul-IT, Juli 2026'], 'priceDetails' => ['netPrice' => 92.00], 'vatInformation' => ['rate' => 19, 'categoryCode' => 'S'], ]], ]); $xml->save('xrechnung.xml');
The output uses UBL syntax by default. Pass ['syntax' => 'cii'] as the second argument for the CII syntax. Both pass the same German rules.
Validate
The API answers valid and invalid documents alike with a result, so check valid.
$result = $client->validateZugferd('incoming.pdf'); // or validateXrechnung('incoming.xml') if (!$result->valid) { foreach ($result->errors as $error) { echo "{$error->rule} ({$error->layer}): {$error->message}\n"; } }
Each finding carries the following fields:
ruleis the rule identifier, for exampleBR-DE-15.layerisxsd,en16931orcius. Aciusfinding means the invoice is valid EN 16931 but misses a German requirement.messageis a plain-language explanation.fieldsnames the invoice fields involved, for exampleseller.name.lineandrawgive the XML line and the verbatim validator output.
$result->data holds the verdict details, such as schemaValid, schematronValid and profile. $result->report is the field report: one row per invoice field with its value and its findings.
Embed XML into an existing PDF
$zugferd = $client->embedZugferd('layout.pdf', 'factur-x.xml');
The XML is validated before embedding. Pass skipValidation: true to embed it as is.
Render an XRechnung as PDF
$preview = $client->renderXrechnungToPdf('xrechnung.xml', language: 'de');
Read incoming invoices
$data = $client->extractJson('incoming.pdf'); // ZUGFeRD PDF or XRechnung XML $invoice = $data['invoice']; // same shape the create methods accept $zip = $client->extractAttachments('incoming.pdf'); // supporting documents as a ZIP
Extraction parses the embedded XML deterministically, with no AI involved. The returned invoice can go straight back into createZugferd() or createXrechnung().
Errors and rate limits
Any error status throws InvoiceXml\ZugferdXrechnung\ApiException. When a create or embed request is refused because the invoice would not be compliant, $e->errors lists the violated rules as findings.
use InvoiceXml\ZugferdXrechnung\ApiException; try { $client->createXrechnung($invoice); } catch (ApiException $e) { echo $e->statusCode, ' ', $e->errorCode, ': ', $e->detail, "\n"; foreach ($e->errors as $error) { echo "{$error->rule}: {$error->message}\n"; } }
errorCode is a stable numeric code, documented under error handling.
When too many requests for one API key run at the same time, the API answers 429 with a Retry-After header. The client waits that long and resends the request, up to 3 times. A request answered with 429 was not processed and not billed, so resending never creates duplicates. If the retries are used up, a TooManyRequestsException carries retryAfter in seconds. Set maxRetries: 0 to handle 429 yourself.
Configuration
$client = new Client( apiKey: getenv('INVOICEXML_API_KEY'), httpClient: $psr18Client, // any PSR-18 client, Guzzle by default requestFactory: $psr17Factory, // PSR-17 factories, Guzzle's by default streamFactory: $psr17Factory, maxRetries: 3, );
Links
- Documentation
- API explorer
- ZUGFeRD and XRechnung in PHP guide
- Factur-X package for PHP
- Contact and support
License
MIT