Implementing Factur-X: Building Compliant EU E-Invoices from Scratch in TypeScript
These articles are AI-generated summaries. Please check the original sources for full details.
Factur-X EN 16931 from scratch: PDF/A-3 + CII XML in Node.js / TypeScript
France’s e-invoicing reform kicks in September 1st, 2026, requiring all B2B invoices to follow structured formats. This implementation provides a production-ready path for Factur-X using under 500 lines of TypeScript without commercial SDKs.
Why This Matters
The technical challenge of Factur-X lies in the synchronization between human-readable PDF/A-3 files and machine-readable CII XML attachments. While ideal models suggest exhaustive schema compliance, real-world production at tevaxia.lu shows that mastering specific business rules like BR-CO-17 rounding and ISO-standard metadata is what prevents rejection by platforms like Chorus Pro and Peppol.
Key Insights
- Factur-X is a hybrid format where the PDF is the legal document and the CII XML (EN 16931) is the machine-readable twin.
- The BASIC profile is the recommended SaaS standard, covering 95% of B2B scenarios while avoiding the complexity of the EXTENDED profile.
- Business Rule BR-CO-17 requires rounding each line item before summation; failing to do so causes validation mismatches in downstream accounting software.
- PDF/A-3 compliance requires an AFRelationship set to ‘Alternative’, signaling that the XML is a machine-readable version of the PDF content.
- The format requires strict adherence to UN/ECE Rec 20 unit codes (e.g., ‘C62’ for piece) and ISO 4217 currency codes to pass automated validation.
Working Examples
Core data model for Factur-X invoice structure.
export interface FacturXInvoice {
profile: "MINIMUM" | "BASIC_WL" | "BASIC" | "EN_16931" | "EXTENDED";
document_type: "380" | "381" | "384" | "386";
invoice_number: string;
issue_date: string;
currency: string;
seller: FacturXParty;
buyer: FacturXParty;
lines: FacturXLine[];
}
Business logic for calculating totals according to EN 16931 rounding rules.
export function computeTotals(inv: FacturXInvoice): FacturXTotals {
const lineTotals = inv.lines.map((l) => {
const gross = l.quantity * l.unit_price_net;
const discount = l.discount_percent ? gross * (l.discount_percent / 100) : 0;
return Math.round((gross - discount) * 100) / 100;
});
const line_total = lineTotals.reduce((s, v) => s + v, 0);
// ... VAT grouping and rounding logic
}
Embedding the XML attachment into the PDF with the mandatory AFRelationship metadata using pdf-lib.
await pdf.attach(xmlBytes, "factur-x.xml", {
mimeType: "application/xml",
description: "Factur-X (EN 16931 CII)",
afRelationship: AFRelationship.Alternative,
});
Practical Applications
- Use Case: Tevaxia.lu generates automated Factur-X invoices for Luxembourg real estate syndic fund calls, ensuring VAT-exempt compliance under Article 261 D CGI.
- Pitfall: Rounding the final invoice sum instead of individual lines leads to BR-CO-17 validation failures in platforms like Pennylane or Sellsy.
- Use Case: Hotel PMS systems mapping various USALI categories to specific VAT rates (3% for accommodation vs 17% for F&B) within a single structured file.
- Pitfall: Relying on Standard14 fonts (Helvetica/Courier) without subsetting fails strict PDF/A-3B veraPDF validation, though it may pass ‘relaxed’ portal checks.
References:
Continue reading
Next article
Secure LLM Agents with Two-Stage Prompt Injection Detection
Related Content
Mastering Shielded Token Lifecycles with Midnight's Compact Language
Implement private value movement in Midnight apps by building a shielded token lifecycle with mint, transfer, and burn operations using Compact.
Implementing Geometric Collision Detection in Chemical Drawing Software
Learn how to implement pixel-perfect atom and bond selection using Euclidean distance and vector projection clamping in a ChemDraw clone.
Overcoming Google Play Developer Account Rejections via EU Alternative Dispute Resolution
Learn how a developer forced a human review of a rejected Google Play account using the EU's ADR mechanism, resulting in account approval and a 250 EUR payment from Google.