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.

CapabilityUBL 2.1CII 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

WhatDetail
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.
reading a file you were sent
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.

ErrorWhen
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 →

Next.

Check your file → · Which rules apply, if it is a German invoice → · Do this in your own code → · Do it over HTTP →