Every published version of @attestwire/en16931, newest
first. Each entry explains the changes users need to know. The hosted API and MCP tools also use this library; check their deployed version before relying on a particular release. The full entry for each
version, with error codes, migration notes and rule-by-rule detail, is
in the package changelog.
Say what happened instead of the VAT code, and the Factur-X container is checked
An invoice line can say vatScenario: "intra-eu-services", or domestic, intra-EU goods, export or the small-business exemption, and the engine writes the VAT category, the exemption code and its text; createCreditNote builds the credit note for an invoice. validate now reports what a Factur-X or ZUGFeRD PDF says about its invoice in the attachment and the XMP metadata, warns about a Leitweg-ID, IBAN, BIC, SIREN or SIRET with the wrong check digits, and recipientClass sorts every finding the way the German BMF letter of October 2025 does.
KoSIT accepts all 18 document shapes the new inputs produce.
Peppol BIS Billing 3.0.21: a malformed Danish identifier now fails, as it does on the network
This release follows Peppol BIS Billing 3.0.21, which the Peppol network has required since 17 August 2026. A malformed Danish P-number (scheme 0096) or SE number (scheme 0198) now makes a Peppol invoice invalid instead of drawing a warning: PEPPOL-COMMON-R052 and PEPPOL-COMMON-R053. Five new warnings check Dutch identifiers, and the address schemes and currencies Peppol accepts follow the release: 14 schemes are gone, and STN replaces STD.
OpenPEPPOL’s own 3.0.21 rules accept all ten sample documents, and nine probe documents draw the same rule ids from them as from this engine.
A Peppol warning that Peppol itself retired is no longer reported
PEPPOL-COMMON-R048, a warning about an Italian VAT number used as a Peppol address under scheme 9906, is gone. Peppol dropped scheme 9906 in November 2022 and switched this check off with it, so the reference validator never raises it. A 9906 address is still refused, by PEPPOL-EN16931-CL008, so no verdict changes; an Italian VAT number goes under scheme 0211, where PEPPOL-COMMON-R047 runs the same check.
A Factur-X PDF whose XML is not UTF-8 is refused by name
A Factur-X or ZUGFeRD PDF whose attached XML is ISO-8859-1, or any encoding other than the UTF-8 those formats require, used to be read with a replacement character in place of every umlaut, and could pass. validate now answers it with one fatal AW-PARSE finding that names the encoding, and extractFacturX throws the new FacturXEncodingError.
A file converted to UTF-8 whose declaration still names ISO-8859-1 is refused the same way instead of being read as mojibake, and the API answers both with a 415 that says what to fix.
A VAT rate passed as a fraction is caught, and the API reads your PDF
A standard-rated line at vatRate: 0.19 used to validate clean and generate a hundredth of its VAT; it now draws ATW-VAT-RATE-FRACTION, which names every line that carries the rate and what it costs, and BR-CO-17 says what the rate is as a percentage. validateInput handed XML, a file’s bytes or no body at all returns one finding that names the right function instead of throwing, and every input field is documented where your editor shows it.
On the API, /v1/validate and /v1/parse now read a Factur-X or ZUGFeRD PDF sent as application/pdf, and the docs show each call in seven languages.
Check a file in one call, and see the line each finding is about
validate(document) takes UBL or CII XML, as text or bytes, or a Factur-X PDF, and returns the same findings as validateInput plus the syntax, the invoice as read and, on every finding, the line and column in your file and an XPath in the file’s own syntax; CII documents get CII paths.
It never throws for anything about the file. The VAT breakdown a document states is now judged in full: a group missing its taxable amount, VAT amount, category or rate fails BR-45 to BR-48, as it does at KoSIT, so every rule the library ships is now reachable.
On the API, paid plans can ask /v1/generate for the Factur-X PDF itself.
A command line, and closer agreement with KoSIT on real documents
npx @attestwire/en16931 invoice.xml validates UBL, CII and Factur-X files, or a whole directory, with no code and no account. Documents read from XML are now judged on the figures they state, not only on figures recomputed from their lines, so invoices that passed here and failed KoSIT now fail here too.
JSON input changes less: there are new checks on values the generators cannot write, such as a field of the wrong type; BR-S-05 judges a VAT rate as it will be written; and VAT in a currency with no minor unit is rounded to whole units.
Every monetary sum is now added in whole cents rather than floating-point numbers, so a 10,000-line invoice totals to the same cent as a pencil-and-paper sum; it is checked against an independent exact calculation. Exact cents need a ceiling, so no amount or total may exceed 999,999,999,999.99.
An invoice over it is a fatal ATW-AMOUNT-OUT-OF-RANGE finding — in practice a price entered in cents instead of euros. Before, such an invoice could come back valid.
Measured against the regulators, and nine defects closed
A new private harness runs every document in the KoSIT, CEN and OpenPeppol test suites through this library and through the regulators’ own validators, then diffs the two verdicts rule by rule. Agreement over the rules we claim to implement went from 18.59% to 96.08%, and the count of rules an official validator raises and we miss is now zero.
The largest change: BR-CO-13, BR-CO-15 and BR-CO-16 compared your declared totals against a recomputation of your lines instead of against the chain the document itself states, so one line rounding a cent away produced three fatal findings on invoices every other validator accepts. Eight more fixes follow the same shape, plus two the other way: a seller with only a trading name, and an amount serialised at six decimal places, are now caught here rather than at the portal.
Verdicts change in both directions; the package changelog lists every rule id.
A tax total we could not read no longer passes silently
An invoice whose declared VAT total was written 12,34 left BT-110 unset, so BR-CO-14 never ran and the document came back valid while KoSIT rejects it at the schema step. BT-110 and BT-111 now flow through the same defect machinery as the other declared totals, in both syntaxes: an absent, empty or unreadable value is a finding rather than silence.
Every numeric read now follows the exact xs:decimal lexical space, so exponent and hex forms (1e2, 0x1F) are reported unreadable instead of being quietly accepted by JavaScript. And validation got about 3.5× faster — 10.4 ms to 2.9 ms on a 1,000-line invoice, 83 µs to 22 µs on a typical one — by computing the totals once per run instead of once per rule family; a 2,272-case differential harness confirmed the findings are byte-identical before and after.
The published dist is byte-for-byte what 0.7.0 shipped: same rule set, same exports, same behaviour, so there is nothing here to upgrade for. What changed is how the package describes itself on npm. The description now leads with validation, and it states in the first line that Factur-X and ZUGFeRD support is the CII payload: the PDF container is read and never written. keywords went from 18 to 25.
Factur-X PDF reading, Peppol fixes, SARIF and JUnit export
New extractFacturX(bytes) reads the XML invoice out of a Factur-X or ZUGFeRD PDF, with zero dependencies and hostile-input hardening, so the file your customer sent is one call from validation; the container is still never built. Peppol changed behaviour: credit notes are now judged by PEPPOL-EN16931-P0101 instead of being wrongly caught by P0100, and peppol-bis-3 gained CII generation.
The recorded OpenPeppol run went from 3 of 5 accepted to 10 of 10. And toSarif / toJunitXml turn any validation into a CI artifact your pipeline already understands.
Reading a document, the engine used to keep a declared total only when it parsed as a number, so a PayableAmount that was absent, empty, or written 12,34 was dropped, nothing compared it, and the file came back valid. KoSIT rejects every one of those.
Now an absent total fails BR-12, BR-13, BR-14 or BR-15, and a total we cannot read as a number fails ATW-DECLARED-TOTAL-NOT-A-NUMBER, quoting the text it saw. Documents that used to pass will now fail, which is why this is a minor version rather than a patch.
Building an invoice from the JSON model is unchanged: omit a total and we still compute it for you.
Set invoiceTypeCode to 381 on the invoice you already build and you get a UBL CreditNote or a CII type-code-381 document: generated, parsed and validated, with the whole rule set running unchanged. Four new fixtures took the recorded KoSIT run to 11 documents, all acceptable.
The XML reader also stopped accepting two ill-formed shapes it should always have refused: a duplicated attribute, and a comment containing --. Both now raise XmlSyntaxError, so a generator of your own that emitted either will now be told.
Security and arithmetic fixes, and much tighter parser caps
A four-lens adversarial review, with every defect reproduced before it was fixed and most of them checked against KoSIT in both syntaxes. Read the migration note before upgrading: the default XML parser caps dropped 25×, so a document carrying a base64 attachment of about 300 kB is now refused where 0.3.0 read it.
generateCii emits UN/CEFACT CII D16B from the same input model the UBL generator takes, and parseCiiInvoice reads one back. That opened XRechnung CII and the Factur-X EN 16931 payload: the XML, never the PDF container.
No code changed. The published README recorded the conformance run it had said was still pending, and a release gate now fails the build whenever the package and its run log disagree.
Discounts, surcharges, prepayments, periods and attachments
The input model grew the EN 16931 groups it could not previously express, and the rules for those groups shipped in the same release, so nothing is modelled without being checked. Existing 0.1.x input validates and generates identically.
Asking for a CII profile no longer returns UBL wearing a CII customization id, and a credit-note type code is refused rather than generated as an invoice.
JSON in, XRechnung UBL out, with a rule set that explains itself.
Versions and dates on this page are parsed from
the package's own changelog at build
time. @attestwire/en16931 is published, still 0.x, so minor
versions can change behaviour; the entry says when one does.
Every entry on this page was a change somebody had to notice; 0.6.0, for example, made documents that used to pass fail. Leave an address and we will email you when the ruleset changes.
One email when the ruleset changes, nothing else. We store the address, the date
you gave it and which page you used (and nothing
else), and we delete it the day you ask.