Validate and generate e-invoices in TypeScript

the whole rule book, as one function call.

You write TypeScript or Node.js and need to check an invoice file a supplier sent, or produce invoice files your customers will accept. You would rather not send anything to a service. @attestwire/en16931 does both from one npm package. It needs no account or key, runs offline, and is MIT-licensed.

A European e-invoice follows EN 16931, the European standard that says what an electronic invoice must contain. On disk it is an XML file in one of two formats, UBL or CII. The package reads either format into one invoice object, checks it against the rules Attestwire implements, and can write UBL or CII XML back out from invoice data. It runs locally, without an API key or a network call.

Install the version used for the examples on this page
npm install @attestwire/en16931@0.14.0

v0.14.0 on npm, published with provenance attestations · source at GitHub.

Check an existing UBL invoice

This section is for you if a supplier sent you a file. If you are producing an invoice from your own data, the generation example below is the other path.

Save this as check-invoice.mjs. It reads a UBL invoice from a local file and does not upload it. The build runs every call in it against the package's own sample before printing it.

check-invoice.mjs
import { readFileSync } from 'node:fs';
import { parseUbl, validateInput } from '@attestwire/en16931';

const file = process.argv[2];
if (!file) throw new Error('Usage: node check-invoice.mjs invoice.xml');

const parsed = parseUbl(readFileSync(file, 'utf8'));
const result = validateInput(parsed.invoice);

console.log(JSON.stringify({
  ...result,
  unmapped: parsed.unmapped
}, null, 2));

process.exitCode = result.valid ? 0 : 1;
Run it with your file
node check-invoice.mjs invoice.xml

For a CII invoice, import parseCiiInvoice and use it in place of parseUbl. A CII file has a root element called CrossIndustryInvoice. UBL invoices and credit notes use Invoice and CreditNote roots with UBL namespaces. A filename alone does not tell you which format you have.

The example is a command-line check. It does not handle every application error. An unreadable or unsupported file can throw before validation, so handle parser exceptions separately from invoice findings in your application.

Understand the result before acting on it

Each rule result is a finding, and each finding has a severity. The result sorts them into three arrays.

A passing result means the parsed invoice data passed the implemented checks. It is not a full check against the XML Schema or against Schematron, the official rule files receivers run against the XML. It is not a promise that the receiver will accept the invoice. Check coverage and limits.

Verify your integration

The package's committed UBL and CII minimal fixtures (the sample invoices kept in the repository for testing) passed this workflow with engine v0.14.0. In a separate synthetic local check with the same engine version, removing the supply timing produced the finding BR-DE-TMP-32 under information, and valid stayed true. That is why applications should inspect all three finding arrays.

Keep a known passing fixture and a fixture that is meant to fail in your own tests. Run invoice checks in CI, your automated test pipeline, when your mapping or generation code changes.

When to use the hosted API or MCP

Use the library when checks should run in your own process. Use the hosted API, the same checks as a web service you call over HTTP, when you want an HTTP service. Use MCP, a way for an AI assistant to call these tools, when your assistant needs the hosted tools.

Live VAT and Peppol lookups (Peppol is the network many European buyers receive invoices through) are separate hosted operations. This local example does not perform them.

Validate, then generate

This example uses one of the package's own exported sample invoices, so you can paste it into an empty project and get the comment on the last line. The build runs it before printing it.

Check and generate a sample invoice
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 sample for your own object and the types do the rest. Keep the type annotation: 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.

Available types

These 7 exports cover the core integration. There is no client to construct, no configuration object and no lifecycle.

ExportWhat 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 extractor for Factur-X, a PDF invoice with the XML invoice attached inside it), 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 XML format comes out.

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

Handle inputs the library cannot accept

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.

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

The generator calculates totals from the invoice data. Verify the output against your target rule version and your receiver's requirements. Generating XML does not guarantee downstream acceptance. Rounding is half-up at two decimals on each line before the sum. That is what the rule BR-CO-10 asks for; it is the standard's rule that checks the line total against the sum of the lines.

Library or hosted API?

The library and the hosted API run the same engine and return the same findings. The difference is where the engine runs and who keeps it up to date.

Use the library when…Use the API when…
Your application runs JavaScript or TypeScriptYour application uses another language
You want processing to stay entirely localYou want a managed endpoint
You prefer to control when the rule set changesYou want the deployed rule set without package upgrades
You do not need centralised usageYou need one shared service across several systems

The hosted plans · The same engine as agent tools

Keep control of your integration

The package is MIT-licensed and runs without us. Read the licence, and see the continuity terms for what happens to the hosted service.

Questions people ask next

Is there an EN 16931 library for Node.js?

@attestwire/en16931 is one: MIT-licensed TypeScript, currently v0.14.0, with zero runtime dependencies and no JVM (Java runtime). It carries the rule set itself rather than shelling out to another engine, so validation is a function call.

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

Run it in a tab first · The German rules it enforces · Reading a file you were sent · Wiring it into CI

Dated library survey: .