2026-09-30 07:22:25 +00:00
2026-09-30 07:22:25 +00:00
2026-09-30 07:22:25 +00:00
2026-09-29 19:15:26 +00:00

@fin.cx/einvoice ⚡

TypeScript e-invoicing for the real world: load invoice XML or hybrid PDFs, detect the format, map it into a typed invoice model, validate it, convert it, and embed XML back into PDFs.

@fin.cx/einvoice is built for programmers who need a practical toolkit around European e-invoice formats without writing a parser zoo from scratch.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Why this library?

  • 🚀 Load invoices from XML strings, files, or PDFs with embedded XML.
  • 🧭 Detect ubl, xrechnung, cii, facturx, zugferd, and fatturapa documents.
  • 🧾 Work on a typed in-memory invoice model based on @tsclass/tsclass.
  • ✅ Validate invoices on syntax, semantic, and business-rule levels.
  • 🔄 Export invoices as facturx, zugferd, xrechnung, ubl, or cii.
  • 📎 Extract XML from invoice PDFs and attach XML back into existing PDFs.
  • 🧱 Use the high-level EInvoice class or lower-level decoders, encoders, validators, and PDF extractors directly.

Install

pnpm add @fin.cx/einvoice

postinstall will try to download Schematron validation resources on a best-effort basis when the package is installed in a normal online environment. Install will not fail if the resources cannot be downloaded.

Useful commands:

pnpm download-schematron
pnpm download-test-samples

Useful environment flag:

EINVOICE_SKIP_RESOURCES=1

Quick Start

import { EInvoice, ValidationLevel } from '@fin.cx/einvoice';

const invoice = await EInvoice.fromFile('./invoice.xml');

console.log(invoice.getFormat());
console.log(invoice.id);
console.log(invoice.from.name, '->', invoice.to.name);

const validation = await invoice.validate(ValidationLevel.BUSINESS);
if (!validation.valid) {
  console.log(validation.errors);
}

const xrechnungXml = await invoice.exportXml('xrechnung');

Browser Bundles

@fin.cx/einvoice provides a browser replacement for its internal Node plugin module. Browser bundlers can use the published package entry for in-memory XML workflows such as createEInvoice(), EInvoice.fromXml(), invoice.exportXml(...), and validateXml(xml) without resolving Node built-ins like fs, path, or crypto.

File-path and PDF file helpers remain Node-only. In browser bundles, use XML strings, ArrayBuffer or Uint8Array data you already loaded through browser APIs, and browser storage APIs for persistence instead of helpers such as EInvoice.fromFile(...) or invoice.saveToFile(...).

What it can do

Load and inspect invoices

import { EInvoice } from '@fin.cx/einvoice';

const fromXml = await EInvoice.fromXml(xmlString);
const fromFile = await EInvoice.fromFile('./invoice.xml');
const fromPdf = await EInvoice.fromPdf(pdfBuffer);

console.log(fromXml.subject);
console.log(fromXml.items.length);
console.log(fromXml.currency);

Create invoices in code

import { EInvoice } from '@fin.cx/einvoice';

const invoice = new EInvoice();

invoice.accountingDocId = 'INV-2026-001';
invoice.issueDate = new Date('2026-04-16');
invoice.currency = 'EUR';
invoice.dueInDays = 14;

invoice.from = {
  type: 'company',
  name: 'Sender GmbH',
  description: '',
  status: 'active',
  foundedDate: { year: 2020, month: 1, day: 1 },
  address: {
    streetName: 'Example Street',
    houseNumber: '1',
    city: 'Berlin',
    postalCode: '10115',
    country: 'DE',
  },
  registrationDetails: {
    vatId: 'DE123456789',
    registrationId: 'HRB 123456',
    registrationName: 'Sender GmbH',
  },
};

invoice.to = {
  type: 'company',
  name: 'Receiver SAS',
  description: '',
  status: 'active',
  foundedDate: { year: 2020, month: 1, day: 1 },
  address: {
    streetName: 'Rue Example',
    houseNumber: '10',
    city: 'Paris',
    postalCode: '75001',
    country: 'FR',
  },
  registrationDetails: {
    vatId: 'FR12345678901',
    registrationId: 'RCS 123456789',
    registrationName: 'Receiver SAS',
  },
};

invoice.items = [
  {
    position: 1,
    name: 'Implementation work',
    articleNumber: 'IMPL-001',
    unitType: 'HUR',
    unitQuantity: 8,
    unitNetPrice: 120,
    vatPercentage: 19,
  },
];

const xml = await invoice.exportXml('facturx');

Credit notes, debit notes, corrected invoices and self-billed invoices

One class holds every accounting document. accountingDocType is the tsclass TAccountingDocType and defaults to invoice; invoiceType is an alias for it.

import { EInvoice } from '@fin.cx/einvoice';

const creditNote = new EInvoice();
creditNote.accountingDocType = 'creditnote';
creditNote.relatedDocuments = [{ relationType: 'corrects', documentId: 'INV-2026-001' }];
accountingDocType Type code (BT-3) UBL / XRechnung CII / Factur-X / ZUGFeRD
invoice 380 Invoice with cbc:InvoiceTypeCode ram:TypeCode
corrected-invoice 384 Invoice with cbc:InvoiceTypeCode ram:TypeCode
creditnote 381 CreditNote with cbc:CreditNoteTypeCode, cac:CreditNoteLine, no cbc:DueDate ram:TypeCode
debitnote 383 Invoice with cbc:InvoiceTypeCode ram:TypeCode
self-billed-invoice 389 Invoice with cbc:InvoiceTypeCode ram:TypeCode

Import reads the type back from the code in UBL, XRechnung, Factur-X and ZUGFeRD (v1 and v2): 383 is a debit note, 384 a corrected invoice, 389 a self-billed invoice, the credit note related codes of EN16931 rule BR-CL-01 (381, 396, 261, ...) a credit note, and every other code an invoice, which covers 380 and the invoice related codes the envelope has no name for (326, 386, 875, ...). A UBL CreditNote root is always a credit note. Those other codes are not kept: a 326 that is exported again is written as 380. A value outside TAccountingDocType, a missing one included, is refused with an EInvoiceFormatError by the invoiceType setter and at export.

Corrected invoices and preceding invoice references (BG-3)

A corrected invoice (TCorrectedInvoice, type code 384) corrects an issued invoice that lacked a mandatory detail or stated one incorrectly (§ 31 Abs. 5 UStDV). It is a complete invoice, and correctedInvoice names the invoice it corrects:

const correction = new EInvoice();
correction.accountingDocType = 'corrected-invoice';
correction.correctedInvoice = { documentId: 'INV-2026-001', issueDate: Date.UTC(2026, 8, 1) };
correction.corrections = [{ detail: 'supply-date', defect: 'missing' }];
correction.deliveryDate = Date.UTC(2026, 7, 28);
correction.notes = ['Berichtigung der Rechnung INV-2026-001 vom 01.09.2026: Leistungsdatum ergänzt.'];

The encoders write a preceding invoice reference (BG-3) with the number (BT-25) and, when stated, the issue date (BT-26) for:

  • the correctedInvoice of a corrected invoice;
  • every entry of relatedDocuments with relationType: 'corrects' on a corrected invoice or a credit note.

UBL and XRechnung write cac:BillingReference/cac:InvoiceDocumentReference (cbc:ID, cbc:IssueDate); Factur-X and ZUGFeRD write ram:InvoiceReferencedDocument (ram:IssuerAssignedID, ram:FormattedIssueDateTime) after the header monetary summation. Import reads them back the same way: on a corrected invoice the first reference is correctedInvoice and further ones are relatedDocuments with 'corrects'; on a credit note every reference is such a related document.

The encoders write only what the document states:

  • A corrected invoice without correctedInvoice is written without BG-3. The XRechnung rules then report the warning BR-DE-26; stating the reference is the issuer's duty (§ 31 Abs. 5 Satz 2 UStDV), which the issuing application has to enforce.
  • corrections has no element in EN 16931 and is not written. State what is corrected as a note (notes, BT-22); the encoders add no text of their own.
  • Factur-X and ZUGFeRD write BG-3 in the EN 16931 profile. The MINIMUM profile, which has no BG-3, is not written at all (see CII profiles).

With the reference stated, the XRechnung output of a corrected invoice passes the official EN 16931 1.3.16 and KoSIT XRechnung Schematron 2.6.0 rules without BR-DE-17 or BR-DE-26.

An EInvoice is a TAccountingDoc, not a TInvoice: narrow on accountingDocType where a function takes one document type only.

The table itself is exported, so an application that has to read or write BT-3 on its own does not need a second copy of it:

import {
  getAccountingDocType,
  getDocumentTypeCode,
  isAccountingDocType,
} from '@fin.cx/einvoice';

getDocumentTypeCode('debitnote'); // '383'; throws EInvoiceFormatError for an unknown type
getAccountingDocType('396');      // 'creditnote'; an unlisted or empty code is 'invoice'
getAccountingDocType('384');      // 'corrected-invoice'
isAccountingDocType('proforma');  // false

Validate at different levels

import { EInvoice, ValidationLevel } from '@fin.cx/einvoice';

const invoice = await EInvoice.fromXml(xmlString);

const syntax = await invoice.validate(ValidationLevel.SYNTAX);
const semantic = await invoice.validate(ValidationLevel.SEMANTIC);
const business = await invoice.validate(ValidationLevel.BUSINESS, {
  featureFlags: ['EN16931_BUSINESS_RULES', 'CODE_LIST_VALIDATION'],
  reportOnly: true,
});

console.log(business.valid);
console.log(business.errors);
console.log(business.warnings ?? []);

At the business level, an XRechnung document in UBL syntax that was loaded from XML is checked by the package's XRechnung validator. Its findings carry the IDs of the official rule sets, the KoSIT XRechnung Schematron 2.6.0 and the CEN EN 16931 validation artefacts 1.3.16, and check what those rules check, in their context:

Rule What is missing
BR-DE-1 Payment instructions (BG-16)
BR-49 The payment means type code (BT-81) of a payment instruction
BR-DE-15 The buyer reference (BT-10)
BR-DE-2 The seller contact (BG-6)
BR-DE-6, BR-DE-7 The telephone number (BT-42) or email address (BT-43) of the seller contact
BR-DE-3, BR-DE-4 The seller city (BT-37) or post code (BT-38)
BR-DE-8, BR-DE-9 The buyer city (BT-52) or post code (BT-53)
BR-DE-16 The seller VAT identifier (BT-31), tax registration identifier (BT-32) or tax representative (BG-11), when a VAT category S, Z, E, AE, K, G, L or M is used
PEPPOL-EN16931-R010 The buyer electronic address (BT-49), a rule the XRechnung Schematron includes
PEPPOL-EN16931-R020 The seller electronic address (BT-34), a rule the XRechnung Schematron includes
BR-21 The identifier (BT-126) of an invoice line
BR-CO-25 The payment due date (BT-9) or payment terms (BT-20), while an amount is due (BT-115), on an Invoice; a CreditNote is not asked, as CEN/TC 434 lifted the rule from the credit note in 2019. EN 16931 states this rule; the CEN artefacts for UBL and CII do not check it

One check of its own has a code no official rule has: EINVOICE-DE-VATID-FORMAT, a seller VAT identifier (BT-31) that begins with DE but is not DE followed by nine digits, the format the Bundeszentralamt für Steuern issues. The official rules do not check that format, and its finding has the source EINVOICE, not XRECHNUNG.

These checks are a subset. The official Schematron remains the reference for whether a document passes.

A ZUGFeRD 1.0 document is not validated: no schema or Schematron of ZUGFeRD 1.0 ships with this package, and the EN 16931 and ZUGFeRD 2.x rules do not apply to it. At every level its result is valid: false with the error ZUGFERD1-NOT-VALIDATED (ZUGFERD1_NOT_VALIDATED_CODE), so a gate on valid fails closed; it is read all the same (fromXml, statedValues). The amounts it states are checked against each other as any document's are: at the business level, by BR-CO-10 to BR-CO-17 (see "Received documents" below), and a rule they break is reported beside ZUGFERD1-NOT-VALIDATED.

Unit codes (BR-CL-23)

A line's unitType is written as the unit code of the invoiced quantity (BT-130), so it has to be a code EN 16931 admits: UN/ECE Recommendation 20 with the Recommendation 21 extension, as business rule BR-CL-23 lists them. Words such as hours or HOURS are not codes; HUR (hour), DAY (day), MON (month), H87 (piece), C62 (one), LS (lump sum) and package codes such as XPP are.

The business level checks the rule on every document, without a feature flag: the unit codes of the invoiced, credited, billed and base quantities (BT-130, BT-150) of UBL, XRechnung, Factur-X and ZUGFeRD documents, and the lines of an invoice built in code. A failure is an error with the code BR-CL-23 and the location of the attribute or line. Whitespace around a code in the XML is ignored, as in the official rule; the list comparison itself is exact.

The list is exported, so an application can check a unit before it builds a document:

import { en16931UnitCodeList, isEn16931UnitCode } from '@fin.cx/einvoice';

isEn16931UnitCode('HUR');   // true
isEn16931UnitCode('HOURS'); // false
isEn16931UnitCode('hur');   // false: codes are compared exactly
en16931UnitCodeList.codes.length; // 2162, a frozen array in the order of the rule
en16931UnitCodeList.source;     // 'EN 16931 validation artefacts (CEN/TC 434) validation-1.3.16'

The 2162 codes are the BR-CL-23 list of the EN 16931 validation artefacts of CEN/TC 434, release validation-1.3.16 (https://github.com/ConnectingEurope/eInvoicing-EN16931), reproduced unaltered. The source files are licensed under EUPL-1.2; the codes are those of UN/ECE Recommendations 20 and 21. createItem() gives a line without a unit the code C62 (one).

A line needs a unit (BR-23). A document whose quantity carries no unitCode reads with an empty unitType, never with a unit it did not state, and the business level reports BR-23 with the location of the line. An empty unitType is written without the attribute, so the unit stays absent when the document is exported again.

A present but empty attribute, unitCode="", fails BR-CL-23, as in the official rule. It reads with an empty unitType too, so after a round trip the document fails BR-23 instead. At the business level the EN16931_BUSINESS_RULES and CODE_LIST_VALIDATION flags leave both unit rules to the document check, so each line is reported once.

Convert between supported XML targets

import { EInvoice } from '@fin.cx/einvoice';

const invoice = await EInvoice.fromFile('./source.xml');

const facturxXml = await invoice.exportXml('facturx');
const zugferdXml = await invoice.exportXml('zugferd');
const xrechnungXml = await invoice.exportXml('xrechnung');
const ublXml = await invoice.exportXml('ubl');
const ciiXml = await invoice.exportXml('cii');

Work with PDFs

import { EInvoice, PDFExtractor } from '@fin.cx/einvoice';

const extracted = await new PDFExtractor().extractXml(pdfBuffer);
if (extracted.success && extracted.xml) {
  const invoice = await EInvoice.fromXml(extracted.xml);
  console.log(extracted.format, invoice.id);
}

const invoice = await EInvoice.fromXml(xmlString);
const pdfWithXml = await invoice.embedInPdf(existingPdfBuffer, 'facturx');

PDF embedding preserves the input PDF and adds the format-specific XML attachment name and associated-file relationship. It does not convert an arbitrary source PDF to PDF/A-3; the source PDF must already satisfy the required archival profile.

Reverse charge

An envelope with reverseCharge: true is a reverse charge document: the recipient owes the tax (§ 13b UStG). Every line and the VAT breakdown are written with the VAT category AE at the rate 0 and a tax amount of 0, and the breakdown states the exemption reason code VATEX-EU-AE and the text "Steuerschuldnerschaft des Leistungsempfängers" (§ 14a Abs. 5 UStG, Abschnitt 13b.14 Abs. 1 UStAE) when the document's language starts with de, and "Reverse charge", the wording Abschnitt 14a.1 Abs. 6 Satz 2 UStAE admits, for any other. EInvoice.language defaults to 'en': set it to 'de' for the German wording.

Export refuses a reverse charge document, naming the rule, when a line states a VAT rate other than 0 (BR-AE-05), the seller has neither a VAT identifier nor a tax number (registrationDetails.taxNumber, BT-32, the Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows), or the buyer has neither a VAT identifier nor a legal registration identifier (registrationDetails.registrationId, BR-AE-02). On the seller side BR-AE-02 would also accept a tax representative's VAT identifier (BT-63), which the envelope cannot state.

The envelope has no place of supply, so einvoice checks only what EN 16931 BR-AE-02 requires. Whether an invoice needs the VAT identification numbers of both parties because the supply is made in another member state (§ 14a Abs. 1 Satz 3 UStG) is for the issuing application to enforce: the parties' countries do not tell it, since a domestic § 13b supply to a buyer based in another member state is taxed in Germany and may state a tax number.

An item can also state reverse charge on its own (vatCategory: 'AE'), so one document can carry reverse charge items beside taxed ones; the same rules apply to it. A decoded document states reverseCharge: true when every line is AE, and each item carries the category its line states.

VAT categories

Every item has a VAT category (EN 16931 BT-151, TAccountingDocItem.vatCategory of @tsclass/tsclass): the one it states, else reverse charge (AE) when the document states reverseCharge, standard rated (S) otherwise. The encoders write one VAT breakdown (BG-23) per category and rate, and the decoders give every item the category its line states and the exemption reason of its breakdown.

Category Meaning Rate Exemption reason (BT-120, BT-121)
S standard rated greater than 0 (BR-S-05) none (BR-S-10)
Z zero rated, e.g. photovoltaic systems (§ 12 Abs. 3 UStG) 0 (BR-Z-05) none (BR-Z-10)
E exempt, e.g. § 4 UStG or a small business (§ 19 Abs. 1 UStG) 0 (BR-E-05) the item's reason or VATEX code, required (BR-E-10)
AE reverse charge (§ 13b UStG) 0 (BR-AE-05) the item's, else VATEX-EU-AE and the § 14a Abs. 5 UStG wording
K intra-community supply (§ 4 Nr. 1 Buchst. b, § 6a UStG) 0 (BR-IC-05) the item's, else VATEX-EU-IC
G export outside the EU (§ 4 Nr. 1 Buchst. a, § 6 UStG) 0 (BR-G-05) the item's, else VATEX-EU-G
O not subject to VAT none written; the item states 0 (BR-O-05) the item's, else VATEX-EU-O
L IGIC (Canary Islands) 0 or more (BR-AF-05); more than 0 in CII none (BR-AF-10)
M IPSI (Ceuta and Melilla) 0 or more (BR-AG-05) none (BR-AG-10)

The tax of a breakdown is its taxable amount × rate for S, L and M, and 0 for every other category. An item states its exemption in vatExemptionReason (BT-120) and vatExemptionReasonCode (BT-121, a code of the CEF VATEX list, BR-CL-22); the items of one breakdown state the same one, as a breakdown states one.

Export refuses a document that breaks a rule of its categories with an EInvoiceFormatError naming the rule, and validate() reports it at the business level for a document built in code: the rate and the exemption reason above; for Z, E, L and M the seller VAT identifier or tax number (BR-Z-02, BR-E-02, BR-AF-02, BR-AG-02); for K the seller and the buyer VAT identifier (BR-IC-02), the delivery date or invoicing period (BR-IC-11) and the deliver to country (BR-IC-12); for G the seller VAT identifier (BR-G-02); for O neither VAT identifier (BR-O-02) and no item of another category (BR-O-11); items of one breakdown with different exemption reasons (EINVOICE-VAT-EXEMPTION-DIFFERS). The rules would also accept a seller tax representative (BT-63), which the envelope cannot state.

The deliver to address (BG-15) is metadata.deliveryAddress; every format writes it when it states a country (countryCode, BT-80) and reads it back. An item at the rate 0 outside reverse charge that states no category is standard rated and refused as BR-S-05: state its category.

The tests check documents of every category with the official CEN EN 16931 rules on all four exports and with the KoSIT XRechnung rules on the XRechnung export only.

A ZUGFeRD 1.0 document states its values under their v1 names; its statedValues hold them (see below), and its items carry the category its lines state.

Paid amount and advance payments

paidAmount is the gross sum received before the document was issued, the paid amount (BT-113). Every format writes it (cbc:PrepaidAmount, ram:TotalPrepaidAmount) and the amount due for payment (BT-115) as the gross total less it (BR-CO-16), zero when paid in full and negative when more was paid; a paid amount of zero is nothing paid and is not written. The decoders read it back. A paid amount with more decimals than the currency has (two at most, BR-DEC-16) is refused, not rounded. The amount due is always derived, so the envelope has no field for it and the business rule check of the EInvoice has nothing to compare it with; the official rules check BR-CO-16 on the written XML.

A final invoice (an invoice, a corrected invoice or a self-billed invoice) may list the advancePayments it deducts (@tsclass/tsclass 9.9.0 on). paidAmount then equals their gross sum, the net plus the vat of every group of every payment, to the last decimal; a document that breaks this, or lists advance payments without a paid amount, is refused, and neither value is recomputed from the other. The advance invoice of each payment that states one is written as a preceding invoice reference (BG-3), after the invoice a corrected invoice corrects. Another document type with advance payments is refused.

The deduction a final invoice owes under § 14 Abs. 5 Satz 2 UStG, the considerations received before the supply and the tax on them per rate (Abschnitt 14.8 Abs. 7 UStAE), has no element in EN 16931: "Derzeit sind die Anforderungen an eine Endrechnung noch nicht im strukturierten Teil einer E-Rechnung darstellbar" (BMF letter of 15 October 2024, Rn. 48). The encoders write the paid amount and the references and add no text of their own. The issuer either adds the deduction as an annex (Abschnitt 14.8 Abs. 8 Nr. 2 UStAE), which Rn. 48 accepts as an unstructured file in an e-invoice issued until 31 December 2027 and which @design.estate/dees-document 3.4 prints, or issues an invoice over the remaining consideration only (Restrechnung, Abschnitt 14.8 Abs. 11 UStAE), which Rn. 48 recommends. Reading an e-invoice back yields its paid amount but no advancePayments: the XML does not carry the net and the tax of every payment. A corrected invoice's advance invoices read back as related documents it corrects, since BG-3 does not tell them apart, and an invoice's are not read.

Received documents: what they state

A document read from XML carries what it states in statedValues, with the EN 16931 business terms, as written: the document totals (BG-22: BT-106 to BT-115, and BT-111 with its currency BT-6), the VAT breakdown (BG-23: BT-116 to BT-121), each line's net amount, VAT category and rate (BT-126, BT-131, BT-151, BT-152), its item gross price (grossPrice, BT-148) and item price discount (priceDiscount, BT-147), and the document level allowances and charges (BG-20, BG-21: BT-92 to BT-105). A field is undefined when the document does not state it. UBL, XRechnung, Factur-X, ZUGFeRD 2.x and ZUGFeRD 1.0 documents are read; a document built in code has none.

ZUGFeRD 1.0 predates EN 16931 and states the same amounts under its v1 names, which are mapped onto the business terms: the totals of the trade settlement, its VAT breakdown, the lines that state a product, and the document level allowances and charges. A v1 logistics service charge (ram:SpecifiedLogisticsServiceCharge, such as shipping costs), which the v1 charge total includes, is a document level charge. A v1 line may state several allowances and charges on its gross price, where EN 16931 states one discount; its priceDiscount is then the allowances less the charges, so that the net price is the gross price less it.

const invoice = await EInvoice.fromXml(receivedXml);
const { totals, vatBreakdown } = invoice.statedValues!;
console.log(totals.taxExclusiveTotal, totals.taxTotal, totals.amountDue);

Book what the document states. The envelope's totals (totalNet, totalVat, totalGross, taxBreakdown) are computed from quantity × net price, and the envelope cannot express document or line allowances and charges, a price base quantity (BT-149) or a VAT category other than standard rated and reverse charge, so for such a document they differ from what it states. Neither value is changed: validate() reports each difference at the business level as a warning EINVOICE-STATED-DIFFERS, the package's own code, naming the business term and both values. statedValues is not updated when the envelope changes, and no encoder writes it: an export writes the envelope's computation, so a received document with allowances, charges or base quantities is not written back with the amounts it states, and a document with lines of a category other than S and AE is refused on export (BR-S-05). The EINVOICE-STATED-DIFFERS warnings are the signal that an export would not state what was received; a faithful round trip needs @tsclass/tsclass to express allowances, charges, base quantities and line categories.

The stated values are checked against each other at the business level as the CEN artefacts 1.3.16 check them, under the official rule IDs BR-CO-10 to BR-CO-17, in UBL and CII; these checks replace the approximate ones of earlier releases. A ZUGFeRD 1.0 document's stated values are checked as CII's, and a finding names the v1 element (ram:SpecifiedTradeSettlementMonetarySummation and ram:ApplicableTradeTax of ram:ApplicableSupplyChainTradeSettlement). The BASIC profile of ZUGFeRD 1.0 states the allowance and charge totals but carries no document level allowances or charges, so a BASIC document that states none is not checked by BR-CO-11 and BR-CO-12. A stated amount that is no decimal number is refused on read with an EInvoiceParsingError that names the business term.

Calendar dates

A calendar date of the invoice (issue date, due date, delivery date, invoicing period, the issue date of a preceding invoice) has no time of day and no time zone: UBL writes it as xsd:date (2026-09-20), CII in format 102 (20260920). In the accounting document envelope such a day is a timestamp: the UTC midnight of that day, Date.UTC(2026, 8, 20).

  • The decoders return that timestamp. A UBL date may carry a time zone (2026-09-20+02:00, 2026-09-20Z); the zone does not move the day, so the decoders return the UTC midnight of the stated day, and every format writes that day. A CII date stated with a time of day, in format 203 (202609202330) or 204 (20260920233000), or without a format code as an ISO 8601 date or date and time (2026-09-20T00:30:00+02:00), is read the same way: the day it states, whatever the time of day and the zone. A CII date in another form, or in a format that names no day, is refused with an EInvoiceParsingError; no day is guessed.
  • The encoders write the day of that timestamp in UTC and count the payment term in UTC days, so what they write does not depend on the time zone of the machine that writes it.
  • An application that keeps its dates as YYYY-MM-DD passes Date.UTC(year, month - 1, day). Which day it is (for example, today in Germany) is the application's decision, made before the date becomes a timestamp.

Supported formats

Format Detect Import Export Validation Notes
ubl Yes Yes Yes Yes Generic UBL flow
xrechnung Yes Yes Yes Yes UBL export; UBL and CII import
cii Yes Yes Yes Partial Generic CII export currently uses the Factur-X encoder path
facturx Yes Yes Yes Yes Main CII-based generation path
zugferd Yes Yes Yes Partial Input supports v1 and v2+; export writes the EN 16931 profile, and v2 reads back as facturx, as XML and from a PDF (see CII profiles)
fatturapa Yes No No Basic placeholder Detection exists, decoder/encoder do not

Important format notes

  • There is no dedicated root-level peppol export format. PEPPOL-specific checks exist, but the XML export targets are still ubl / xrechnung.
  • XRechnung import routes UBL and CII by the namespace-qualified XML root. Missing or invalid document-level invoice IDs and issue dates are rejected.
  • The document currency (BT-5): the decoders read it as the document states it, UBL and XRechnung from cbc:DocumentCurrencyCode of the document root, Factur-X and ZUGFeRD from ram:InvoiceCurrencyCode of the header settlement. EN 16931 requires it (BR-5): a document without it is refused with an EInvoiceParsingError, and no currency is put in its place. ZUGFeRD 1.0 input is read from ram:InvoiceCurrencyCode of its trade settlement and refused alike.
  • A document refused because it lacks its invoice number, issue date, document currency code, seller or buyer is refused with an EInvoiceParsingError whose missingTerm names the EN 16931 business term it lacks (BT-1, BT-2, BT-5, BG-4, BG-7), in every format; tell these refusals apart by it, not by the message.
  • The buyer reference (BT-10): every format writes buyerReference (cbc:BuyerReference, ram:BuyerReference) and reads it back, ZUGFeRD 1.0 input as well. XRechnung requires it (BR-DE-15), and it is the buyer's: the identifier the buyer gives the seller for routing the invoice, a Leitweg-ID for a public buyer, any identifier of the buyer's choice in B2B. exportXml('xrechnung') writes buyerReference (or the reference a decoded UBL document keeps) and refuses a document without one with an EInvoiceFormatError whose unsupportedFeatures is ['BR-DE-15']; nothing, not the invoice number, is put in its place. A seller whose buyer gave no reference exports ubl (EN 16931), where BT-10 is optional. The metadata has no buyer reference of its own: buyerReference is the one field.
  • The payment due date (BT-9) and the payment terms (BT-20): dueInDays is optional, as BT-9 is in EN 16931. A document that states no due date decodes with dueInDays undefined and is written without one; no term is put in its place. The payment terms a document states as text (UBL cac:PaymentTerms/cbc:Note, CII ram:SpecifiedTradePaymentTerms/ram:Description) are kept in metadata.extensions.paymentInformation.paymentTermsNote, and every format writes them back from there. Without a kept text, UBL and XRechnung write a stated due date with a note that names it (Zahlbar bis 04.10.2026, Payable by 2026-10-04), ZUGFeRD does so with its payment options, and Factur-X writes the due date alone (ram:DueDateDateTime). While an amount is due (BT-115 greater than zero), EN 16931 asks for the one or the other (BR-CO-25): the export of an invoice, corrected invoice, debit note or self-billed invoice that states neither is refused with an EInvoiceFormatError whose unsupportedFeatures is ['BR-CO-25']. A document paid in full, and a credit note, may state neither.
  • FatturaPA should be documented as detection-only right now.
  • The day of supply: UBL and XRechnung write deliveryDate as cac:Delivery/cbc:ActualDeliveryDate (BT-72) and periodOfPerformance as cac:InvoicePeriod (BG-14), and read both back. Factur-X and ZUGFeRD write deliveryDate as ram:ActualDeliverySupplyChainEvent and periodOfPerformance as ram:BillingSpecifiedPeriod of the header settlement, and read each back only when the document states it; ZUGFeRD 1.0 input has its period read as well. periodOfPerformance has a start and an end date, so a document that states only one of them (EN 16931 allows that, BR-CO-19) reads back without periodOfPerformance, in every format; the date it states is kept in metadata.extensions.dateInformation.periodStart or .periodEnd (YYYY-MM-DD), and every encoder writes it back as the one date of the period, so it survives a round trip and a conversion between UBL and CII.
  • Decoded parties state what the document states: no status and no founding date (EN 16931 has no element for either; @tsclass/tsclass lets a company state none), and a new EInvoice starts with parties that state none. A house number the document does not state (cbc:BuildingNumber, CII ram:LineTwo) decodes as ''; no house number is split off the street line. The encoders write none for '' and, only because stored data may carry it, for the '0' earlier decoders put in place of an absent one. The UBL, Factur-X and ZUGFeRD 2.x decoders refuse a document without its seller (BG-4) or buyer (BG-7) with an EInvoiceParsingError; it is not read with an empty party. ZUGFeRD 1.0 input is refused alike.
  • The contacts (BG-6 seller, BG-9 buyer) and the payment instructions (BG-16, BG-17): every format writes a party's contact, its metadata.contactInformation ({ name, phone, email }) or else its name, phone and email: UBL and XRechnung as cac:Contact, last in cac:Party, Factur-X and ZUGFeRD as ram:DefinedTradeContact (ram:PersonName, ram:TelephoneUniversalCommunication/ram:CompleteNumber, ram:EmailURIUniversalCommunication/ram:URIID). Every format writes a paymentOptions.sepaConnection with an IBAN as a SEPA credit transfer (type code 58) to that account with its BIC and paymentOptions.description as its text: UBL and XRechnung as cac:PaymentMeans (the text as cbc:InstructionNote), Factur-X and ZUGFeRD as ram:SpecifiedTradeSettlementPaymentMeans. The remittance information (BT-83) is paymentOptions.paymentReference (@tsclass/tsclass 10.2.0), such as the invoice number the payer quotes with the payment, written as cbc:PaymentID and ram:PaymentReference; a document that states none has none, and no value is put in its place. Payment options without an IBAN write no payment means, as a credit transfer (30, 58) asks for its account (BR-61), and an account, with its BIC, is written only with its identifier (BR-50). Every decoder except ZUGFeRD 1.0 reads the contacts back into the parties' metadata.contactInformation and the payment means into metadata.extensions.paymentInformation (paymentMeansCode, paymentID, iban, accountName, bic; the CII decoders keep an account number that is no IBAN, ram:ProprietaryID, as accountId, where the UBL decoder keeps any account identifier as iban), and every encoder writes kept payment means back, with the kept type code and remittance information, so a conversion between UBL and CII keeps both.
  • The electronic addresses (BT-34 seller, BT-49 buyer): the seller's is from.electronicAddress, the buyer's to.electronicAddress (TElectronicAddress of @tsclass/tsclass: { scheme, value }, the scheme an EAS code such as EM for an email address or 0204 for a Leitweg-ID). UBL and XRechnung write each as cbc:EndpointID with its scheme as schemeID, first of what the encoder writes in cac:Party; Factur-X and ZUGFeRD as ram:URIUniversalCommunication/ram:URIID of the trade party, with its scheme, for a company or a person. Every decoder reads them back into from and to; a ZUGFeRD 1.0 trade party has no electronic address. EN 16931 makes both optional; XRechnung asks for both (PEPPOL-EN16931-R020 and R010). An address without a value is none. One with a value and no scheme is refused with an EInvoiceFormatError whose unsupportedFeatures is ['BR-62'] for the seller or ['BR-63'] for the buyer: no scheme is assumed. A contact's email is never taken for an electronic address; state { scheme: 'EM', value: <the email address> } for that.
  • The document-level electronicAddress is deprecated (@tsclass/tsclass 10.1.0) and goes in the next major version. Until then it is read as the seller electronic address where the seller states none (from.electronicAddress wins where both are stated), and the decoders set it to the seller's, so code that reads it keeps working.
  • Country codes: every format writes the seller and buyer country (BT-40, BT-55) as an ISO 3166-1 alpha-2 code EN 16931 accepts (BR-CL-14), taken from address.countryCode, or from address.country when that is already such a code (letter case does not matter). A code is never derived from a country name: a name without a countryCode, a code EN 16931 does not know, or a missing country is refused with an EInvoiceFormatError naming the field (BR-CL-14, BR-09, BR-11). The list of codes is the UBL one of the CEN EN 16931 validation artefacts 1.3.16, for every format. Their CII list differs in two codes: it has AN (Netherlands Antilles, withdrawn from ISO 3166-1) and lacks SS (South Sudan). So a Factur-X or ZUGFeRD document with a party in South Sudan is written, and the official CII rules report BR-CL-14 for it; AN is refused in every format.
  • Party identifiers: registrationDetails.vatId is the VAT identifier (BT-31 seller, BT-48 buyer), registrationId the legal registration identifier such as the commercial register number (BT-30, BT-47), and taxNumber the seller's tax registration identifier, the Steuernummer (BT-32), which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows in place of the VAT identifier. UBL and XRechnung write the tax number as a second cac:PartyTaxScheme with the tax scheme FC, after the VAT one; Factur-X and ZUGFeRD as ram:SpecifiedTaxRegistration/ram:ID with the scheme FC. The decoders read each by its scheme, whatever order the document lists them in. EN 16931 has no tax number for the buyer, so a buyer's taxNumber is not written.
  • The seller's identification (BR-CO-26): EN 16931 asks the seller to state a seller identifier (BT-29), a legal registration identifier (BT-30, registrationDetails.registrationId) or a VAT identifier (BT-31, registrationDetails.vatId); a tax number (BT-32) alone does not identify it. Every export whose seller states none of them is refused with an EInvoiceFormatError whose unsupportedFeatures is ['BR-CO-26'], and validate() reports BR-CO-26 at the business level, for a loaded document and for one built in code. The party identifiers, the seller identifier (BT-29) and the buyer identifier (BT-46), have no typed field in the envelope: the decoders keep them in the party's additionalIdentifiers ({ value, scheme }), and every format writes them from there, UBL as cac:PartyIdentification/cbc:ID with the scheme as schemeID, CII as ram:GlobalID with its scheme or ram:ID without one. A UBL seller identification with the scheme SEPA is the bank assigned creditor identifier (BT-90) and is not written to CII. A buyer identifier with the scheme SEPA is refused (BR-CL-10): UBL admits the scheme for the seller and the payee only, CII for no party.
  • The specification identifier (BT-24): the decoders set metadata.customizationId to the identifier the document states (UBL cbc:CustomizationID, CII ram:GuidelineSpecifiedDocumentContextParameter/ram:ID), so MainValidator runs the XRechnung, PEPPOL or Factur-X checks on a decoded document of that kind. The PEPPOL checks run for the PEPPOL BIS Billing 3.0 identifier only, urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0, not for PINT, A-NZ or another derivative that names PEPPOL. metadata.profileId is the business process type (BT-23, UBL cbc:ProfileID). The encoders write the identifier of the format they write, not customizationId.
  • Credit notes, debit notes, corrected invoices and self-billed invoices share the EInvoice class; see the type code table above. Credit and debit amounts are written as given, the encoders do not change signs.
  • ZUGFeRD 1.0 input (rsm:CrossIndustryDocument) is read with the v1 namespaces and the v1 structure: the identifier (BT-1) and the type code (BT-3) of rsm:HeaderExchangedDocument, the seller and the buyer of ram:ApplicableSupplyChainTradeAgreement, the currency of ram:ApplicableSupplyChainTradeSettlement, and the lines that state a product (ram:SpecifiedTradeProduct), with their quantity, net price and VAT rate. A v1 line without a product is a text line: its note is read with the others, and it is no item. A document without its identifier, seller, buyer or currency code is refused with an EInvoiceParsingError. The deliver to address of a v1 document is not read.

CII profiles

The Factur-X and ZUGFeRD encoders, and cii, which uses the Factur-X one, write the full EN 16931 content, so they state the EN 16931 profile as the specification identifier (BT-24): urn:cen.eu:en16931:2017, the identifier the CEN validation artefacts use and the EN 16931 profile of Factur-X 1.0 and ZUGFeRD 2.x, which ZUGFeRD also calls COMFORT.

FacturXEncoder.setProfile and ZUGFeRDEncoder.setProfile accept EN16931 and COMFORT, both written as that identifier. The other choices are refused with an EInvoiceFormatError whose message names the profile and why, and whose unsupportedFeatures is ['profile MINIMUM'] and so on; nothing is downgraded to EN 16931 silently:

Choice Why it is refused
MINIMUM The content written is EN 16931, which MINIMUM does not allow (no lines, no VAT breakdown, no street, postcode or city). A MINIMUM document is not an e-invoice (BMF letter of 15 October 2024, Rn. 25 and 30).
BASIC The content written is EN 16931, which neither BASIC nor BASIC WL allows; BASIC WL has no lines and is not an e-invoice either (same letter, Rn. 25 and 30).
EXTENDED The output has not been checked against the EXTENDED schema and rules.

Factur-X 1.0 and ZUGFeRD 2.x are one standard with one XML, and their EN 16931 profile has one identifier. A ZUGFeRD 2.x export therefore reads back as Factur-X, as XML and from a PDF alike, because a PDF's format is detected from its embedded XML: FormatDetector.detectFormat and getFormat() return facturx, and FacturXDecoder reads it. A ZUGFeRD PDF is still written with the zugferd-invoice.xml attachment.

Decoding does not check the profile. A document that states another profile's identifier, such as Factur-X MINIMUM or BASIC WL, a ZUGFeRD 2.0 identifier, the urn:zugferd: identifiers earlier releases of this library wrote, or ZUGFeRD v1, is detected and read as before. An identifier that names Factur-X reads as facturx, one that names ZUGFeRD as zugferd: ZUGFeRD 2.0 has identifiers of its own (urn:zugferd.de:2p0:minimum, and for BASIC and EXTENDED the EN 16931 identifier extended by urn:zugferd.de:2p0:basic or urn:zugferd.de:2p0:extended), where ZUGFeRD 2.1 and Factur-X 1.0 share theirs.

Public API overview

Top-level exports from @fin.cx/einvoice include:

  • EInvoice
  • createEInvoice()
  • validateXml(xml, level?)
  • getDocumentTypeCode(accountingDocType), getAccountingDocType(typeCode), isAccountingDocType(value)
  • en16931UnitCodeList, isEn16931UnitCode(value) (the BR-CL-23 unit code list)
  • FormatDetector
  • PDFExtractor, PDFEmbedder
  • DecoderFactory, EncoderFactory, ValidatorFactory
  • BaseDecoder, BaseEncoder, BaseValidator
  • UBLBase* and CIIBase* extension classes
  • FacturX* and ZUGFeRD* format-specific classes
  • ValidationLevel, InvoiceFormat
  • exported types like TAccountingDoc, TAccountingDocType, TInvoice, TCorrectedInvoice, TCorrectedInvoiceReference, TInvoiceCorrection, TCreditNote, TDebitNote, TSelfBilledInvoice, IEn16931UnitCodeList, ValidationResult, ValidationError, ExportFormat, IPdf, EInvoiceOptions

Validation resources and install-time behavior

The package includes an install bootstrap in ts_install/ that tries to download validation resources into assets_downloaded/schematron.

Behavior to know:

  • It only runs in a real postinstall lifecycle.
  • It skips cleanly when offline.
  • It skips when dist_ts/ is not present yet.
  • It can be disabled in CI with EINVOICE_SKIP_RESOURCES=1.
  • Missing resources do not break install, but they can reduce advanced validation coverage.

If you need the resources manually:

pnpm download-schematron
pnpm download-test-samples

Architecture in one screen

XML / PDF
  -> format detection
  -> decoder
  -> EInvoice model
  -> validation / editing
  -> encoder
  -> XML / PDF with embedded XML

This repo also exposes the lower-level pieces separately so you can plug into the pipeline at the stage you actually need.

Good things to know before using it

  • embedInPdf() attaches XML to an existing PDF buffer. It does not render invoice PDFs from scratch.
  • saveToFile('something.pdf', ...) only works when invoice.pdf already exists, for example after loading from a PDF first.
  • Validation coverage is useful and broad, but this README deliberately does not claim blanket certification or “100% compliance”.
  • Schematron integration and richer business-rule layers exist, but they depend on downloaded resources and are not the same as guaranteed certification against every profile.

Example workflow

import { EInvoice, ValidationLevel } from '@fin.cx/einvoice';

const invoice = await EInvoice.fromFile('./incoming.pdf');

const validation = await invoice.validate(ValidationLevel.BUSINESS, {
  featureFlags: ['EN16931_BUSINESS_RULES', 'CODE_LIST_VALIDATION'],
  reportOnly: false,
});

if (!validation.valid) {
  throw new Error(validation.errors.map((error) => error.message).join('\n'));
}

const xmlForGermanB2G = await invoice.exportXml('xrechnung');

Module layout

  • ts/: main published API and implementation
  • ts_install/: install-time resource bootstrap and download helpers
  • assets_downloaded/: downloaded validation resources
  • test/: corpus-heavy validation, format, PDF, performance, and security coverage

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme
46 MiB
Languages
TypeScript 57%
XSLT 42.4%
JavaScript 0.3%
HTML 0.2%