UBL vs CII: the two syntaxes of EN 16931
Every EN 16931-based e-invoice is written in one of exactly two XML syntaxes: UBL or CII. They express the same semantic model — same business terms, same business rules — but the XML on the wire looks nothing alike. Here's the practical difference.
UBL 2.1
UBL (OASIS Universal Business Language) uses descriptive element names — you can often guess what a tag means without a spec open:
<cbc:ID>INV-2026-0042</cbc:ID>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName><cbc:Name>Acme Ltd</cbc:Name></cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>
cbc: (Common Basic Components) holds leaf values,
cac: (Common Aggregate Components) holds structure. UBL is
the only syntax Peppol BIS 3.0 accepts — if you're sending through
the Peppol four-corner network, this is your format, no choice involved.
CII D16B
UN/CEFACT Cross Industry Invoice has EDI roots and reads more like a generic message envelope than a labeled form:
<ram:ID>INV-2026-0042</ram:ID> <ram:SellerTradeParty> <ram:Name>Acme Ltd</ram:Name> </ram:SellerTradeParty>
rsm: wraps the whole message, ram:
(Reusable Aggregate Business Information Entity) holds most of the
content. CII is the syntax embedded inside Factur-X and ZUGFeRD
hybrid PDFs, and it's one of the two syntaxes XRechnung accepts.
Same rules, different shape
Because both syntaxes are restatements of the same EN 16931 business
terms, the same BR-* rule codes apply to both — a missing
buyer VAT identifier is BR-08 whether you wrote UBL or CII.
The location of the error in the XML differs, but the rule and its
fix don't.
Converting between them
This shared model is what makes lossless conversion possible. Our
/v1/convert/ubl-to-cii and /v1/convert/cii-to-ubl
endpoints do exactly that:
curl -X POST "$BASE/v1/convert/ubl-to-cii?profile=en16931" \ -H "X-API-Key: $KEY" -H "Content-Type: application/xml" \ --data-binary @invoice-ubl.xml -o invoice-cii.xml
Conversion is lossless or rejected: if the source uses something
outside the shared EN 16931 subset (document-level allowances/charges,
prepaid amounts, attachments), you get
422 unsupported_features with the exact list, never a quietly
degraded invoice. Declared totals are re-checked against EN 16931
arithmetic on the way through, and if the source is valid but the
target profile needs something the source didn't have (Peppol
endpoint IDs, for instance), you get
422 target_profile_unsatisfied with the full validation
report instead of a half-built document.
Picking one
In practice you rarely "pick" — the receiving side decides. Peppol means UBL. Factur-X/ZUGFeRD means CII. XRechnung takes either. The practical move is to keep your own canonical invoice model format-agnostic and generate (or convert) into whichever syntax the destination actually wants, rather than maintaining two parallel originals. See the UBL/Peppol and CII format pages for full endpoint tables and sample files.
Keep reading
Want to try this against a real invoice?
Every example above calls the same API you can call today — validate for free, or read the full reference.