@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, andfatturapadocuments. - 🧾 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, orcii. - 📎 Extract XML from invoice PDFs and attach XML back into existing PDFs.
- 🧱 Use the high-level
EInvoiceclass 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
correctedInvoiceof a corrected invoice; - every entry of
relatedDocumentswithrelationType: '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
correctedInvoiceis 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. correctionshas 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 anEInvoiceParsingError; 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-DDpassesDate.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
peppolexport format. PEPPOL-specific checks exist, but the XML export targets are stillubl/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:DocumentCurrencyCodeof the document root, Factur-X and ZUGFeRD fromram:InvoiceCurrencyCodeof the header settlement. EN 16931 requires it (BR-5): a document without it is refused with anEInvoiceParsingError, and no currency is put in its place. ZUGFeRD 1.0 input is read fromram:InvoiceCurrencyCodeof 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
EInvoiceParsingErrorwhosemissingTermnames 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')writesbuyerReference(or the reference a decoded UBL document keeps) and refuses a document without one with anEInvoiceFormatErrorwhoseunsupportedFeaturesis['BR-DE-15']; nothing, not the invoice number, is put in its place. A seller whose buyer gave no reference exportsubl(EN 16931), where BT-10 is optional. The metadata has no buyer reference of its own:buyerReferenceis the one field. - The payment due date (BT-9) and the payment terms (BT-20):
dueInDaysis optional, as BT-9 is in EN 16931. A document that states no due date decodes withdueInDaysundefined and is written without one; no term is put in its place. The payment terms a document states as text (UBLcac:PaymentTerms/cbc:Note, CIIram:SpecifiedTradePaymentTerms/ram:Description) are kept inmetadata.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 anEInvoiceFormatErrorwhoseunsupportedFeaturesis['BR-CO-25']. A document paid in full, and a credit note, may state neither. FatturaPAshould be documented as detection-only right now.- The day of supply: UBL and XRechnung write
deliveryDateascac:Delivery/cbc:ActualDeliveryDate(BT-72) andperiodOfPerformanceascac:InvoicePeriod(BG-14), and read both back. Factur-X and ZUGFeRD writedeliveryDateasram:ActualDeliverySupplyChainEventandperiodOfPerformanceasram:BillingSpecifiedPeriodof the header settlement, and read each back only when the document states it; ZUGFeRD 1.0 input has its period read as well.periodOfPerformancehas a start and an end date, so a document that states only one of them (EN 16931 allows that, BR-CO-19) reads back withoutperiodOfPerformance, in every format; the date it states is kept inmetadata.extensions.dateInformation.periodStartor.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/tsclasslets a company state none), and a newEInvoicestarts with parties that state none. A house number the document does not state (cbc:BuildingNumber, CIIram: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 anEInvoiceParsingError; 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 itsname,phoneandemail: UBL and XRechnung ascac:Contact, last incac:Party, Factur-X and ZUGFeRD asram:DefinedTradeContact(ram:PersonName,ram:TelephoneUniversalCommunication/ram:CompleteNumber,ram:EmailURIUniversalCommunication/ram:URIID). Every format writes apaymentOptions.sepaConnectionwith an IBAN as a SEPA credit transfer (type code 58) to that account with its BIC andpaymentOptions.descriptionas its text: UBL and XRechnung ascac:PaymentMeans(the text ascbc:InstructionNote), Factur-X and ZUGFeRD asram:SpecifiedTradeSettlementPaymentMeans. The remittance information (BT-83) ispaymentOptions.paymentReference(@tsclass/tsclass10.2.0), such as the invoice number the payer quotes with the payment, written ascbc:PaymentIDandram: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.contactInformationand the payment means intometadata.extensions.paymentInformation(paymentMeansCode,paymentID,iban,accountName,bic; the CII decoders keep an account number that is no IBAN,ram:ProprietaryID, asaccountId, where the UBL decoder keeps any account identifier asiban), 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'sto.electronicAddress(TElectronicAddressof@tsclass/tsclass:{ scheme, value }, the scheme an EAS code such asEMfor an email address or0204for a Leitweg-ID). UBL and XRechnung write each ascbc:EndpointIDwith its scheme asschemeID, first of what the encoder writes incac:Party; Factur-X and ZUGFeRD asram:URIUniversalCommunication/ram:URIIDof the trade party, with its scheme, for a company or a person. Every decoder reads them back intofromandto; 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 anEInvoiceFormatErrorwhoseunsupportedFeaturesis['BR-62']for the seller or['BR-63']for the buyer: no scheme is assumed. A contact'semailis never taken for an electronic address; state{ scheme: 'EM', value: <the email address> }for that. - The document-level
electronicAddressis deprecated (@tsclass/tsclass10.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.electronicAddresswins 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 fromaddress.countrywhen that is already such a code (letter case does not matter). A code is never derived from a country name: a name without acountryCode, a code EN 16931 does not know, or a missing country is refused with anEInvoiceFormatErrornaming 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.vatIdis the VAT identifier (BT-31 seller, BT-48 buyer),registrationIdthe legal registration identifier such as the commercial register number (BT-30, BT-47), andtaxNumberthe 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 secondcac:PartyTaxSchemewith the tax schemeFC, after the VAT one; Factur-X and ZUGFeRD asram:SpecifiedTaxRegistration/ram:IDwith the schemeFC. 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'staxNumberis 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 anEInvoiceFormatErrorwhoseunsupportedFeaturesis['BR-CO-26'], andvalidate()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'sadditionalIdentifiers({ value, scheme }), and every format writes them from there, UBL ascac:PartyIdentification/cbc:IDwith the scheme asschemeID, CII asram:GlobalIDwith its scheme orram:IDwithout 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.customizationIdto the identifier the document states (UBLcbc:CustomizationID, CIIram:GuidelineSpecifiedDocumentContextParameter/ram:ID), soMainValidatorruns 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.profileIdis the business process type (BT-23, UBLcbc:ProfileID). The encoders write the identifier of the format they write, notcustomizationId. - Credit notes, debit notes, corrected invoices and self-billed invoices share the
EInvoiceclass; 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) ofrsm:HeaderExchangedDocument, the seller and the buyer ofram:ApplicableSupplyChainTradeAgreement, the currency ofram: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 anEInvoiceParsingError. 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:
EInvoicecreateEInvoice()validateXml(xml, level?)getDocumentTypeCode(accountingDocType),getAccountingDocType(typeCode),isAccountingDocType(value)en16931UnitCodeList,isEn16931UnitCode(value)(the BR-CL-23 unit code list)FormatDetectorPDFExtractor,PDFEmbedderDecoderFactory,EncoderFactory,ValidatorFactoryBaseDecoder,BaseEncoder,BaseValidatorUBLBase*andCIIBase*extension classesFacturX*andZUGFeRD*format-specific classesValidationLevel,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
postinstalllifecycle. - 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 wheninvoice.pdfalready 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 implementationts_install/: install-time resource bootstrap and download helpersassets_downloaded/: downloaded validation resourcestest/: corpus-heavy validation, format, PDF, performance, and security coverage
License and Legal Information
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.