Building Factur-X support in Node.js: what actually works
Factur-X is not just CII XML and not just a PDF. It is CII XML embedded inside a PDF/A-3b container with specific requirements around embedded fonts, ICC colour profiles, XMP metadata declaring the Factur-X version and profile, and the AFRelationship="Alternative" declaration on the XML attachment. Most Node.js PDF libraries handle PDF generation but PDF/A-3b conformance is a different problem entirely.
The approach in this guide is to keep the compliance layer behind a single REST API call and focus your Node.js code on the business logic: assembling invoice data, calling the API, and storing or transmitting the response.
Why Factur-X is hard in Node.js specifically
Three technical realities make this format particularly awkward in the Node.js ecosystem.
PDF/A-3b conformance. Factur-X is not just CII XML and not just a PDF. It is CII XML embedded inside a PDF/A-3b container with specific requirements around embedded fonts, ICC colour profiles, XMP metadata declaring the Factur-X version and profile, and the AFRelationship="Alternative" declaration on the XML attachment. Node.js PDF libraries like PDFKit, pdf-lib, and jsPDF handle PDF generation but PDF/A-3b conformance is a different problem. Most Node.js implementations fall short here, producing files that look correct in a PDF viewer but fail when submitted to a Plateforme Agréée.
Schematron validation. EN 16931 defines over 200 business rules implemented as Schematron XSLT 2.0 artefacts. Node.js has limited XSLT 2.0 support. There is no Node.js equivalent of Java's Saxon-HE. The standard solution in other ecosystems is to shell out to a Java process running Saxon, which defeats the purpose of staying in Node.js.
Version maintenance. ZUGFeRD 2.4 (Factur-X 1.08) shipped with new mandatory fields and deprecated NIL elements. XRechnung releases a new CIUS version roughly annually. Peppol BIS Schematron updates quarterly. Tracking these updates across multiple format families is ongoing work that someone has to do, and the existing Node.js libraries are maintained by individual contributors with limited time.
Setting up the InvoiceXML client
Install your preferred HTTP client. All examples below use axios and form-data, which work in any Node.js version from 18 onwards.
npm install axios form-data
Set your API key as an environment variable. Get one by signing up for a free InvoiceXML account (100 free credits per month, no credit card required):
export INVOICEXML_API_KEY="your_key_here"
Create a reusable client:
// invoicexml-client.js
import axios from 'axios';
export const invoicexml = axios.create({
baseURL: 'https://api.invoicexml.com',
headers: {
Authorization: `Bearer ${process.env.INVOICEXML_API_KEY}`,
},
timeout: 30000,
});
Create Factur-X invoices in Node.js
The most common use case. You have invoice data in your database or coming from a webhook, and you need to produce a compliant Factur-X PDF.
import fs from 'node:fs/promises';
const payload = {
invoice: {
invoiceNumber: 'INV-2025-001',
issueDate: '2025-07-01',
currency: 'EUR',
seller: {
name: 'Acme GmbH',
vatIdentifier: 'DE123456789',
legalRegistration: { identifier: 'HRB98765' },
postalAddress: { line1: 'Musterstr. 1', city: 'Berlin', postCode: '10115', country: 'DE' },
},
buyer: {
name: 'Example Corp',
postalAddress: { line1: '12 Rue de Rivoli', city: 'Paris', postCode: '75001', country: 'FR' },
},
paymentDetails: { paymentAccountIdentifier: 'DE89370400440532013000' },
lines: [{
quantity: 10,
priceDetails: { netPrice: 150.00 },
vatInformation: { rate: 19 },
item: { name: 'Consulting Services' },
}],
},
};
const response = await fetch('https://api.invoicexml.com/v1/create/facturx', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.INVOICEXML_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
await fs.writeFile('invoice-facturx.pdf', Buffer.from(await response.arrayBuffer()));
The response is a binary PDF/A-3b file with the factur-x.xml attachment embedded. Totals, VAT breakdowns, and the specification identifier are computed by the API from the line items.
Full create example on GitHub →
Validate Factur-X invoices in Node.js
Before sending an invoice to a customer or submitting it to a French Plateforme Agréée, validate that it passes all EN 16931 Schematron rules and PDF/A-3b conformance checks.
import { invoicexml } from './invoicexml-client.js';
import FormData from 'form-data';
import { createReadStream } from 'fs';
async function validateFacturX(pdfPath) {
const form = new FormData();
form.append('file', createReadStream(pdfPath));
const response = await invoicexml.post('/v1/validate/facturx', form, {
headers: form.getHeaders(),
});
return response.data;
}
// Usage
const result = await validateFacturX('invoice-facturx.pdf');
if (result.valid) {
console.log(`Valid Factur-X invoice (${result.data.conformanceLevel})`);
} else {
console.error(`Validation failed with ${result.errors.length} errors:`);
result.errors.forEach(err => {
console.error(` [${err.rule}] ${err.message}`);
if (err.fields?.length) console.error(` Fields: ${err.fields.join(', ')}`);
});
}
Both valid and invalid invoices return HTTP 200. Branch on result.valid rather than the HTTP status code. This keeps the integration clean without exception handling for the expected invalid case.
errors is a flat array of findings. Each entry carries a plain-language message, the business term codes (btCodes), the JSON paths into the document (fields), and the verbatim validator output (raw). warnings uses the same shape for non-blocking issues.
{
"valid": false,
"detail": "Validation failed with 1 error(s)",
"data": {
"schemaValid": true,
"schematronValid": false,
"conformanceLevel": "EN16931"
},
"errors": [
{
"rule": "BR-CO-14",
"line": null,
"message": "The invoice total VAT amount does not match the sum of VAT breakdown amounts.",
"btCodes": ["BT-110"],
"fields": ["totals.taxTotalAmount"],
"raw": "[BR-CO-14] EN16931: Invoice total VAT amount = sum of VAT category tax amounts. (at /...)"
}
],
"warnings": []
}
Use this in CI/CD pipelines to validate generated output on every build, or as a pre-flight check before submitting invoices to a Plateforme Agréée or your customer's accounting system.
Full validation example on GitHub →
Convert PDF invoices to Factur-X
If you receive PDF invoices from suppliers and need to ingest them as Factur-X for archival or further processing, use the transform endpoint. The AI pipeline extracts every EN 16931 field from the source PDF, including scanned and photographed documents.
import { invoicexml } from './invoicexml-client.js';
import FormData from 'form-data';
import { createReadStream } from 'fs';
import { writeFile } from 'fs/promises';
async function pdfToFacturX(pdfPath) {
const form = new FormData();
form.append('file', createReadStream(pdfPath));
form.append('language', 'en');
const response = await invoicexml.post('/v1/transform/to/facturx', form, {
headers: form.getHeaders(),
responseType: 'arraybuffer',
});
return Buffer.from(response.data);
}
const facturxBuffer = await pdfToFacturX('supplier-invoice.pdf');
await writeFile('supplier-facturx.pdf', facturxBuffer);
The optional language parameter (default en) localises the rendered PDF layer (labels, dates, currency formatting). The embedded CII XML is always emitted under the EN 16931 (Comfort) profile. The endpoint also accepts JPEG, PNG, TIFF, WEBP, HEIC, DOCX, and XLSX inputs in addition to PDF.
Full transform example on GitHub →
For incoming Factur-X invoices that need to be imported into your ERP or AP system, the extract endpoint returns either the raw CII XML or a fully parsed JSON representation:
import { invoicexml } from './invoicexml-client.js';
import FormData from 'form-data';
import { createReadStream } from 'fs';
async function extractFacturX(pdfPath) {
const form = new FormData();
form.append('file', createReadStream(pdfPath));
const response = await invoicexml.post('/v1/extract/json', form, {
headers: form.getHeaders(),
});
return response.data;
}
const data = await extractFacturX('incoming-invoice.pdf');
console.log(`Invoice ${data.invoiceNumber} from ${data.seller.name}`);
console.log(`Total: ${data.totals.grandTotalAmount} ${data.currency}`);
console.log(`Lines: ${data.lines.length}`);
To extract just the embedded CII XML rather than parsed JSON, use /v1/extract/xml instead. The response is the raw XML as application/xml.
Full extract example on GitHub →
Error handling pattern for production
Wrap API calls in a service module that handles retry logic, timeout, and error parsing consistently:
import { invoicexml } from './invoicexml-client.js';
export async function callInvoiceXmlApi(endpoint, form, options = {}) {
const { responseType = 'json', retries = 2 } = options;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await invoicexml.post(endpoint, form, {
headers: form.getHeaders(),
responseType,
});
return response.data;
} catch (error) {
if (error.response?.status >= 500 && attempt < retries) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
continue;
}
if (error.response?.status === 400) {
const detail = error.response.data?.detail || 'Bad request';
throw new Error(`InvoiceXML validation error: ${detail}`);
}
if (error.response?.status === 401) {
throw new Error('InvoiceXML API key invalid');
}
throw error;
}
}
}
Why use a REST API instead of building it in Node.js
There are Node.js libraries that handle parts of the Factur-X lifecycle. node-zugferd and @stafyniaksacha/facturx both exist and can generate Factur-X PDFs. Both are functional for basic use cases but neither runs the official EN 16931 Schematron validation, neither implements the full PDF/A-3b conformance pipeline including XMP metadata extension blocks and ICC profile embedding, and neither tracks Schematron updates across the major format families (Factur-X, ZUGFeRD, XRechnung, Peppol BIS).
For development and learning, these libraries are fine. For production e-invoicing where the output determines whether your customer's invoice is accepted by a French Plateforme Agréée or a German B2B partner, the maintenance commitment is significant.
InvoiceXML handles:
The full PDF/A-3b conformance pipeline including embedded fonts, ICC colour profile injection, XMP metadata with the correct Factur-X extension namespace, and the AFRelationship="Alternative" declaration that DATEV and other accounting systems check on import.
EN 16931 Schematron validation across all profiles (MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED). Updates are deployed before the new specification's effective date, with no integration change required on your side.
National CIUS validation overlays. The same API handles Peppol BIS 3.0 for Belgian and Dutch B2B, XRechnung CIUS-REC-DE for German public sector, NLCIUS for Netherlands public sector, and EHF for Norway.
Stateless processing. Documents are processed in memory and purged immediately on response delivery. Nothing is written to disk, nothing is logged, no invoice data is used for model training. GDPR and HIPAA compliant by architecture.
Complete endpoint reference for Node.js
| Operation | Endpoint | Input | Output |
| Create Factur-X | POST /v1/create/facturx | Form data | PDF/A-3b binary |
| PDF to Factur-X (AI) | POST /v1/transform/to/facturx | PDF, image, DOCX, XLSX | PDF/A-3b binary |
| Validate Factur-X | POST /v1/validate/facturx | Factur-X PDF | Validation JSON |
| Extract as JSON | POST /v1/extract/json | Factur-X PDF | Structured JSON |
| Extract CII XML | POST /v1/extract/xml | Factur-X PDF | CII XML |
| Render as PDF | POST /v1/render/cii/to/pdf | CII XML | PDF binary |
Full OpenAPI 3.1 schema: api.invoicexml.com/v1/openapi
Get started
Every Node.js snippet in this guide is available as a runnable example on GitHub. Clone the repo, set INVOICEXML_API_KEY, and you have a working integration in under a minute:
InvoiceXML/facturx-api-examples/nodejs →
Create a free InvoiceXML account → get 100 credits for free, no credit card required.
Related resources:
InvoiceXML is a REST API for European e-invoice compliance covering Factur-X, ZUGFeRD, XRechnung, Peppol UBL, and CII. Stateless processing, GDPR compliant by architecture, and integration support from any Node.js stack.