DEV Community

Olivier EBRAHIM
Olivier EBRAHIM

Posted on

Factur-X 2026 Implementation: 6 Gotchas We Hit at Scale

Factur-X 2026 Implementation: 6 Gotchas We Hit at Scale

Factur-X is France's mandatory e-invoicing standard starting 2026 for all B2B invoices. If you're building a French SaaS or integrating e-invoicing for contractors, you'll encounter these gotchas. Here's what we learned shipping Factur-X at Anodos across 200+ job sites in the French construction industry.

1. XML Schema Versioning: The Silent Breakage

The Problem: Factur-X uses UBL 2.1 XML under the hood, but France's FNFE-MPE (the standards body) releases patch updates without major version bumps. A schema you validated against in January 2025 might differ from the June 2025 release.

What breaks in production:

  • A "valid" invoice from your old validator fails in 2025 if you cached the XSD locally
  • Payment term codes (PaymentMeansCode) expand; old enums no longer match
  • Namespace URIs changed between v1.2 and v1.3 (and yes, a trailing slash matters)

How we fixed it:

// WRONG: Hardcoded schema URL, cached locally
const schema = fs.readFileSync('./facturx-1.2.xsd');

// RIGHT: Fetch fresh on each invoice validation
const schemaUrl = "https://www.fnfe-mpe.org/factur-x/downloads/latest/";
const validator = new XMLValidator(schemaUrl, {cache: false});
Enter fullscreen mode Exit fullscreen mode

Store schema versions in your database with fetch timestamps. When a validator rejects a "known good" invoice, check the schema age first—it's probably the culprit.

2. PDF Attachment Encoding Breaks Outlook & Sage

The Problem: Factur-X is a hybrid format: an XML file embedded inside a PDF. The PDF 1.7 spec says to use EmbeddedFile, but:

  • Outlook silently strips the attachment if you use Content-Transfer-Encoding: base64 instead of binary
  • Sage (France's #1 accounting software) silently fails to read the attachment if your PDF /EmbeddedFile stream doesn't declare FlateDecode compression

Real case from production:
We shipped invoices with Content-Transfer-Encoding: base64 for months. Outlook users never saw the XML attachment in their inbox. Sage customers got "invalid file" errors. Root cause took 2 weeks of support escalation to isolate.

How we fixed it:

from PyPDF import PdfWriter
from io import BytesIO

pdf = PdfWriter()
# ... build PDF ...

# Embed XML with correct settings
xml_bytes = invoice_xml.encode('utf-8')
pdf.add_attachment(
    filename="factur-x.xml",
    data=xml_bytes,
    compress=True,  # FlateDecode
    transfer_encoding='binary'  # NOT base64
)

# Test with real Outlook account before shipping
Enter fullscreen mode Exit fullscreen mode

Checklist:

  • ✓ Use binary encoding, never base64
  • ✓ Compress EmbeddedFile stream with FlateDecode
  • ✓ Set MIME type /application#2Fxml
  • ✓ Forward test invoice to your @outlook.com account before production

3. Line-Item VAT: Multi-Country Nightmare

The Problem: Factur-X VAT rates must match a predefined list. France allows:

  • Standard: 20%
  • Reduced: 5.5%, 2.1%
  • Reverse charge: 0%

But here's the trap: if your French contractor works on a cross-border job (France + Belgium + Luxembourg), Factur-X refuses custom rates like Belgium's 6% VAT on a single invoice. The spec is strict: one VAT treatment per invoice, or rigid line-level mapping.

Even worse: reverse-charge invoices (0% VAT, buyer pays) require a TaxExemptionReason code from the official UNTDID list. Use the wrong code (e.g., VAT_EXEMPTION instead of REVERSE_CHARGE_EU), and French tax software rejects it silently.

How we fixed it:

<!-- RIGHT: Explicit reverse charge with correct code -->
<cac:TaxSubtotal>
  <cbc:TaxableAmount>1000.00</cbc:TaxableAmount>
  <cbc:TaxAmount>0.00</cbc:TaxAmount>
  <cac:TaxCategory>
    <cbc:ID>Z</cbc:ID>
    <cbc:Percent>0.00</cbc:Percent>
    <cac:TaxScheme>
      <cbc:ID>VAT</cbc:ID>
      <cbc:Name>Reverse Charge</cbc:Name>
      <cbc:TaxExemptionReason>REVERSE_CHARGE_EU</cbc:TaxExemptionReason>
    </cac:TaxScheme>
  </cac:TaxCategory>
</cac:TaxSubtotal>
Enter fullscreen mode Exit fullscreen mode

Best practice: For multi-country jobs, emit separate Factur-X invoices per country/VAT regime, not one invoice with mixed rates.

4. Timestamps & Timezone: One Character Kills the Invoice

The Problem: Factur-X demands ISO 8601 timestamps in UTC. But:

  • If you emit 2026-01-15T14:30:00+02:00 (with timezone offset), some validators reject it
  • InvoiceIssueDate must be YYYY-MM-DD (date only), not datetime—one extra character breaks parsing
  • Milliseconds are tricky: the schema accepts 0 or 1 decimal place, not 3

Real case: We emitted timestamps as 2026-01-15T14:30:00.000Z (3 decimal places). A customer's ERP validator rejected it because the schema definition was strict: exactly 0-1 decimal, not 3. We traced the bug by comparing 20 invoices byte-by-byte.

How we fixed it:

from datetime import datetime, timezone

# RIGHT
issue_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")  # 2026-01-15
issue_datetime = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")  # ISO 8601 UTC

# WRONG
issue_date = datetime.now().isoformat()  # Includes time, breaks schema
issue_datetime = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")  # 6 decimal places, rejected
Enter fullscreen mode Exit fullscreen mode

Checklist:

  • ✓ Always use UTC (Z suffix), never timezone offsets
  • ✓ Date fields: YYYY-MM-DD (no time)
  • ✓ DateTime fields: YYYY-MM-DDTHH:MM:SSZ (zero or one decimal for seconds)
  • ✓ Test your timestamp format against an online Factur-X validator before shipping

5. Buyer/Seller Identification: SIREN vs. GLN vs. VAT ID

The Problem: French Factur-X invoices identify buyer and seller. You can use:

  • SIREN (9 digits, French business ID) — most common
  • GLN (13 digits, barcode) — rare, mainly retail
  • VAT ID (FR + 2 digits + SIREN) — required for EU cross-border

But you cannot mix formats on the same invoice. If you use SIREN for the seller, use SIREN (or a VAT ID derived from it) for the buyer—never a mix of SIREN + GLN.

Also: SIREN validation. Many SaaS skip this. An invoice with a typo SIREN like 12345678X will pass your XML schema but fail Chorus Pro (France's official tax e-invoicing portal). The Luhn algorithm catches typos; most implementations skip it.

How we fixed it:

def validate_siren(siren: str) -> bool:
    """Validate French SIREN with Luhn algorithm."""
    if len(siren) != 9 or not siren.isdigit():
        return False

    # Luhn check (similar to credit card validation)
    total = 0
    for i in range(9):
        digit = int(siren[i])
        if i % 2 == 0:  # Even positions: multiply by 2
            digit *= 2
            if digit > 9:
                digit -= 9
        total += digit

    return total % 10 == 0

# Usage
assert validate_siren("732829823")  # Valid Anodos SIREN
assert not validate_siren("12345678X")  # Typo, fails Luhn
Enter fullscreen mode Exit fullscreen mode

6. Pre-Launch Testing & Validation Checklist

Before you ship Factur-X to production, test exhaustively:

  • ✓ Validate against official FNFE-MPE XSD (not a local copy > 3 months old)
  • ✓ Test PDF attachment rendering in Outlook, Sage, and Chorus Pro
  • ✓ Verify all timestamps are ISO 8601 UTC with correct decimal places (0-1)
  • ✓ Check VAT rates and exemption codes against official UNTDID lists
  • ✓ Validate SIREN/SIRET with Luhn algorithm before invoice creation
  • ✓ Generate a test invoice, emit as Factur-X, then re-import it into your own system (round-trip test catches schema mismatches)
  • ✓ Submit 5 real invoices to Chorus Pro test environment pre-launch (France's tax portal has a sandbox)

Pro tip: Automate these checks. We added validation gates to our CI/CD pipeline—invoices fail build if they don't meet the checklist above. Caught 3 gotchas before they reached customers.


Conclusion

Factur-X compliance is non-negotiable starting 2026. These 6 gotchas represent ~200 hours of production support, debugging, and rework at Anodos. Learn from them: schema version your XSD, test encodings with real clients, split multi-country invoices, validate timestamps and SIRENs, and test exhaustively before launch.

The Factur-X spec is dense, but the payoff is real: once you ship correctly, invoicing compliance stops being a liability and becomes a feature.


Olivier Ebrahim is founder of Anodos, a French SaaS for construction SMEs featuring real-time job site management, AI voice-to-quote, and Factur-X 2026 invoicing. He ships Factur-X invoices for 200+ contractors.

Top comments (0)