Factur-X 2026 for Developers: Implementing E-Invoice in 2 Hours (Really)
By 2026, all French SME invoices must be Factur-X compliant by law. If you're building for construction (BTP), you're looking at ~70% of your user base requiring this by April 2026. Here's how to ship it without the usual XML nightmare.
What is Factur-X?
Factur-X (EN 16931 XML + PDF hybrid) is France's mandated e-invoice format. It's not just XML in a folder—it's XML embedded inside a PDF. When you email an invoice, it looks like a PDF to humans. To machines, it's structured data.
Why this matters for your code: You can't slap XML on a PDF. You need:
- A valid PDF/A-3 (not regular PDF)
- XML conforming to CII (Cross Industry Invoice) schema
- An attachment relationship in the PDF metadata
- MIME type
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet(yep, that's a thing)
The 2-Hour Implementation Path
Step 1: Grab a Library (30 mins)
Don't write PDF manipulation yourself. Use:
-
Python:
facturx(PyPI, pip install facturx). French-first, used by 1000+ production systems. -
Node.js:
factur-xnpm package. Solid, but less mature than Python. -
PHP:
setasign/SetaPDF-Signeror pure XML generation +tcpdf(pain, avoid if possible). - Java: Apache PDFBox + custom XML schema. Overkill for SME flows.
pip install facturx
Step 2: Generate the Invoice XML (45 mins)
You need to map your invoice data → CII schema. The schema has ~200 fields. You'll use ~12.
Minimal invoice XML (CII format):
<?xml version="1.0" encoding="UTF-8"?>
<CrossIndustryInvoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<ExchangedDocumentContext>
<GuidelineSpecifiedDocumentContextParameter>
<ID>urn:cen.eu:en16931:2017</ID>
</GuidelineSpecifiedDocumentContextParameter>
</ExchangedDocumentContext>
<ExchangedDocument>
<ID>DEVIS-2026-001</ID>
<TypeCode>380</TypeCode> <!-- 380 = invoice, 381 = credit note -->
<IssueDateTime><DateTimeString format="102">20260415</DateTimeString></IssueDateTime>
<IncludedNote>
<Content>Devis de plâtrerie cuisine</Content>
</IncludedNote>
</ExchangedDocument>
<SupplyChainTradeTransaction>
<!-- Seller (your PM) -->
<ApplicableHeaderTradeAgreement>
<SellerTradeParty>
<ID>FR12345678901</ID>
<Name>ACME Plâtrerie SARL</Name>
<DefinedTradeContact>
<PersonName>Jean Dupont</PersonName>
<TelephoneUniversalCommunication><CompleteNumber>+33612345678</CompleteNumber></TelephoneUniversalCommunication>
</DefinedTradeContact>
<PostalTradeAddress>
<LineOne>123 Rue de la Paix</LineOne>
<CityName>Lyon</CityName>
<PostcodeCode>69000</PostcodeCode>
<CountryID>FR</CountryID>
</PostalTradeAddress>
<SpecifiedTaxRegistration>
<ID schemeID="VA">FR12345678901</ID>
</SpecifiedTaxRegistration>
</SellerTradeParty>
<!-- Buyer (the client) -->
<BuyerTradeParty>
<Name>Maçonnerie Martin EIRL</Name>
<PostalTradeAddress>
<LineOne>456 Ave de l'Industrie</LineOne>
<CityName>Villeurbanne</CityName>
<PostcodeCode>69100</PostcodeCode>
<CountryID>FR</CountryID>
</PostalTradeAddress>
</BuyerTradeParty>
</ApplicableHeaderTradeAgreement>
<!-- Payment & totals -->
<ApplicableHeaderTradeSettlement>
<InvoiceCurrencyCode>EUR</InvoiceCurrencyCode>
<ApplicableTradeTax>
<CalculatedAmount>240.00</CalculatedAmount>
<TypeCode>VAT</TypeCode>
<BasisAmount>1200.00</BasisAmount>
<RateApplicablePercent>20</RateApplicablePercent>
</ApplicableTradeTax>
<SpecifiedTradeSettlementHeaderMonetarySummation>
<LineTotalAmount>1200.00</LineTotalAmount>
<TaxBasisTotalAmount>1200.00</TaxBasisTotalAmount>
<TaxTotalAmount currencyID="EUR">240.00</TaxTotalAmount>
<GrandTotalAmount>1440.00</GrandTotalAmount>
</SpecifiedTradeSettlementHeaderMonetarySummation>
</ApplicableHeaderTradeSettlement>
<!-- Line items -->
<IncludedSupplyChainTradeLineItem>
<AssociatedDocumentLineDocument>
<LineID>1</LineID>
</AssociatedDocumentLineDocument>
<SpecifiedTradeProduct>
<Description>Plâtrerie salle de bain (plafond + 2 cloisons)</Description>
</SpecifiedTradeProduct>
<SpecifiedLineTradeAgreement>
<NetPriceProductTradePrice>
<ChargeAmount>1200.00</ChargeAmount>
</NetPriceProductTradePrice>
</SpecifiedLineTradeAgreement>
<SpecifiedLineTradeSettlement>
<ApplicableTradeTax>
<TypeCode>VAT</TypeCode>
<RateApplicablePercent>20</RateApplicablePercent>
</ApplicableTradeTax>
<SpecifiedTradeSettlementLineMonetarySummation>
<LineTotalAmount>1200.00</LineTotalAmount>
</SpecifiedTradeSettlementLineMonetarySummation>
</SpecifiedLineTradeSettlement>
</IncludedSupplyChainTradeLineItem>
</SupplyChainTradeTransaction>
</CrossIndustryInvoice>
This is the core. You can generate this with templating (Jinja2/EJS) or a library like xml.etree.ElementTree (Python).
Step 3: Embed into PDF (30 mins)
from facturx import make_facturx
# 1. Create your PDF first (reportlab, weasyprint, whatever)
pdf_bytes = generate_pdf_with_reportlab(invoice_data)
# 2. Embed Factur-X XML
facturx_bytes = make_facturx(
pdf_bytes,
xml_string,
facturx_version="2p0" # Factur-X 2.0 = newest standard
)
# 3. Save to file or email
with open("devis-2026-001-facturation.pdf", "wb") as f:
f.write(facturx_bytes)
That's it. facturx handles PDF/A-3 conversion, metadata injection, and schema validation.
Step 4: Validate Before Sending (15 mins)
In production, ALWAYS validate before hitting Send:
from facturx import get_facturx_xml_from_pdf
# Load the file you just created
with open("devis-2026-001-facturation.pdf", "rb") as f:
extracted_xml = get_facturx_xml_from_pdf(f)
if extracted_xml:
print("✓ Factur-X embedded correctly")
else:
print("✗ Not a valid Factur-X PDF")
Common Pitfalls
1. "I forgot the VAT registration (SIRET / SIREN)"
Factur-X REQUIRES:
- Seller SIRET (14 digits) or SIREN (9 digits) with VAT number
- If missing, your invoice is legally invalid in France
Check before embed:
if not seller_siret or len(seller_siret) not in [9, 14]:
raise ValueError("Invalid SIRET/SIREN")
2. "I'm embedding the wrong version"
Factur-X has 3 versions: 1.0 (legacy), 2.0 (current), 2p0 (2.0 with profiles). Use 2p0 for 2026 compliance.
3. "PDF won't open in Outlook"
If you generated a pure PDF/A-3, some older Outlook versions choke. Solution: test with make_facturx(pdf_bytes, xml_string, facturx_version="2p0", profile="CIUS"). The CIUS profile is more compatible with Outlook + Sage/Ciel accounting software.
4. "Quantity and unit are confusing the schema"
CII schema expects BilledQuantity + CalculatedAmount. Don't mix Quantity (CII term) with LineQuantity (different spec). Stick to official CII schema examples.
Integration with Anodos or Similar PM Software
If you're building a BTP SaaS (like Anodos, which generates 400+ invoices/month across its PME network), integrate Factur-X at the quote → invoice transition:
- User generates quote (voice input, AI-assisted)
- Client approves quote
- User creates invoice from quote
- System auto-generates Factur-X PDF (no user input needed)
- Email goes out with embedded invoice
- Accounting software auto-imports VAT, line items, totals
This 2-min integration saves your users 15 mins of manual accounting entry per invoice. For a 50-user PM, that's 125 hours/month.
Testing Checklist
- [ ] Generate invoice with 0%, 5.5%, 20% VAT lines (all rates)
- [ ] Test with missing optional fields (description, contact info)
- [ ] Validate XML against official CII XSD schema
- [ ] Open resulting PDF in Adobe Reader, Outlook, Firefox (3 clients minimum)
- [ ] Extract XML with
get_facturx_xml_from_pdfand verify round-trip - [ ] Email to your accountant, ask them to import into their software
- [ ] Test with 2+ year-old invoice (historical data migration)
Ship It
You've got 30 months until April 2026. If you ship Factur-X support now (2 hours of dev time), you'll be ahead of 90% of your competition. Your users will thank you.
Olivier Ebrahim, fondateur d'Anodos — logiciel de gestion chantier avec facturation Factur-X 2026 intégrée pour PME BTP.
Top comments (0)