Skip to main content

ZUGFeRD, Factur-X and XRechnung CII

ZUGFeRD and Factur-X, and XRechnung when written as CII, use the UN/CEFACT Cross Industry Invoice syntax (rsm:CrossIndustryInvoice). Popular libraries produce it: horstoeko/zugferd (PHP, CII only), Mustang (Java) and factur-x (Python, PDF embedding).

Peppol carries UBL. POST /api/v1/documents refuses a CII document with 400 โ€ฆ [INVALID_ROOT] โ€ฆ got 'CrossIndustryInvoice' โ€” GoRoute does not convert it for you. So the question is how your invoice becomes UBL. There are three good answers.

RouteBest whenYou keep
A. Produce UBL directlyYou can change which writer your code callsYour own XML, end to end
B. Convert CII to UBLYour CII pipeline is fixed and you only need a delivery stepYour CII for archive and email; UBL for Peppol
C. Let GoRoute build itYou have the invoice data, not just the XMLOne call; GoRoute renders UBL, XRechnung or CII as the buyer needs

A. Produce UBL directlyโ€‹

Many libraries write both syntaxes. Check whether yours can export UBL โ€” for EN 16931 and XRechnung the UBL and CII forms carry the same business content. For example, horstoeko/zugferd's README names its successor horstoeko/invoicesuite, which covers ZUGFeRD, Factur-X and XRechnung in both CII and UBL. In PHP, josemmo/einvoicing writes UBL with a Peppol preset.

Then follow Send XML you already generate.

B. Convert CII to UBLโ€‹

horstoeko/zugferdublbridge converts ZUGFeRD/Factur-X CII to Peppol UBL (and back). Its README marks it experimental, so validate every converted document before you rely on the pipeline.

composer require horstoeko/zugferdublbridge guzzlehttp/guzzle
use horstoeko\zugferdublbridge\XmlConverterCiiToUbl;
use GuzzleHttp\Client;

// $ciiXml: the CII you already produce, e.g. with horstoeko/zugferd
$ublXml = XmlConverterCiiToUbl::fromString($ciiXml)->convert()->saveXmlString();

$goroute = new Client(['base_uri' => 'https://app.goroute.ai/peppol-api/', 'timeout' => 30]);
$headers = ['X-API-Key' => getenv('GOROUTE_API_KEY')];

// 1. Check the converted document before it leaves
$check = json_decode((string) $goroute->post('api/v1/documents/validate', [
'headers' => $headers,
'json' => ['document' => $ublXml],
])->getBody(), true);

if (!$check['valid']) {
throw new RuntimeException(json_encode($check['issues']));
}

// 2. Send it. The receiver comes from you, not from the XML.
$response = $goroute->post('api/v1/documents', [
'headers' => $headers + ['Idempotency-Key' => $invoiceNumber],
'json' => [
'receiver_scheme' => $buyerScheme, // e.g. "0204" for a Leitweg-ID
'receiver_id' => $buyerId,
'document' => $ublXml,
],
]);

Before you go live, look at the converted document's cbc:CustomizationID. GoRoute sends under the Peppol BIS 3.0 Invoice document type unless you pass document_type; an XRechnung or credit-note document needs its own value โ€” see Credit notes and other document types and the Germany guide.

Starting from a Factur-X or ZUGFeRD PDFโ€‹

A hybrid PDF carries its CII as an embedded file. GoRoute does not accept the PDF itself. Extract the XML first โ€” the factur-x Python package (pip install factur-x) ships a facturx-pdfextractxml command-line tool for this โ€” and then convert the extracted CII as above. The extracted XML is still CII: sending it to POST /api/v1/documents without converting it is refused.

Java (Mustang) and other CII writersโ€‹

The same rule applies to any library that writes CII: either export UBL (route A), convert the CII with a converter you trust and validate the result (route B), or hand GoRoute the data (route C).

C. Let GoRoute build itโ€‹

If your system holds the invoice data โ€” seller, buyer, lines, VAT โ€” you do not need to produce XML at all. POST /api/v1/invoices takes the invoice as JSON, renders the right syntax for the buyer and delivers it:

{
"invoice": { "โ€ฆ": "your canonical invoice" },
"document_format": "auto",
"delivery_channel": "auto"
}
You needSet
Peppol BIS 3.0 or PINT UBL for a buyer on the networkdocument_format: "auto" (default) or "ubl"
XRechnung 3.0 (UBL) for a German public buyerdocument_format: "xrechnung"
A ZUGFeRD / Factur-X PDF to email to a buyer who is not on Peppoldocument_format: "cii" (or "zugferd") with delivery_channel: "email"

CII is an email format here: asking for cii with delivery_channel: "peppol" is refused with CII_IS_EMAIL_ONLY. To check an invoice against the full official Schematron before you send it, call POST /api/v1/invoices/validate/deep with the same invoice data. See Send an invoice and Delivery and formats.