What node-zugferd does well
Credit where it is due, because the comparison is meaningless without it:
TypeScript first. You create an invoicer for a profile, and the shape of the invoice data is inferred from that profile's schema (typeof invoicer.$Infer.Schema). Your editor knows which fields the profile expects before anything runs, and zod checks the data at runtime. For a Node.js codebase that is the most natural e-invoicing API on npm.
The full profile ladder, and room to extend it. MINIMUM, BASIC WL, BASIC, EN 16931 and EXTENDED ship by default, and the package lets you define your own profiles. Its README is also refreshingly direct about the legal fine print: it warns that MINIMUM and BASIC WL documents do not count as invoices under German law and recommends BASIC at minimum.
Your PDF stays yours. invoice.embedInPdf(pdf, ...) attaches the generated XML to a PDF you already render, using pdf-lib, so teams with an existing invoice layout can add the ZUGFeRD layer without touching their design.
Free, light and widely used. MIT licensed, a small dependency tree (pdf-lib, fast-xml-parser, zod, defu), and heavy weekly download numbers. That matters, and an honest comparison says so.
So the question this page answers is not whether node-zugferd is good. It is whether the piece it covers is the piece your compliance problem actually consists of, and that question is best answered with the full picture on the table.
Capability matrix
Library capabilities as documented in the official repository and npm releases as of October 2026; API capabilities link to their documentation.
| Capability | node-zugferd | InvoiceXML REST API |
| ZUGFeRD / Factur-X XML generation | Yes, CII syntax, profiles MINIMUM to EXTENDED plus custom profiles | Yes, JSON in, validated document out (/v1/create/zugferd, /v1/create/facturx) |
| Hybrid PDF | Attaches the XML to a PDF you supply (embedInPdf); conformance of the result depends on that input PDF | Generates a finished PDF/A-3, or embeds into your own PDF (options.pdfUrl, /v1/embed/zugferd), checked before it is returned |
| Validation | XSD only, through the optional xsd-schema-validator package, which needs Java on the machine; returns valid or invalid | XSD plus the official EN 16931 Schematron, JSON findings with rule ids and field paths (/v1/validate/zugferd) |
| KoSIT BR-DE (XRechnung) validation | No | Yes, /v1/validate/xrechnung, both syntaxes auto-detected |
| XRechnung | No built-in profile | Native UBL and CII (/v1/create/xrechnung, syntax selected via options.syntax) |
| Reading ZUGFeRD and Factur-X invoices | Listed as coming soon | Yes, /v1/extract/json and /v1/extract/xml |
| Reading plain supplier PDFs (no embedded XML) | No | Yes, AI parsing with confidence scores (/v1/parse/json) |
| Peppol BIS | Not its scope | Peppol BIS 3.0 UBL via /v1/create/ubl, plus EHF, NLCIUS and PINT |
| Rendering previews of XML invoices | No | /v1/render/cii/to/pdf and /v1/render/xrechnung/to/pdf |
| Specification updates | Wait for a package release, bump, retest, redeploy | Live server-side with no change on your side |
| Project status | Labelled work in progress; latest release 0.1.1-beta.1 (August 2025) | Production API with versioned endpoints |
| Support | Community, via GitHub issues | Professional support; missing features integrated on request |
Two rows decide most evaluations, and neither is about generation. Validation and project status are about risk: who catches the invoice that would be rejected, and who carries the work while the dependency is still taking shape. The next two sections take them in turn.
The validation gap in Node.js
node-zugferd's validation step checks the generated XML against the profile's XSD. That catches malformed XML: a missing mandatory element, a wrong data type, an element in the wrong place. It is useful, and it is not where real rejections come from. The two hundred plus business rules of EN 16931 live in Schematron: totals that do not reconcile per BR-CO-15, a VAT category whose required exemption reason is missing, an allowance without its reason code. A document can pass the XSD and fail every one of them, and the recipient's system, which does run the official rules, is where you find out.
Two details make the gap wider in practice. The XSD check depends on the optional xsd-schema-validator package, which performs the validation with Java and expects java and javac on the path, so a Node.js service quietly gains a JVM requirement. And the official Schematron artifacts are published as XSLT 2.0; running them in Node.js means compiling them for SaxonJS yourself and recompiling every time FeRD, CEN or KoSIT publish a new release. Most teams decline, and rightly so.
The API's answer is to run the machinery where it can run: POST /v1/validate/zugferd executes the official XSD and Schematron stacks plus the hybrid checks server-side and returns findings as structured JSON with rule ids, plain-language messages, and field paths. And on the create side the same rules run before any document leaves: /v1/create/zugferd refuses to return a file that would fail them, converting a recipient-side rejection into an immediate HTTP 400 with the reasons enumerated.
A work-in-progress dependency
European e-invoicing specifications move on a schedule your roadmap does not control: FeRD and FNFE-MPE revise ZUGFeRD and Factur-X, KoSIT revises XRechnung annually, CEN maintains the EN 16931 artifacts underneath. For a library-based stack, every publication starts the same loop: watch the announcement, wait for the package release that supports it, bump, retest the pipeline, and redeploy every service that touches invoices.
For node-zugferd that loop starts from an earlier point. The README carries a clear caution that the package is still under development, every published version so far is below 1.0, the latest release (0.1.1-beta.1) dates from August 2025, and reading existing invoices is announced as coming soon. That is a transparent status for an open-source project maintained in the open, and teams evaluating today should price it in: pre-1.0 releases can change the API between versions, the features you need next may not have a release date, and the gap between the package and the current specifications is yours to watch.
The API removes the loop rather than relocating it. Current FeRD, KoSIT, and CEN artifacts are applied server-side before their effective dates; your integration does not change, and nothing on your side needs monitoring, bumping, or redeploying. The team whose full-time job is e-invoicing compliance absorbs the treadmill so yours does not.
Migrating from node-zugferd
The switch is smaller than most teams expect, because the API's request model covers the same semantic ground as a profile schema, and the API computes totals and the VAT breakdown from line items, so the migration is a mapping exercise, not a redesign. Three subsections: the concept mapping, the same invoice written both ways, and the migration sequence.
Mapping the API surface
The table maps the node-zugferd calls a typical generation pipeline uses onto the InvoiceXML endpoints and request fields of POST /v1/create/zugferd (the identical model serves /v1/create/facturx and /v1/create/xrechnung):
| node-zugferd concept | InvoiceXML request field or endpoint |
zugferd({ profile: BASIC }) | The endpoint choice; EN 16931 is the default profile, and invoice.specificationId (BT-24) selects another official profile |
The invoice data passed to invoicer.create(data) | The invoice object: invoiceNumber, issueDate, currency, seller, buyer, lines[] |
| Totals and VAT breakdown you compute and pass in | Computed by the API from lines[] |
invoice.toXML() | POST /v1/create/cii for the XML alone |
invoice.embedInPdf(pdf, ...) with your own PDF | options.pdfUrl on the create call, or POST /v1/embed/zugferd with your PDF and XML |
| No PDF of your own | POST /v1/create/zugferd renders the PDF (with options.logoUrl, brandColor and language) |
Profile validate (XSD, Java required) | POST /v1/validate/zugferd (XSD and Schematron), and the same checks run on every create call |
| Custom profile for XRechnung | POST /v1/create/xrechnung, UBL or CII |
| No equivalent yet | POST /v1/extract/json, /v1/parse/json, /v1/render/cii/to/pdf |
Everything that has no row on the right side is work that disappears rather than moves: the totals arithmetic, the Java requirement, the validation gap, and the version-tracking habit.
The same invoice, side by side
First the library version, following the pattern its README teaches. The invoice data itself depends on the profile schema, so it is left as a comment here; note what the code leaves open afterwards:
import fs from "node:fs";
import { zugferd } from "node-zugferd";
import { BASIC } from "node-zugferd/profile/basic";
const invoicer = zugferd({ profile: BASIC });
const data = {
// Seller, buyer, lines, totals and VAT breakdown, shaped by the
// BASIC profile schema (typeof invoicer.$Infer.Schema).
};
const invoice = invoicer.create(data);
// The PDF must show exactly the data passed above.
const pdf = fs.readFileSync("./invoice.pdf");
const hybrid = await invoice.embedInPdf(pdf, {
metadata: { title: "RE-2026-001" },
});
// Still ahead: Schematron validation, PDF/A conformance of your input,
// and every specification release from here on.
The same invoice as one HTTP call. The API generates the PDF layout, computes totals and the VAT breakdown, produces the conformant PDF/A-3, and validates against the official rule set before returning:
import fs from "node:fs/promises";
const payload = {
invoice: {
invoiceNumber: "RE-2026-001",
issueDate: "2026-10-01",
currency: "EUR",
seller: {
name: "Mustermann Software GmbH",
vatIdentifier: "DE123456789",
postalAddress: {
line1: "Hauptstrasse 12", city: "Berlin",
postCode: "10115", country: "DE"
}
},
buyer: {
name: "Beispiel Handel AG",
postalAddress: {
line1: "Marienplatz 8", city: "Muenchen",
postCode: "80331", country: "DE"
}
},
lines: [
{
quantity: 10,
unitCode: "HUR",
item: { name: "Softwareentwicklung" },
priceDetails: { netPrice: 250.0 },
vatInformation: { rate: 19 }
}
]
}
};
const response = await fetch("https://api.invoicexml.com/v1/create/zugferd", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INVOICEXML_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(await response.text());
await fs.writeFile("invoice-zugferd.pdf", Buffer.from(await response.arrayBuffer()));
The difference is not line count; both snippets are short. The difference is what stands behind them. Behind the first: an XML attachment on a PDF you are responsible for, checked against the schema at most. Behind the second: a finished, validated hybrid, and if the request cannot yield a compliant invoice, an HTTP 400 with the violated rules as structured findings instead of a file. To keep your own PDF layout rather than the generated one, pass it as options.pdfUrl and the API embeds the compliant XML into your design.
The migration path in three steps
Step 1: validate your current output. The opening move costs one HTTP call and no code changes: upload hybrids your node-zugferd pipeline produces today to POST /v1/validate/zugferd. The endpoint accepts files from any producer, extracts the XML, detects the declared profile, and runs the official XSD and Schematron rules plus the hybrid checks, returning findings with rule ids, plain-language messages, and field paths. The report is your baseline: it shows precisely where your current output stands against the current rule sets and therefore exactly what the switch resolves. Wiring the same call into Jest or Vitest keeps that baseline visible for the rest of the migration.
Step 2: swap generation. Replace invoicer.create() and embedInPdf() with the JSON request from the mapping table. This is the substantive step, and it is smaller than it looks: the field mapping is mechanical, the API computes totals and the VAT breakdown from your line items, and the container work disappears because the response already is the finished hybrid. If your product renders its own invoice PDF, keep it and pass it as options.pdfUrl. Teams that also serve B2G swap in /v1/create/xrechnung for those buyers with the same request model, and the Node.js guide has the full lifecycle code including serverless patterns.
Step 3: remove the package. Drop node-zugferd, pdf-lib and xsd-schema-validator from your dependencies, and the Java runtime from your images if nothing else needs it. From this point your invoicing code has zero compliance dependencies and no specification-driven redeploys ahead of it. Incoming documents run through /v1/extract/json for hybrids and /v1/parse/json for the plain supplier PDFs, with /v1/extract/attachments unpacking embedded BG-24 documents.
Step 1 is the first move of the migration, not a permanent arrangement: once generation moves in step 2, the same validation endpoint simply becomes your regression check on the API's own output inside CI.
A complete compliance service
What the switch buys, stated as the value proposition it is:
The whole lifecycle behind one integration. Creating ZUGFeRD, Factur-X, XRechnung (both syntaxes), and Peppol BIS UBL from one request model; validating documents from any producer against the official rules; extracting incoming hybrids as JSON; AI parsing for the plain PDFs no library reads; rendering previews; attachment handling. The capability matrix above is not a feature race, but the right column is the complete problem, covered.
The specifications stop being your problem. Current FeRD, KoSIT, and CEN artifacts are live server-side before their effective dates. No release monitoring, no version bumps, no retest-and-redeploy cycle, and no dependency on any package's roadmap. Your integration is finished the day it works.
Minimal infrastructure, by design. No PDF library, no Java runtime for validation, no XSLT compiler, no Schematron artifacts. One fetch call, which Node.js already ships.
Expertise on call. Deep e-invoicing knowledge with professional support behind the integration, and missing features are integrated on request rather than filed and hoped for.
Processing is stateless throughout: documents are handled in memory and purged when the response ships, nothing is stored or logged, and no invoice data trains any model.
Get started
The fastest way to ground the decision in your own data is step 1 of the migration: validate a few invoices your current pipeline produces and read the report. Create a free InvoiceXML account → and get 100 credits for free, no credit card required.
Runnable Node.js examples for every operation live in the examples repository.
Related resources:
InvoiceXML is a REST API for European e-invoice compliance covering ZUGFeRD, Factur-X, XRechnung, Peppol UBL, and CII. Stateless processing, GDPR compliant by architecture, and callable from any stack: Node.js, .NET, Java, Python, PHP, Ruby, or anything else with an HTTP client.