Check a UBL or CII invoice in your browser

two spellings of the same invoice.

Someone has sent you an electronic invoice as an XML file, or your software has produced one, and you want to know whether anything in it is wrong before it goes any further. The check reports every problem it finds, not just the first.

The file will be in one of two formats. EN 16931 is the European standard that says what an electronic invoice must contain, and UBL and CII are the two XML file formats such an invoice can be written in (this page reads UBL 2.1 and CII D16B).

You do not need to know which one you have. Paste the file into the playground, read the findings, and come back here for what the check does and does not cover.

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 two formats, so what the engine can do is a two-column question. Some cells carry a qualification instead of a plain yes or no. Read the whole cell before relying on it. The check described here looks at the invoice data, not the XML structure; the table's closing sentence calls that a semantic preflight.

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; container checked
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 API, every plan (watermarked on Free)
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 stated totals do not match its lines produces a finding.
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. Everything the parser could not use is reported.
checking a file you were sent
import { readFile } from "node:fs/promises";
import { validate } from "@attestwire/en16931";

const result = validate(await readFile("invoice.xml"));
const { invoice, unmapped } = result;

validate tells UBL from CII by itself and reads a Factur-X PDF too. The readers underneath are parseUbl and parseCiiInvoice, for when you want only the invoice object. 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 rather than dropped, because 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 }).

The parser does not process DTDs or expand custom entities, 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

The xpath on a finding describes where the field lives in a UBL invoice. It is not a pointer into your own 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 field lives" and map it to your file yourself.

A Factur-X or ZUGFeRD PDF cannot be pasted here. Those are a PDF invoice with the CII XML attached inside it (a PDF/A-3 container). Extract the XML first, which extractFacturX(bytes) does in the library, then check that, or send the PDF itself to the hosted API as application/pdf.

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, or, on the Solo, Starter, and Scale plans, ask the hosted API for ?format=pdf, which returns the Factur-X PDF. The PDF boundary in both directions

This is a preflight

Attestwire reads your XML into the input model and checks the model. In other words, we check the invoice data, not the XML structure. Receivers also run Schematron, the official rule files that check the XML itself, and a receiver that checks structure can still refuse a document this engine passed.

The rules that constrain the XML rather than the data in it do not run here. 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 file formats an EN 16931 invoice can be written in, and they carry 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 format carried inside ZUGFeRD and Factur-X PDFs.

The fields (BT-1, BT-10, BG-16 and so on) 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 may contain invoice XML as part of a ZUGFeRD or Factur-X file. An ordinary PDF may have no invoice XML. 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. 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. This check reads the invoice data and tests it against the rules; the rules that constrain the XML structure itself do not run at all. A document this engine accepts can still be rejected by the official national checker, an access point, a national add-on or the receiver. What a passing result means

Check your file · Which rules apply, if it is a German invoice · Do this in your own code · Which Peppol identifier the buyer address should carry · Do it over HTTP