CRATEAPI

UBL vs CII: the two syntaxes of EN 16931

Standards2 min read

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.