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

FieldNeeded?
number, issueDate, currencyYes
seller - name + address (country)Yes
buyer - name + address (country)Yes
lines - at least one (name, quantity, unit, netUnitPrice, vat)Yes
seller.vatIdYes, when you charge VAT

Optional - reach for these as needed

FieldWhat it adds
dueDate, payment (IBAN, terms)payment details
notes, purchaseOrderRef, contractRefreferences
allowancesChargesdocument discounts / surcharges
paidAmount, payeeName, delivery, typethe long tail

XRechnung (German B2G) additionally requires

FieldWhat it is
buyerReferencethe Leitweg-ID
seller / buyer .electronicAddresselectronic addresses
seller.contact (name, phone, email)a reachable contact
dueDate or payment.terms, plus an IBANdue 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.

jasypdf

Declarative PDFs in pure TypeScript. ZUGFeRD & XRechnung compliant, with no headless browser and no Java.

Resources

© 2026 Florian Heuberger · MIT License

Built with Nuxt · self-hosted fonts · no trackers