Automation Features Blog Pricing Contact
Back to Blog
Integration May 11, 2026 17 min read

Create and Validate Factur-X Invoices in Python: Complete Guide

A practical guide to handling the full Factur-X lifecycle in Python: creating invoices from structured data, validating against EN 16931 with the official Schematron, converting PDFs with AI extraction, and rendering CII XML as human-readable PDFs. Includes Django, FastAPI, and Odoo integration patterns.

France's September 2026 B2B mandate makes Factur-X compliance a near-term requirement for every business that invoices French customers. For Python developers building Django apps, FastAPI services, Odoo modules, data pipelines, or automation scripts, the question is no longer whether to support Factur-X but how to do it without spending weeks on PDF/A-3b conformance and Schematron infrastructure.

The Python ecosystem has one notable open-source library: akretion/factur-x by Akretion. It generates Factur-X PDFs from existing PDF input plus structured XML, handles basic XSD validation, and is reasonably maintained. For a development prototype or a simple use case where you already have both a PDF and a CII XML in hand, it works.

What it does not do is generate the embedded XML from structured invoice data, run EN 16931 Schematron validation (200+ business rules where most real-world rejections occur), apply national CIUS overlays for cross-market support, or track standards updates ahead of effective dates. These are the layers where most production e-invoicing implementations spend the bulk of their engineering time.

This guide shows how to handle the full Factur-X lifecycle in Python using the InvoiceXML REST API: creating invoices from structured data, validating them against EN 16931 with the official Schematron, converting source PDFs to Factur-X using AI extraction, and rendering CII XML as human-readable PDFs.

Complete Factur-X Toolkit

Everything you need to create, convert, validate, and extract Factur-X invoices via REST API or online.

Build a Factur-X PDF/A-3 from structured data, no source PDF required.

Validate a Factur-X PDF against EN 16931 XSD schema and Schematron business rules.

Convert any PDF invoice into a Factur-X PDF/A-3 with embedded CII XML using AI extraction.

Extract the embedded CII XML from a Factur-X PDF for ERP import or further processing.

Why Factur-X is harder in Python than it looks

Three technical realities make this format awkward in Python specifically, despite the language being well-suited to invoice automation work.

PDF/A-3b conformance is fragile. 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. Python's PDF libraries (reportlab, pypdf, pikepdf, fpdf) handle PDF generation well but PDF/A-3b conformance requires careful manipulation across XMP namespaces, font subsetting, and ICC profile injection. The akretion/factur-x library handles this when given an existing PDF/A-3b input, but creating compliant PDF/A-3b from scratch in pure Python is non-trivial. Files often pass visual inspection in PDF viewers but fail validation at Plateformes Agréées.

XSLT 2.0 is awkward in Python. EN 16931 Schematron validation requires XSLT 2.0. Python's lxml library is excellent but uses libxslt, which is XSLT 1.0 only. The official Schematron artefacts cannot be executed in pure Python. The workaround is saxonpy or saxonche (Saxonica's official Python bindings to Saxon-HE), which work but require a heavy native dependency that complicates deployment, especially in serverless environments (AWS Lambda, Google Cloud Functions) where binary dependencies are limited.

Version maintenance is real work. ZUGFeRD 2.4 (Factur-X 1.08) shipped with new mandatory fields and deprecated NIL elements. The French September 2026 mandate brings additional CIUS requirements through the certified Plateformes Agréées. Tracking updates across multiple Schematron families and integrating them into a Python project is ongoing work that compounds quietly over time.


Setting up the InvoiceXML client

All examples below use requests, the de facto standard Python HTTP client. Install:

pip install requests

Set your API key as an environment variable. Sign up for a free InvoiceXML account and you will receive 100 credits to get started, no credit card required:

export INVOICEXML_API_KEY="your_key_here"

Create Factur-X invoices in Python

Send your invoice as JSON. Totals and the VAT breakdown are auto-calculated from the line items.

import os
import requests

invoice = {
    "invoice": {
        "invoiceNumber": "INV-2025-001",
        "issueDate": "2025-09-01",
        "currency": "EUR",
        "seller": {
            "name": "Dupont Conseil SARL",
            "vatIdentifier": "FR32123456789",
            "postalAddress": {
                "line1": "12 Rue de Rivoli",
                "city": "Paris",
                "postCode": "75001",
                "country": "FR",
            },
        },
        "buyer": {
            "name": "Martin Industries SAS",
            "postalAddress": {
                "line1": "45 Avenue des Champs-Elysees",
                "city": "Paris",
                "postCode": "75008",
                "country": "FR",
            },
        },
        "paymentDetails": {
            "paymentAccountIdentifier": "FR7630006000011234567890189",
        },
        "lines": [{
            "quantity": 10,
            "item": {"name": "Conseil en transformation digitale"},
            "priceDetails": {"netPrice": 250.00},
            "vatInformation": {"rate": 20},
        }],
    },
}

response = requests.post(
    "https://api.invoicexml.com/v1/create/facturx",
    headers={
        "Authorization": f"Bearer {os.environ['INVOICEXML_API_KEY']}",
        "Content-Type": "application/json",
    },
    json=invoice,
)

with open("invoice-facturx.pdf", "wb") as f:
    f.write(response.content)

That is the whole request. The API computes totals and the VAT breakdown, embeds the factur-x.xml attachment, applies the PDF/A-3b conformance layer with all required XMP metadata, and validates against EN 16931 Schematron before returning the binary PDF.

Full create example on GitHub →


Validate Factur-X invoices in Python

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 os
import requests

def validate_facturx(pdf_path: str) -> dict:
    with open(pdf_path, "rb") as f:
        response = requests.post(
            "https://api.invoicexml.com/v1/validate/facturx",
            headers={
                "Authorization": f"Bearer {os.environ['INVOICEXML_API_KEY']}",
            },
            files={"file": (os.path.basename(pdf_path), f, "application/pdf")},
        )
    return response.json()

result = validate_facturx("invoice-facturx.pdf")

if result["valid"]:
    print(f"Valid Factur-X invoice ({result['data']['conformanceLevel']})")
else:
    print(f"Validation failed with {len(result['errors'])} errors:")
    for err in result["errors"]:
        print(f"  [{err['rule']}] {err['message']}")
        if err.get("fields"):
            print(f"    Fields: {', '.join(err['fields'])}")

Both valid and invalid invoices return HTTP 200. Branch on result["valid"] rather than the HTTP status code.

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 via pytest or unittest to validate generated output on every build. The validation API is fast enough (typically under a second) to include in pre-commit hooks or automated test suites.

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 os
import requests

def pdf_to_facturx(pdf_path: str) -> bytes:
    with open(pdf_path, "rb") as f:
        response = requests.post(
            "https://api.invoicexml.com/v1/transform/to/facturx",
            headers={
                "Authorization": f"Bearer {os.environ['INVOICEXML_API_KEY']}",
            },
            files={"file": (os.path.basename(pdf_path), f, "application/pdf")},
            data={"language": "en"},
        )
    return response.content

facturx_pdf = pdf_to_facturx("supplier-invoice.pdf")
with open("supplier-facturx.pdf", "wb") as f:
    f.write(facturx_pdf)

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 →


Extract structured data from Factur-X

For incoming Factur-X invoices that need to be imported into your accounting system, ERP, or AP database, the extract endpoint returns either the raw CII XML or a fully parsed JSON representation:

import os
import requests

def extract_facturx(pdf_path: str) -> dict:
    with open(pdf_path, "rb") as f:
        response = requests.post(
            "https://api.invoicexml.com/v1/extract/json",
            headers={
                "Authorization": f"Bearer {os.environ['INVOICEXML_API_KEY']}",
            },
            files={"file": (os.path.basename(pdf_path), f, "application/pdf")},
        )
    return response.json()

data = extract_facturx("incoming-invoice.pdf")
print(f"Invoice {data['invoiceNumber']} from {data['seller']['name']}")
print(f"Total: {data['totals']['grandTotalAmount']} {data['currency']}")
print(f"Lines: {len(data['lines'])}")

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, which you can parse with lxml.etree for further processing.

Full extract example on GitHub →


Render Factur-X as a human-readable PDF

The XML data inside a Factur-X file is not designed for human reading. For standalone CII XML files (extracted from a Factur-X PDF or transmitted via EDI), render as a clean PDF for review workflows:

import os
import requests

def render_facturx_pdf(xml_path: str) -> bytes:
    with open(xml_path, "rb") as f:
        response = requests.post(
            "https://api.invoicexml.com/v1/render/cii/to/pdf",
            headers={
                "Authorization": f"Bearer {os.environ['INVOICEXML_API_KEY']}",
            },
            files={"file": (os.path.basename(xml_path), f, "application/xml")},
        )
    return response.content

pdf_data = render_facturx_pdf("invoice-cii.xml")
with open("invoice-preview.pdf", "wb") as f:
    f.write(pdf_data)

The rendered PDF is for visual review only. It has no legal standing and should not be submitted as an invoice. The original Factur-X file remains the authoritative document.


Django integration: service class with dependency injection

For Django applications, wrap the InvoiceXML client in a service class. Add it to a dedicated services module:

# invoicing/services/facturx.py

import requests
from django.conf import settings
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile


class FacturXService:
    def __init__(self, api_key: str = None):
        self.api_key = api_key or settings.INVOICEXML_API_KEY
        self.base_url = "https://api.invoicexml.com"

    def _headers(self) -> dict:
        return {"Authorization": f"Bearer {self.api_key}"}

    def generate(self, invoice_data: dict) -> bytes:
        response = requests.post(
            f"{self.base_url}/v1/create/facturx",
            headers={**self._headers(), "Content-Type": "application/json"},
            json={"invoice": invoice_data},
            timeout=30,
        )
        response.raise_for_status()
        return response.content

    def validate(self, pdf_bytes: bytes) -> dict:
        response = requests.post(
            f"{self.base_url}/v1/validate/facturx",
            headers=self._headers(),
            files={"file": ("invoice.pdf", pdf_bytes, "application/pdf")},
            timeout=30,
        )
        return response.json()

    def generate_and_store(self, invoice_data: dict, invoice_number: str) -> str:
        pdf = self.generate(invoice_data)
        validation = self.validate(pdf)

        if not validation["valid"]:
            errors = "\n".join(
                f"[{e['rule']}] {e['message']}"
                for e in validation["errors"]
            )
            raise ValueError(f"Generated invoice failed validation:\n{errors}")

        path = f"invoices/facturx/{invoice_number}.pdf"
        default_storage.save(path, ContentFile(pdf))
        return path

Usage in a Django view or DRF viewset:

# invoicing/views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.core.files.storage import default_storage
from .models import Invoice
from .services.facturx import FacturXService

@api_view(["POST"])
def generate_invoice(request, pk):
    invoice = Invoice.objects.get(pk=pk)

    service = FacturXService()
    path = service.generate_and_store(
        invoice_data=invoice.to_facturx_dict(),
        invoice_number=invoice.number,
    )

    return Response({"path": path, "url": default_storage.url(path)})

FastAPI integration: async endpoint with httpx

For FastAPI applications, use httpx for async HTTP calls so the framework can handle other requests while waiting on the InvoiceXML response:

# main.py

import os
import httpx
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel

app = FastAPI()

class Address(BaseModel):
    line1: str
    city: str
    postCode: str
    country: str

class Party(BaseModel):
    name: str
    vatIdentifier: str | None = None
    postalAddress: Address

class PaymentDetails(BaseModel):
    paymentAccountIdentifier: str

class Item(BaseModel):
    name: str

class PriceDetails(BaseModel):
    netPrice: float

class VatInformation(BaseModel):
    rate: float

class InvoiceLine(BaseModel):
    quantity: float
    item: Item
    priceDetails: PriceDetails
    vatInformation: VatInformation

class InvoiceData(BaseModel):
    invoiceNumber: str
    issueDate: str
    currency: str
    seller: Party
    buyer: Party
    paymentDetails: PaymentDetails
    lines: list[InvoiceLine]


@app.post("/invoices/facturx")
async def create_facturx(invoice: InvoiceData):
    async with httpx.AsyncClient(timeout=30) as client:
        response = await client.post(
            "https://api.invoicexml.com/v1/create/facturx",
            headers={
                "Authorization": f"Bearer {os.environ['INVOICEXML_API_KEY']}",
                "Content-Type": "application/json",
            },
            json={"invoice": invoice.model_dump(exclude_none=True)},
        )

        if response.status_code != 200:
            raise HTTPException(
                status_code=response.status_code,
                detail=response.json().get("detail", "Generation failed"),
            )

        return Response(
            content=response.content,
            media_type="application/pdf",
            headers={
                "Content-Disposition": f"attachment; filename={invoice.invoiceNumber}.pdf"
            },
        )

The Pydantic models double as request validation and documentation. FastAPI generates an OpenAPI spec automatically that matches the InvoiceXML schema.


Odoo integration: extending the account module

For Odoo deployments serving French customers, replace or supplement Odoo's built-in Factur-X module with InvoiceXML for full Schematron validation and CIUS overlay support. Add to a custom Odoo module:

# models/account_move.py

import requests
from odoo import models, fields
from odoo.exceptions import UserError


class AccountMove(models.Model):
    _inherit = "account.move"

    facturx_pdf = fields.Binary(string="Factur-X PDF", attachment=True)
    facturx_validation_status = fields.Selection([
        ("pending", "Pending"),
        ("valid", "Valid"),
        ("invalid", "Invalid"),
    ], default="pending")

    def action_generate_facturx(self):
        for move in self:
            invoice_data = move._build_facturx_payload()
            api_key = self.env["ir.config_parameter"].sudo().get_param(
                "invoicexml.api_key"
            )

            response = requests.post(
                "https://api.invoicexml.com/v1/create/facturx",
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "application/json",
                },
                json={"invoice": invoice_data},
                timeout=30,
            )

            if response.status_code != 200:
                raise UserError(
                    f"Factur-X generation failed: {response.json().get('detail')}"
                )

            move.facturx_pdf = response.content
            move.facturx_validation_status = "valid"

    def _build_facturx_payload(self) -> dict:
        return {
            "invoiceNumber": self.name,
            "issueDate": self.invoice_date.isoformat(),
            "currency": self.currency_id.name,
            "seller": {
                "name": self.company_id.name,
                "vatIdentifier": self.company_id.vat,
                "postalAddress": {
                    "line1": self.company_id.street,
                    "city": self.company_id.city,
                    "postCode": self.company_id.zip,
                    "country": self.company_id.country_id.code,
                },
            },
            "buyer": {
                "name": self.partner_id.name,
                "postalAddress": {
                    "line1": self.partner_id.street,
                    "city": self.partner_id.city,
                    "postCode": self.partner_id.zip,
                    "country": self.partner_id.country_id.code,
                },
            },
            "paymentDetails": {
                "paymentAccountIdentifier": self.partner_bank_id.acc_number,
            },
            "lines": [{
                "quantity": line.quantity,
                "item": {"name": line.name},
                "priceDetails": {"netPrice": line.price_unit},
                "vatInformation": {
                    "rate": line.tax_ids[0].amount if line.tax_ids else 20,
                },
            } for line in self.invoice_line_ids if line.product_id],
        }

This replaces Odoo's native Factur-X output (which lacks full Schematron validation) with InvoiceXML's validated output, while keeping the rest of Odoo's invoicing workflow intact.


Error handling pattern for production

Wrap API calls with retry logic and structured error handling. Python applications often run in long-lived workers (Celery tasks, RQ jobs, Django Q clusters), and transient failures should not surface as fatal errors:

import os
import time
import requests


class InvoiceXMLError(Exception):
    pass


class InvoiceXMLClient:
    def __init__(
        self,
        api_key: str = None,
        max_retries: int = 2,
        timeout: int = 30,
    ):
        self.api_key = api_key or os.environ["INVOICEXML_API_KEY"]
        self.max_retries = max_retries
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {self.api_key}"

    def post(self, endpoint: str, **kwargs) -> requests.Response:
        url = f"https://api.invoicexml.com{endpoint}"

        for attempt in range(self.max_retries + 1):
            try:
                response = self.session.post(url, timeout=self.timeout, **kwargs)

                if response.status_code >= 500 and attempt < self.max_retries:
                    time.sleep(2 ** attempt)
                    continue

                if response.status_code == 401:
                    raise InvoiceXMLError("API key is invalid")

                if response.status_code == 400:
                    detail = response.json().get("detail", "Bad request")
                    raise InvoiceXMLError(f"Validation error: {detail}")

                response.raise_for_status()
                return response

            except requests.exceptions.Timeout:
                if attempt < self.max_retries:
                    continue
                raise InvoiceXMLError("Request timed out")

        raise InvoiceXMLError("Maximum retries exceeded")

Why use a REST API instead of building it in Python

The akretion/factur-x library is functional and well-maintained for what it covers: PDF/A-3b generation when given an existing PDF input plus CII XML, and basic XSD validation. For Odoo deployments and similar setups where Factur-X is a small piece of a larger invoicing flow, it works.

What it does not do, and what InvoiceXML handles:

EN 16931 Schematron validation across all 200+ business rules. The Python library validates against XSD only. XSD catches structural errors but not the business rule violations where most real-world rejections occur (VAT calculation mismatches, conditional field requirements, code list violations). Without Schematron, invoices can pass library validation while being rejected by Plateformes Agréées.

CII XML generation from structured data. The Python library accepts pre-built CII XML as input but does not generate it from a Pythonic data structure. You either need a second library or hand-write the XML, both of which are error-prone.

National CIUS validation overlays. The same InvoiceXML endpoint handles Peppol BIS 3.0 for Belgian and Dutch B2B, XRechnung CIUS-REC-DE for German public sector, NLCIUS for Netherlands public sector, EHF for Norway, and PINT variants for APAC markets. If your Python application serves customers in multiple European markets, switching profiles is a single parameter change.

Version maintenance. Updates for new ZUGFeRD versions, new Schematron releases, and new CIUS profile changes are deployed before the new specification's effective date with no integration change required on your side.

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 Python

OperationEndpointInputOutput
Create Factur-XPOST /v1/create/facturxJSON invoicePDF/A-3b binary
PDF to Factur-X (AI)POST /v1/transform/to/facturxPDF, image, DOCX, XLSXPDF/A-3b binary
Validate Factur-XPOST /v1/validate/facturxFactur-X PDFValidation JSON
Extract as JSONPOST /v1/extract/jsonFactur-X PDFStructured JSON
Extract CII XMLPOST /v1/extract/xmlFactur-X PDFCII XML
Render as PDFPOST /v1/render/cii/to/pdfCII XMLPDF binary

Full OpenAPI 3.1 schema: api.invoicexml.com/v1/openapi. Interactive API explorer: api.invoicexml.com/v1/scalar.


Get started

Every Python 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/python →

Create a free InvoiceXML account: includes 100 credits to get started, 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 Python application: Django, FastAPI, Flask, Odoo, Celery workers, data pipelines, or automation scripts.


Ready to automate your invoices?

Start your 30-day free trial. No credit card required.

Get Started For Free