DEV Community

Olivier EBRAHIM
Olivier EBRAHIM

Posted on

Factur-X 2026 Guide: Implementation Patterns for French BTP SaaS

Factur-X 2026 Guide: Implementation Patterns for French BTP SaaS

Factur-X (UBL 2.3 + embedded XML) became mandatory for B2B invoices in France in 2026. This guide covers the non-obvious implementation gotchas that trip up construction software vendors.

Core Structure: PDF + Embedded XML Hybrid

Factur-X invoices have two distinct layers:

Invoice PDF
├── Visual Layer (human-readable)
│   ├── Line items, totals, client details
│   ├── Accountant opens in Adobe, prints, files
│   └── Signature (handwritten or digital)
│
└── Machine Layer (XML embedded)
    ├── Structured UBL 2.3 XML (ISO 20022 compliant)
    ├── Buyer's ERP auto-imports, validates, matches PO
    └── Invoice matching → accounts payable automation
Enter fullscreen mode Exit fullscreen mode

Critical mistake most SaaS make: Embed any XML and call it Factur-X. You must validate against the UBL 2.3 XSD schema + declare the correct Factur-X profile (MINIMUM vs. EXTENDED).

If your XML doesn't validate → buyer's ERP silently rejects it → invoice marked "non-compliant" → your client calls you angry.

XSD Schema Validation: The Blocking Check

# Step 1: Download UBL 2.3 XSD from oasis-open.org
# Step 2: Validate your generated XML
xmllint --schema UBL-Invoice-2.3.xsd invoice.xml

# Success:
# invoice.xml validates

# Failure: You'll see specific errors like
# "invoice.xml:42: element InvoiceLine: Missing required element CustomizationID"
Enter fullscreen mode Exit fullscreen mode

Common XSD Failures in the Wild

Issue Root Cause Fix
Missing namespace declaration Copy-pasted template without xmlns:cac Add full namespace block from official UBL spec
Date format mismatch Used French format 15/02/2026 Convert to ISO 8601: 2026-02-15 everywhere
Missing VAT ID on supplier Assumed domestic = no VAT ID needed Mandatory even for French invoices: scheme="FR"
Rounding discrepancy Rounded each line item, then summed Sum net as float, round only the total
Empty TaxCategory/Percent Forgot to declare VAT rate on multi-rate invoices Each <TaxSubtotal> needs explicit <Percent>

BTP-Specific Fields: Materials, Labor, Equipment Rental

French construction invoices split costs by type. Each type has a different UNSPSC code (UN's standard classification):

<!-- Line Item 1: Material -->
<InvoiceLine>
  <ID>1</ID>
  <Description>Enduit taloché matière (200 kg)</Description>
  <Quantity unitCode="KGM">200</Quantity>
  <Item>
    <CommodityClassification>
      <!-- 30101500 = Construction materials -->
      <ItemClassificationCode listID="UNSPSC">30101500</ItemClassificationCode>
    </CommodityClassification>
  </Item>
  <Price>
    <PriceAmount currencyID="EUR">28.50</PriceAmount>
  </Price>
  <LineExtensionAmount currencyID="EUR">5700.00</LineExtensionAmount>
</InvoiceLine>

<!-- Line Item 2: Labor -->
<InvoiceLine>
  <ID>2</ID>
  <Description>Pose enduit (4 jours, 150€/jour)</Description>
  <Quantity unitCode="DAY">4</Quantity>
  <Item>
    <CommodityClassification>
      <!-- 93101500 = Labor/services -->
      <ItemClassificationCode listID="UNSPSC">93101500</ItemClassificationCode>
    </CommodityClassification>
  </Item>
  <Price>
    <PriceAmount currencyID="EUR">150.00</PriceAmount>
  </Price>
  <LineExtensionAmount currencyID="EUR">600.00</LineExtensionAmount>
</InvoiceLine>

<!-- Line Item 3: Equipment rental -->
<InvoiceLine>
  <ID>3</ID>
  <Description>Échafaudage (4 jours)</Description>
  <Quantity unitCode="DAY">4</Quantity>
  <Item>
    <CommodityClassification>
      <!-- 43201500 = Equipment rental -->
      <ItemClassificationCode listID="UNSPSC">43201500</ItemClassificationCode>
    </CommodityClassification>
  </Item>
  <Price>
    <PriceAmount currencyID="EUR">75.00</PriceAmount>
  </Price>
  <LineExtensionAmount currencyID="EUR">300.00</LineExtensionAmount>
</InvoiceLine>
Enter fullscreen mode Exit fullscreen mode

Why this matters: Buyers' ERPs group expenses by UNSPSC for cost analysis. If you omit codes or mix categories without proper classification → import fails silently.

VAT Complexity: Multi-Rate Handling

France has four VAT tiers: standard (20%), reduced (10%), super-reduced (5.5%), zero (rare). Many construction invoices span multiple rates (materials ≠ labor tax treatment).

<TaxTotal>
  <TaxAmount currencyID="EUR">1594.00</TaxAmount>

  <!-- Tax subtotal for standard rate (materials) -->
  <TaxSubtotal>
    <TaxableAmount currencyID="EUR">5700.00</TaxableAmount>
    <TaxAmount currencyID="EUR">1140.00</TaxAmount>
    <TaxCategory>
      <ID>S</ID>  <!-- S = Standard 20% -->
      <Percent>20</Percent>
      <TaxScheme>
        <ID>VAT</ID>
        <Name>TVA</Name>
      </TaxScheme>
    </TaxCategory>
  </TaxSubtotal>

  <!-- Tax subtotal for reduced rate (labor in some contexts) -->
  <TaxSubtotal>
    <TaxableAmount currencyID="EUR">900.00</TaxableAmount>
    <TaxAmount currencyID="EUR">90.00</TaxAmount>
    <TaxCategory>
      <ID>AA</ID>  <!-- AA = Reduced 10% -->
      <Percent>10</Percent>
      <TaxScheme>
        <ID>VAT</ID>
        <Name>TVA</Name>
      </TaxScheme>
    </TaxCategory>
  </TaxSubtotal>
</TaxTotal>
Enter fullscreen mode Exit fullscreen mode

Gotcha: If you concatenate all <TaxSubtotal> elements into one instead of splitting by rate → XSD validation fails.

Embedding XML into PDF Programmatically

// Node.js + pdf-lib example
const PDFDocument = require('pdfkit');
const fs = require('fs');

const doc = new PDFDocument();
const xmlData = fs.readFileSync('invoice.xml', 'utf8');

// Embed XML as hidden attachment
doc.file(Buffer.from(xmlData), 'facturx.xml', {
  description: 'Factur-X Invoice (Embedded)',
  subtype: 'application/xml'
});

doc.pipe(fs.createWriteStream('invoice.pdf'));
doc.end();
Enter fullscreen mode Exit fullscreen mode

The PDF reader (Adobe, Preview) won't see the XML—it's hidden in the file structure. But Factur-X-aware tools (Sage, Ciel, buyer ERPs) automatically extract it.

Pre-Flight Checklist: 10 Steps Before Production

  • [ ] XSD validation: Generate 50 invoices, validate each against UBL 2.3 XSD. Zero failures allowed.
  • [ ] ERP import test: Upload a sample invoice to Sage X3, Ciel, Coala. Confirm parse success.
  • [ ] VAT correctness: Multi-rate invoices have separate <TaxSubtotal> per rate. Totals reconcile.
  • [ ] UNSPSC codes: Every line item has <CommodityClassification> with valid code (30101500, 93101500, etc.).
  • [ ] Date format: All dates ISO 8601 (YYYY-MM-DD). No French format.
  • [ ] Invoice numbering: Sequential, no gaps. Tax authority red-flags gaps.
  • [ ] SIRET/VAT ID: Issuer (you) and receiver (client) fully specified with scheme.
  • [ ] Rounding logic: Total HT = sum of line items (rounded at total, NOT per line).
  • [ ] PDF signature: If you add handwritten/digital signature, ensure it doesn't corrupt XML layer.
  • [ ] Audit retention: Store XML + PDF for 6 years. French law requirement.

Common Real-World Gotchas

1. "Invoices validate but buyers can't import"

→ XML is embedded, but in the wrong PDF structure. Buyers' tools expect /EmbeddedFile in PDF's /Names tree. Use pdfbox CLI to inspect your PDF structure.

2. "VAT doesn't reconcile (tax authority flags us)"

→ You're rounding line items individually, then summing. Standard is: sum net amounts as floats, round only the final total.

3. "Validator says OK but Sage rejects it"

→ You declared MINIMUM profile but uploaded an EXTENDED invoice (or vice versa). Sage enforces stricter profile. Check XML metadata.

4. "Accented characters break on Windows (é, è, à)"

→ UTF-8 BOM issue. Ensure XML declaration: <?xml version="1.0" encoding="UTF-8"?> with no BOM prefix.

5. "Buyers can't parse 'services-only' invoices"

→ Labor line items missing <CommodityClassification>. Every line needs a UNSPSC code.

Tooling: What to Use (and What Not To)

Tool Cost Use Case Notes
Zugferd.io validator Free Quick XSD check Best for dev/test
Billto €50/mo Full invoice generation + tax rules Purpose-built for Factur-X 2026
PDF-lib (npm) Free Open-source PDF + XML embed Good for startups
Apptio Vantage Enterprise Large-scale compliance audit Overkill for SMB SaaS

Recommendation: For BTP SaaS, use Billto (or similar compliance-first library). The tax + rounding edge cases are too costly to debug in production.

Closing: The Real Cost of Non-Compliance

One invalid invoice = €5,000 fine from French tax authority (per invoice). If you have 100 clients averaging 50 invoices/year = 5,000 invoices/year at risk.

Test 50 invoices end-to-end before go-live. Your accountant will thank you.


Anodos integrates Factur-X 2026 natively: automatic XML generation, XSD validation, multi-rate VAT handling, 6-year audit trail. Built to pass tax inspection, not custom XML workarounds.

Olivier Ebrahim — Founder, Anodos

Top comments (0)