Documentation
A complete example
Everything from the last pages, together: the fields at a glance, a complete invoice you can paste and run, and the proof that the file it produces is valid - checked on your own machine. Afraid this is complicated? Watch how short it is.
The fields at a glance
You need the essentials. Everything else you add only if your case calls for it.
Always
| Field | Needed? |
|---|---|
number, issueDate, currency | Yes |
seller - name + address (country) | Yes |
buyer - name + address (country) | Yes |
lines - at least one (name, quantity, unit, netUnitPrice, vat) | Yes |
seller.vatId | Yes, when you charge VAT |
Optional - reach for these as needed
| Field | What it adds |
|---|---|
dueDate, payment (IBAN, terms) | payment details |
notes, purchaseOrderRef, contractRef | references |
allowancesCharges | document discounts / surcharges |
paidAmount, payeeName, delivery, type | the long tail |
XRechnung (German B2G) additionally requires
| Field | What it is |
|---|---|
buyerReference | the Leitweg-ID |
seller / buyer .electronicAddress | electronic addresses |
seller.contact (name, phone, email) | a reachable contact |
dueDate or payment.terms, plus an IBAN | due date + how to pay |
If an XRechnung is missing any of these, the library tells you by field name - see Validation.
A complete invoice
Paste this into invoice.ts, run npx tsx invoice.ts, and you get invoice.pdf and invoice.xml.
import { writeFileSync } from "node:fs";
import { renderZugferd, type Invoice } from "@jasy/zugferd";
const invoice: Invoice = {
number: "INV-001",
issueDate: "2026-06-21",
currency: "EUR",
dueDate: "2026-07-05",
purchaseOrderRef: "PO-7782",
notes: ["Thank you for your business."],
seller: {
name: "Northwind GmbH",
vatId: "DE265013614",
address: { line1: "Hauptstrasse 1", city: "Berlin", postCode: "10115", country: "DE" },
contact: { name: "A. Schmidt", email: "billing@northwind.de", phone: "+49 30 123456" },
},
buyer: {
name: "Globex Ltd",
address: { line1: "5 Market Square", city: "Munich", postCode: "80331", country: "DE" },
},
lines: [
{
name: "Consulting",
description: "On-site workshop",
quantity: 8,
unit: "HUR",
netUnitPrice: 120,
vat: { category: "S", ratePercent: 19 },
},
{
name: "License",
quantity: 1,
unit: "C62",
netUnitPrice: 480,
vat: { category: "S", ratePercent: 19 },
},
],
allowancesCharges: [
{
isCharge: false,
amount: 50,
reason: "Loyalty discount",
vat: { category: "S", ratePercent: 19 },
},
{ isCharge: true, amount: 12, reason: "Shipping", vat: { category: "S", ratePercent: 19 } },
],
payment: {
iban: "DE89370400440532013000",
bic: "COBADEFFXXX",
accountName: "Northwind GmbH",
terms: "Payable within 14 days, net.",
},
};
async function build() {
const { bytes, xml } = await renderZugferd(invoice, { locale: "en" });
writeFileSync("invoice.pdf", bytes); // the ZUGFeRD PDF/A-3
writeFileSync("invoice.xml", xml); // the EN-16931 CII XML
}
build();
Every total is worked out for you: the net (1402.00), the 19% VAT (266.38) and the gross (1668.38) come from the lines, the discount and the surcharge - you never add them up.
Validate it
Now prove it. Nothing is uploaded - it runs on your machine:
npx @jasy/cli validate ./invoice.pdf
invoice.pdf · ZUGFeRD · EN 16931 (CII)
EN 16931 rules ✓ valid
PDF/A-3 structure ✓ 13/13
PDF/A (veraPDF) ✓ compliant
→ VALID
That is the whole loop: you described the invoice, the library built the PDF and the XML and did the maths, and the same checker a tax office uses confirmed it - on your machine. Create, validate, done.