Library
An EN 16931 engine you can npm install
MIT, TypeScript-first, zero runtime dependencies. It runs locally and offline, and generation and validation come off one invoice model.
npm install @attestwire/en16931
v0.7.3 on npm, published with provenance attestations · source at GitHub · no account, no key, no network call.
Validate, then generate
The successful path first. This uses one of the package's own exported fixtures, so you can paste it into an empty project and get the comment on the last line — the build runs it before printing it.
import {
generateXRechnungUBL,
minimalXRechnung,
validateInput,
type InvoiceInput,
} from '@attestwire/en16931';
// Swap this for your own object. Keep the type on it: without it,
// TypeScript widens `profile` to `string` and this stops compiling.
const invoice: InvoiceInput = minimalXRechnung;
const { valid, errors } = validateInput(invoice);
const xml = valid ? generateXRechnungUBL(invoice) : '';
console.log(valid, errors.length, xml.startsWith('<?xml')); // true 0 true
Swap the fixture for your own object and the types do the rest. The
annotation is not decoration: profile is a union of string
literals, so an unannotated object widens it to string and
stops compiling. That is the compiler catching a profile the generator
would have thrown on.
The type surface, in full
7 symbols are the whole integration. There is no client to construct, no configuration object and no lifecycle.
| Export | What it is |
|---|---|
InvoiceInput |
The one input model, and the whole contract. profile is a union of string literals, so a profile the generators would refuse does not compile. |
validateInput |
Runs every input rule and returns { valid, profile, errors, warnings, information }. It reports every finding, not the first. |
TeachingError |
One finding: rule, field, severity, message, fix, xpath, docsUrl, example. The same object in all three doors. |
generateXRechnungUBL |
Model → UBL 2.1 Invoice XML, or CreditNote when the type code is a credit-note code. |
generateCii |
Model → UN/CEFACT CrossIndustryInvoice (D16B) XML. XML only: this function never writes a PDF. |
parseUbl |
UBL XML → { invoice, unmapped, customizationId, profileId }. Reads invoices and credit notes alike. |
parseCiiInvoice |
The same, for a CII document — where invoices and credit notes are one document type. |
The package exports considerably more — the official code lists, the totals helpers, the CI report writers, the Factur-X extractor — and the package README tables all of it. None of it is needed to get a document out.
The profile field is the control
One field decides which rule sets run and which syntax comes out. Set it and everything else follows.
-
generateXRechnungUBLwrites UBL 2.1 foren16931,xrechnung-ubl,peppol-bis-3. -
generateCiiwrites CII D16B foren16931,xrechnung-cii,facturx-en16931,peppol-bis-3.
Credit notes are one field too: invoiceTypeCode: "381" emits
a ubl:CreditNote in UBL and ram:TypeCode 381 in
CII, with the same rule set applied.
facturx-en16931 writes the Factur-X CII XML payload and
nothing else. Building the PDF/A-3 container around it is a separate
step, with a separate library.
Where the PDF boundary sits →
Refusals are typed
Generation refuses rather than emitting a document that will be rejected
downstream. Both errors extend GenerationError, carry a
stable code, and say in the message what is supported.
| Error | When |
|---|---|
UnsupportedProfileError |
From generateXRechnungUBL, when the profile is a CII one. |
UnsupportedCiiProfileError |
From generateCii, when the profile is xrechnung-ubl — the name of the UBL binding specifically. |
Totals are computed from the lines, never echoed from your input, so an
arithmetic rejection cannot originate in the document we wrote. Rounding
is half-up at two decimals on each line before the sum, which is what
BR-CO-10 asks for and what both obvious JavaScript
approaches get wrong.
Library or hosted API?
Same engine, same rules, same findings. The question is where it runs.
| Use the library when… | Use the API when… |
|---|---|
| Your application runs JavaScript or TypeScript | Your application uses another language |
| You want processing to stay entirely local | You want a managed endpoint |
| You prefer to control when the rule set changes | You want the deployed rule set without package upgrades |
| You do not need centralised usage | You need one shared service across several systems |
The hosted plans → · The same engine as agent tools →
The exit path
The package is MIT and runs without us, so an integration built on it outlives any decision we make about the hosted service. Read the licence.
Questions people ask next
Is there an EN 16931 library for Node.js?
@attestwire/en16931 is one: MIT-licensed TypeScript, currently v0.7.3, with zero runtime dependencies and no JVM. It carries the rule set itself rather than shelling out to another engine, so validation is a function call. As of 15 August 2026 we know of no other EN 16931 rule-set validator written in TypeScript, with no JVM and no runtime dependencies — that survey, with its method, is published so you can check it.
Does it run in the browser?
Yes, and on Deno, Bun, Cloudflare Workers and the edge. It uses no platform API beyond the JavaScript standard library — even the DEFLATE decoder the Factur-X reader needs is in-repo rather than borrowed from Node. The playground is this package running client-side: the invoice never leaves the tab.
How big is the bundle?
The package is ESM with sideEffects: false, so a bundler drops what you do not import. The bulk of the data is the official code lists — 16 of them, 4,762 codes, 16.6 kB gzipped for the whole set and 6.0 kB of that the unit list — and each is a separate module, so a build that never references one drops it. There are no runtime dependencies to add to that.
What happens when the rules change?
You upgrade the package, in a pull request, and review any changed findings before they enforce. That is the trade against the hosted API, where the deployed rule set moves without you. Either way the tracked artefacts and their versions are published, and the changelog says what moved in each release.
What is the licence, and what happens if Attestwire stops?
MIT (the licence), and the source is at github.com/attestwire/en16931. The package runs with no key and no network call, so an integration built on it keeps working whatever happens to the hosted service. The continuity terms →