Technique

Convertir une facture PDF en Factur-X 1.08 contrôlé (PDF/A-3 + XML CII D22B)

11 min de lecture Par FacturX API

Guide technique complet pour convertir un PDF en Factur-X PDF/A-3 avec XML CII embarqué. Mode extraction PDF et mode ERP (JSON). Exemples curl, Python, Node.js.

Comment convertir un PDF de facture classique en Factur-X PDF/A-3 contrôlé selon EN16931 ? Deux modes avec l’endpoint /convert : mode ERP (JSON structuré + PDF, déterministe) ou mode extraction (lecture automatique du PDF, OCR possible). Le résultat passe les contrôles XSD, Schematron et veraPDF exposés dans la réponse.

En bref

  • Endpoint : POST /api/v1/convert — input PDF (+ éventuellement invoice_data JSON), output PDF/A-3 Factur-X.
  • Mode ERP recommandé : vous fournissez les données comptables en JSON, le PDF reste le support visuel. Déterministe, sans OCR.
  • Mode extraction : le moteur lit le texte/OCR du PDF et en déduit les champs. Pratique pour numériser l’existant.
  • Contexte : Factur-X 1.08 / ZUGFeRD 2.4 (basé sur UN/CEFACT CII D22B), applicable depuis le 15 janvier 2026.
  • Quatre niveaux de contrôle exécutés et détaillés : PDF/A-3 ISO 19005-3, structure Factur-X, XSD CII, Schematron EN16931.

Quand utiliser cet endpoint

BesoinMode recommandé
Flux B2B production depuis ERP (Sage, SAP, Odoo, Dolibarr…)Mode ERP — données structurées en JSON
Numériser un stock de factures fournisseurs PDF existantesMode extraction — OCR activé automatiquement si nécessaire
Prototypage rapide sans intégration ERPMode extraction — faible friction
PDF généré par votre propre SI (layouts standards)Mode ERP — idéal, pas d’OCR à charger

Un PDF de facture classique ne suffit plus. La réforme de facturation électronique impose que les factures B2B échangées via les PA soient au format normé — Factur-X, UBL ou CII. Pour les millions de factures PDF qui circulent encore sans XML structuré, la question technique est concrète : comment convertir un PDF existant en document Factur-X conforme ?

Ce guide se place dans le contexte de Factur-X 1.08 / ZUGFeRD 2.4, version applicable depuis le 15 janvier 2026, basée sur UN/CEFACT CII D22B. Il couvre les deux approches de conversion, leurs cas d’usage, et leur implémentation via l’API FacturX. Le contexte réglementaire complet est traité dans Facturation électronique 2026 : le guide technique complet pour développeurs.

Pourquoi un PDF classique ne suffit pas

Un PDF “normal” est un document visuel. Il contient du texte rendu dans une mise en page, mais aucune donnée structurée exploitable par une machine. Un destinataire humain peut le lire — un ERP ne peut pas l’intégrer automatiquement sans ressaisie ou OCR.

Factur-X résout ce problème avec un format hybride : un PDF lisible par l’humain et un XML CII embarqué lisible par la machine, le tout dans un conteneur PDF/A-3 conforme à l’archivage long terme.

Les trois éléments manquants dans un PDF classique :

  1. Le XML CII (factur-x.xml) — les données structurées de la facture au format UN/CEFACT Cross-Industry Invoice
  2. Le conteneur PDF/A-3 — conformité ISO 19005-3 avec polices embarquées, profil ICC, métadonnées XMP
  3. Les métadonnées Factur-XAFRelationship cohérent avec la relation PDF/XML (Data, Source ou Alternative selon le cas — Alternative étant le plus courant dans le contexte Factur-X), namespace fx: dans le XMP, GuidelineID correspondant au profil

Construire ce pipeline soi-même — Ghostscript pour le PDF/A-3, une bibliothèque XML pour le CII, l’embedding avec les bons attributs, la validation veraPDF — est faisable mais fragile. Les pièges sont documentés dans PDF/A-3 pour Factur-X : checklist de conformité et pièges courants.

Anatomie d’un PDF Factur-X conforme

Avant de convertir, il faut comprendre ce qu’on produit. Un PDF Factur-X valide est un fichier qui passe quatre niveaux de validation :

1. PDF/A-3 (ISO 19005-3) — Le conteneur est conforme : polices embarquées, profil ICC dans OutputIntents, pas de JavaScript ni d’actions Launch, métadonnées XMP avec pdfaid:part=3.

2. Structure Factur-X — Un fichier factur-x.xml est présent dans le name tree EmbeddedFiles du catalogue PDF, avec un AFRelationship approprié (typiquement /Alternative pour Factur-X) et le type MIME text/xml.

3. XSD — Le XML embarqué est structurellement valide par rapport au schéma CII D22B (CrossIndustryInvoice_100pD22B.xsd).

4. Schematron — Le XML respecte les règles métier EN16931 : totaux arithmétiquement cohérents, codes devise ISO 4217, champs obligatoires présents selon le profil déclaré.

Le GuidelineID dans le XML doit correspondre au profil déclaré. Pour le profil EN16931 (recommandé) : urn:cen.eu:en16931:2017. Pour la liste complète des profils et leurs GuidelineIDs, voir Profils Factur-X : MINIMUM, BASIC, EN16931, EXTENDED.

Les deux modes de conversion

L’API FacturX propose deux approches distinctes pour convertir un PDF en Factur-X, selon la source des données.

Mode 1 : extraction automatique depuis le PDF

Le moteur lit le texte natif du PDF et identifie les champs de la facture : numéro, date d’émission, montants HT/TVA/TTC, informations vendeur et acheteur, lignes de facturation.

Si le PDF est scanné (image sans couche texte), un moteur OCR prend le relais automatiquement. Un score de confiance est calculé pour chaque champ extrait — le seuil par défaut est 70% en mode strict.

Cas d’usage :

  • Factures fournisseurs reçues en PDF classique
  • Numérisation de l’existant (archives PDF)
  • Prototypage rapide sans intégration ERP

Limites :

  • La qualité de l’extraction dépend de la mise en page du PDF
  • Les layouts exotiques (tableaux imbriqués, multi-colonnes) peuvent produire des résultats incomplets
  • Le mode OCR peut prendre plus de temps selon le nombre de pages

Mode 2 : données structurées depuis l’ERP (recommandé)

L’ERP fournit les données comptables au format JSON via le champ invoice_data. Le PDF reste le support visuel — les données structurées JSON sont la source de vérité pour générer le XML CII.

Cas d’usage :

  • Intégration ERP (Sage, SAP, Odoo, Dolibarr)
  • Flux B2B automatisés en production
  • Toute situation où les données sont déjà structurées dans le système source

Avantages :

  • Déterministe : pas d’extraction aléatoire, pas d’OCR
  • Source structurée : les données viennent directement du système comptable

Exemple pratique : mode ERP

Le mode ERP est l’approche recommandée pour la production. L’appel API envoie le PDF et les données structurées en parallèle.

curl

curl -X POST https://api.facturxapi.com/api/v1/convert \
  -H "Authorization: Bearer votre-cle-api" \
  -F "file=@./facture.pdf" \
  -F 'invoice_data={
    "invoice_number": "FA-2026-042",
    "issue_date": "2026-04-01",
    "invoice_type": "380",
    "currency": "EUR",
    "seller": {
      "name": "Ma Société SAS",
      "siret": "12345678900012",
      "vat_id": "FR12345678901",
      "address": {
        "street": "10 Rue de Rivoli",
        "city": "Paris",
        "postal_code": "75001",
        "country": "FR"
      }
    },
    "buyer": {
      "name": "Client SA",
      "address": { "country": "FR" }
    },
    "line_items": [
      {
        "number": "1",
        "description": "Prestation conseil",
        "quantity": 1,
        "unit": "C62",
        "unit_price": 1000,
        "net_amount": 1000,
        "vat_rate": 20,
        "vat_category": "S"
      }
    ],
    "tax_breakdown": [
      {
        "rate": 20,
        "category": "S",
        "base": 1000,
        "amount": 200
      }
    ],
    "totals": {
      "net": 1000,
      "tax": 200,
      "gross": 1200,
      "due": 1200
    }
  }'

La réponse enveloppe le résultat dans success, target et result. Les artefacts sont encodés en base64 dans result.pdf et result.xml :

{
  "success": true,
  "target": {
    "requested": "en16931",
    "executed": "en16931",
    "status": "verified",
    "missing_inputs": [],
    "not_run_reason": null
  },
  "result": {
    "durationMs": 2500,
    "targetProfile": "EN16931",
    "conversionSuccessful": true,
    "packagingPerformed": true,
    "xml": "PD94bWwg...",
    "xmlSize": 4096,
    "pdf": "JVBERi0x...",
    "pdfSize": 102400,
    "extraction": {
      "sourceType": "structured_data",
      "pageCount": 1
    },
    "validation": {
      "valid": true,
      "profile": "EN16931",
      "summary": { "errorCount": 0, "warningCount": 0 }
    },
    "processedInvoicesCharged": 1,
    "remainingInvoices": 999
  }
}

Python

import requests
import json
import base64
import os
from pathlib import Path

API_KEY = "votre-cle-api"
API_URL = "https://api.facturxapi.com/api/v1/convert"

invoice_data = {
    "invoice_number": "FA-2026-042",
    "issue_date": "2026-04-01",
    "invoice_type": "380",
    "currency": "EUR",
    "seller": {
        "name": "Ma Société SAS",
        "siret": "12345678900012",
        "vat_id": "FR12345678901",
        "address": {
            "street": "10 Rue de Rivoli",
            "city": "Paris",
            "postal_code": "75001",
            "country": "FR",
        },
    },
    "buyer": {
        "name": "Client SA",
        "address": {"country": "FR"},
    },
    "line_items": [
        {
            "number": "1",
            "description": "Prestation conseil",
            "quantity": 1,
            "unit": "C62",
            "unit_price": 1000,
            "net_amount": 1000,
            "vat_rate": 20,
            "vat_category": "S",
        }
    ],
    "tax_breakdown": [
        {
            "rate": 20,
            "category": "S",
            "base": 1000,
            "amount": 200,
        }
    ],
    "totals": {
        "net": 1000,
        "tax": 200,
        "gross": 1200,
        "due": 1200,
    },
}

with open("facture.pdf", "rb") as f:
    response = requests.post(
        API_URL,
        headers={"Authorization": f"Bearer {API_KEY}"},
        files={"file": ("facture.pdf", f, "application/pdf")},
        data={"invoice_data": json.dumps(invoice_data)},
        timeout=90,
    )

response.raise_for_status()
payload = response.json()
target = payload.get("target") or {}
result = payload.get("result") or {}
target_status = target.get("status")

if target_status not in {"verified", "incomplete"}:
    raise ValueError(f"Conversion sans livrable exploitable: {payload}")

pdf_base64 = result.get("pdf")
xml_base64 = result.get("xml")
if result.get("packagingPerformed") is not True or not pdf_base64 or not xml_base64:
    raise ValueError("Livrable PDF/XML absent de la réponse")

def write_base64_atomically(encoded, destination):
    destination = Path(destination)
    temporary = destination.with_name(f".{destination.name}.{os.getpid()}.tmp")
    try:
        decoded = base64.b64decode(encoded, validate=True)
        if destination.suffix == ".pdf" and not decoded.startswith(b"%PDF-"):
            raise ValueError("Le livrable décodé n'est pas un PDF")
        temporary.write_bytes(decoded)
        temporary.replace(destination)
    finally:
        temporary.unlink(missing_ok=True)

write_base64_atomically(pdf_base64, "facture-facturx.pdf")
write_base64_atomically(xml_base64, "factur-x.xml")

charged = result.get("processedInvoicesCharged")
remaining = result.get("remainingInvoices")
if target_status == "incomplete":
    readiness = result.get("validationReadiness") or {}
    reservations = (readiness.get("frCtc") or {}).get("advisories") or payload.get("warnings") or []
    print(f"Fichier généré avec réserves — à traiter avant transmission: {reservations}")
else:
    profile = result["validation"]["profile"]
    print(f"Conversion vérifiée pour la cible demandée — profil {profile}")

print(f"Quota: {charged} facture traitée, {remaining} restante(s)")

Exemple pratique : mode extraction PDF

Quand les données structurées ne sont pas disponibles, l’API extrait les informations directement depuis le texte du PDF.

curl -X POST https://api.facturxapi.com/api/v1/convert \
  -H "Authorization: Bearer votre-cle-api" \
  -F "file=@./facture.pdf"

La réponse indique la méthode d’extraction utilisée :

{
  "success": true,
  "target": {
    "requested": "en16931",
    "executed": "en16931",
    "status": "verified",
    "missing_inputs": [],
    "not_run_reason": null
  },
  "result": {
    "durationMs": 4200,
    "targetProfile": "EN16931",
    "conversionSuccessful": true,
    "packagingPerformed": true,
    "xml": "PD94bWwg...",
    "xmlSize": 3584,
    "pdf": "JVBERi0x...",
    "extraction": {
      "sourceType": "native_text",
      "pageCount": 1,
      "extractionQualityScore": 0.92,
      "extractionFieldsCount": 18
    },
    "validation": {
      "valid": true,
      "profile": "EN16931"
    },
    "processedInvoicesCharged": 1,
    "remainingInvoices": 999
  }
}

Le champ extraction.sourceType indique comment les données ont été obtenues :

  • native_text — le PDF contient une couche texte exploitable
  • ocr — le PDF est une image, le moteur OCR a été utilisé
  • structured_data — les données viennent du champ invoice_data (mode ERP)

Le champ extractionQualityScore (0 à 1) donne une indication globale de la fiabilité de l’extraction. En mode strict (défaut), une confiance inférieure à 0.70 provoque un rejet 422 avec le code ocr_confidence_too_low.

Validation intégrée

Chaque conversion exécute la séquence de contrôles documentaires du profil demandé et en expose le résultat. Les quatre étapes sont exécutées dans l’ordre :

  1. PDF/A-3 — conformité ISO 19005-3 via veraPDF
  2. Structure Factur-X — présence du XML embarqué avec les bons attributs
  3. XSD — validation structurelle du XML CII
  4. Schematron — validation des règles métier EN16931

La réponse de conversion expose le détail des contrôles exécutés dans le champ validation. Un livrable ne doit être présenté comme vérifié que si le profil demandé est effectivement marqué verified ; sinon, la réponse conserve les erreurs ou l’état non évalué. Cette vérification reste bornée aux profils exécutés et ne vaut ni transmission ni acceptation par une PA ou une solution compatible (SC).

Pour comprendre les erreurs Schematron et les déboguer, voir Valider EN16931/Factur-X : XSD vs Schematron, erreurs BR-*.

Quotas

1 livrable final = 1 facture traitée

Un appel abouti à /validate, /extract ou /repair décompte une facture traitée. Une génération /convert réserve une facture, décomptée si un livrable final est produit et remboursée sinon. Une simulation /convert avec dry_run=true ne consomme pas le quota et ne produit aucun fichier final.

Pour tester avec le plan Sandbox, consultez les quotas et conditions à jour sur la page pricing.

Gestion des erreurs

Les erreurs de conversion retournent un code HTTP avec un payload JSON structuré :

Code HTTPErreurCause
415Unsupported Media TypeLe fichier envoyé n’est pas un PDF
422insufficient_dataLe texte extrait ne contient pas assez d’informations pour construire un XML CII valide
422ocr_confidence_too_lowLe score de confiance OCR est inférieur au seuil (mode strict)
402quota_exceededQuota mensuel épuisé
429rate_limit_exceededTrop de requêtes simultanées
503Service UnavailableService OCR temporairement indisponible

Exemple de réponse 422 :

{
  "error": "insufficient_data",
  "message": "Cannot extract enough invoice fields from PDF text",
  "missing_fields": ["seller.vat_id", "buyer.name"],
  "diagnostics": [
    {
      "field": "seller.vat_id",
      "reason": "No VAT number pattern found",
      "suggestion": "Provide invoice_data with seller.vat_id"
    }
  ]
}

La réponse diagnostics indique exactement quels champs manquent et suggère d’utiliser le mode ERP (invoice_data) pour les fournir explicitement. C’est le cas le plus fréquent : un PDF dont la mise en page ne permet pas d’extraire tous les champs obligatoires EN16931.

Idempotence et retries

En production, les appels réseau échouent. Un timeout, une déconnexion, une erreur 503 transitoire — le client doit pouvoir réessayer sans risque de double traitement.

L’API supporte le header Idempotency-Key :

curl -X POST https://api.facturxapi.com/api/v1/convert \
  -H "Authorization: Bearer votre-cle-api" \
  -H "Idempotency-Key: fa-2026-042-convert-v1" \
  -F "file=@./facture.pdf" \
  -F 'invoice_data={...}'

Si le serveur a déjà traité une requête avec la même clé d’idempotence, il retourne le résultat en cache au lieu de relancer la conversion. La clé doit être unique par opération logique — typiquement le numéro de facture combiné avec un suffixe de version.

Le pattern recommandé en production — la clé doit être stable pour la même opération logique (même facture, même payload) afin que les retries retournent le résultat en cache :

# Clé stable par facture — même clé à chaque retry = idempotence garantie
idempotency_key = f"convert-{invoice_number}-v1"

response = requests.post(
    API_URL,
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Idempotency-Key": idempotency_key,
    },
    files={"file": ("facture.pdf", pdf_file, "application/pdf")},
    data={"invoice_data": json.dumps(invoice_data)},
    timeout=90,
)

Si le payload métier change réellement (correction de montant, ajout d’une ligne), incrémentez la version : convert-FA-2026-042-v2.

Erreurs fréquentes lors de la conversion

Les rejets Schematron/veraPDF les plus communs sur un output /convert (avec lien vers la fiche dédiée) :

Le catalogue complet des erreurs BR-* liste les 22 fiches unitaires par famille.

Et maintenant ?

Convertir mon PDF et contrôler le résultat →

set -euo pipefail

response_file="$(mktemp "${TMPDIR:-/tmp}/facturx-response.XXXXXX")"
pdf_tmp="$(mktemp "./facture-facturx.pdf.tmp.XXXXXX")"
trap 'rm -f "$response_file" "$pdf_tmp"' EXIT

curl --fail-with-body --silent --show-error \
  -X POST https://api.facturxapi.com/api/v1/convert \
  -H "Authorization: Bearer VOTRE_CLE" \
  -F "file=@./facture.pdf" \
  --output "$response_file"

jq -e '
  (.target.status == "verified" or .target.status == "incomplete") and
  (.result.packagingPerformed == true) and
  (.result.pdf | type == "string" and length > 0)
' "$response_file" > /dev/null

jq -er '.result.pdf' "$response_file" | base64 --decode > "$pdf_tmp"
test "$(head -c 5 "$pdf_tmp")" = "%PDF-"
mv "$pdf_tmp" facture-facturx.pdf

if jq -e '.target.status == "incomplete"' "$response_file" > /dev/null; then
  jq -r '.result.validationReadiness.frCtc.advisories // .warnings // []' "$response_file" >&2
  printf '%s\n' 'Fichier généré avec réserves : traitez-les avant transmission.' >&2
fi

La clé Sandbox permet de tester l’API sans carte bancaire. Le scanner public reste gratuit et séparé du quota API. Consultez les quotas et conditions à jour, puis obtenez une clé API.

Aller plus loin

La conversion est une étape du pipeline. Pour une intégration complète :

La conversion produit un document Factur-X et indique les contrôles exécutés. Le dépôt, la transmission et l’acceptation par une PA restent un parcours séparé, décrit dans le guide technique 2026.

#factur-x #convert #PDF/A-3 #CII #API #ERP #OCR #EN16931