UBL and CII
Check a UBL or CII invoice — in your browser, without uploading it
Paste a UBL 2.1 Invoice or CreditNote, or a
UN/CEFACT CrossIndustryInvoice (D16B). It is parsed on your
machine. No key, no upload, no request.
Where your file goes
Your file is processed locally in your browser. It is not uploaded. No request carrying your document leaves this page, and no analytics event records anything about it. Open your network panel and watch, if you would rather see it than read it.
Paste your XML in the playground
What exists in each syntax
EN 16931 has exactly two syntaxes, so this is a two-column question. The cells that are not a plain yes or no carry the whole answer in the cell, because flattening them is how a reader ends up shipping the wrong thing.
| Capability | UBL 2.1 | CII D16B |
|---|---|---|
| Generate invoices | Yes | Yes |
| Generate credit notes | Yes | Yes |
| Read existing XML | Yes | Yes |
| Read a Factur-X or ZUGFeRD PDF | No PDF form exists | XML extracted (library) |
| XRechnung profile | Yes | Yes |
| Peppol BIS 3 | Yes | Optional binding (library); API emits UBL |
| Factur-X EN 16931 payload | No | XML payload only |
| Finished PDF/A-3 | No | No |
| Transmission | No | No |
Attestwire builds and checks the document data. Your access point or approved platform handles delivery. Uploaded XML gets a semantic preflight, not a receiver guarantee.
The same boundary, with everything around it →
What the reader gets back
| What | Detail |
|---|---|
| Both root elements, by URI | A UBL 2.1 Invoice or CreditNote, and a UN/CEFACT CrossIndustryInvoice (D16B). Namespaces resolve by URI, not by prefix, so a document using a: and b: reads the same as one using cac: and cbc:. Element order does not matter. |
| The document type, detected | Taken from the root element rather than asked for, and returned in invoiceTypeCode. A credit note read here regenerates as a credit note. |
| The totals the document states | BT-106 to BT-115 land in declaredTotals and are checked against ours under the BR-CO rules, so a document whose own arithmetic disagrees with its lines is a finding rather than a shrug. |
| Everything it could not use | Anything that did not reach the model comes back in unmapped with its path, its name and the reason. kind: "unknown" means there is no field for it and the content is gone from the model; kind: "recomputed" means the value is derived from the lines instead of stored. Nothing is dropped in silence. |
import { parseUbl, validateInput } from "@attestwire/en16931";
const { invoice, unmapped } = parseUbl(xmlString);
const findings = validateInput(invoice);
Use parseCiiInvoice for a CII document. Both return the same
shape, and both read credit notes as readily as invoices.
What it refuses
The XML came from someone else, so the reader throws rather than
returning a half-read invoice. Every error extends
ParseError and carries a stable code.
| Error | When |
|---|---|
UnsupportedSyntaxError |
The root element is neither a UBL invoice nor a UBL credit note. A CII document gets its own message naming parseCiiInvoice rather than a generic failure. |
XmlSecurityError |
The document hit a size, depth, element or attribute cap, declared a DOCTYPE, or used a custom entity. An unknown entity is refused, never silently dropped — dropping one would change the text of a tax document without saying so. |
XmlSyntaxError |
Not well-formed, or outside the accepted subset: mixed content, unbound prefixes, control characters XML 1.0 does not permit. |
The caps are memory limits, chosen from measurement:
8,000,000 characters,
50,000 elements,
100 levels deep and
256 attributes per element.
A UBL invoice nests about eight levels and a thousand-line invoice lands
around 300 kB, so ordinary documents are nowhere near them. Every one is
an option on the call: parseUbl(xml, { maxCharacters }).
There is no DTD processing and no custom entity expansion, which is what closes XXE and billion-laughs. Comments and processing instructions are skipped: a stylesheet instruction cannot make this library fetch anything.
Two things that catch people out
An xpath on a finding is documentation, not a
location in your file. It is a fixed UBL path per rule, and it
stays a UBL path on CII documents and on credit notes. Rules that exist
because the document is a credit note name
/ubl:CreditNote correctly; the rest do not. Read it as
"where this business term lives" and map it yourself.
A Factur-X or ZUGFeRD PDF cannot be pasted here. Those
are CII XML inside a PDF/A-3 container — extract the XML first, which
extractFacturX(bytes) does in the library, then check that.
In the other direction, what generation writes for
facturx-en16931 is the CII XML payload and not a Factur-X
file: take it to a PDF/A-3 library to build the container.
The PDF boundary in both directions →
This is a preflight
Attestwire reads your XML into the input model and checks the model. That is a semantic preflight, not an XSD or Schematron pass over the document, and it is not a receiver's verdict — rules that constrain the XML rather than the data in it do not run. The limits page lists every gap rather than a representative sample.
Questions people ask next
What is the difference between UBL and CII?
They are the two XML syntaxes EN 16931 defines for the same invoice. UBL 2.1 is the OASIS binding, with a separate Invoice and CreditNote document and the cac:/cbc: element families. UN/CEFACT CII (D16B) is one CrossIndustryInvoice document for both, with the ram: family, and it is the syntax carried inside ZUGFeRD and Factur-X PDFs. The business terms — BT-1, BT-10, BG-16 — are identical in both; only the spelling changes.
How do I know which syntax my invoice uses?
Look at the root element. <Invoice> or <CreditNote> in the OASIS UBL namespace is UBL; <rsm:CrossIndustryInvoice> is CII. If the file is a PDF, it is a ZUGFeRD or Factur-X container with CII XML attached inside. You do not have to work it out to check the file — the playground reads the root element itself and tells you what it found.
Can I validate against EN 16931 offline?
Yes. The MIT package has zero runtime dependencies and makes no network call, so parseUbl plus validateInput runs entirely in your own process — including in CI, where the GitHub Action bundles a pinned engine and never leaves the runner. The browser check on this site is the same code compiled for a tab.
Is my invoice uploaded when I check it here?
No. The playground loads the engine as a module and runs it in your tab, so no request carrying your document leaves the page. If you would rather check that than take our word for it, open your network panel first. The one door that does receive the document is the hosted API, and payloads there are processed in memory and never stored (security).
Does a clean result mean my customer will accept the invoice?
No. What runs is a semantic preflight over the parsed model: the rules that constrain the XML itself, rather than the data in it, do not run at all. A document this engine accepts can still be rejected by the reference validator, an access point, a national overlay or the receiver. What a passing result actually means →