Invoice Ninja v5 est l’une des rares plateformes de facturation open source qui propose un support natif de la facturation électronique européenne. Son backend Laravel s’appuie sur la bibliothèque PHP horstoeko/zugferd pour produire des documents Factur-X, ZUGFeRD et XRechnung. Pour les développeurs et intégrateurs qui accompagnent des PME françaises vers la réforme de septembre 2026, cette architecture pose une question concrète : le pipeline de génération d’Invoice Ninja produit-il des fichiers conformes aux exigences françaises, ou faut-il intervenir en amont ?
Cet article couvre l’architecture interne du pipeline e-invoicing d’Invoice Ninja, la configuration nécessaire pour la conformité française, les erreurs de validation spécifiques à cette stack, et la stratégie de validation pré-envoi à mettre en place avant de raccorder une PDP.
Pour les erreurs de validation courantes et les corrections rapides spécifiques à Invoice Ninja, voir InvoiceNinja et Factur-X : valider ses factures avant la réforme 2026. Pour le contexte général de la réforme, voir Facturation électronique 2026 : le guide technique complet.
Architecture du pipeline e-invoicing
Le pipeline de génération d’une facture électronique dans Invoice Ninja v5 traverse quatre couches distinctes, chacune avec ses propres responsabilités et points de défaillance possibles.
Couche 1 — Modèle métier Laravel
Invoice Ninja stocke les données de facturation dans un modèle Eloquent classique. Les entités clés sont Invoice, Company, Client, Product et Payment. Les données fiscales (taux TVA, montants HT/TTC, remises) sont calculées par le service InvoiceCalc avant sérialisation.
Le point critique à ce niveau : Invoice Ninja gère les arrondis monétaires via la propriété precision de l’entité Currency. Pour l’euro, la précision est de 2 décimales. Les calculs intermédiaires peuvent introduire des écarts de centimes qui deviennent des erreurs BR-CO-14 ou BR-CO-15 en sortie Schematron.
Couche 2 — horstoeko/zugferd (génération CII XML)
La bibliothèque horstoeko/zugferd est le moteur de sérialisation XML. Elle prend en charge la norme UN/CEFACT Cross-Industry Invoice (CII) dans sa syntaxe D16B et son extension D22B. Factur-X 1.08 et ZUGFeRD 2.4 utilisent CII D22B. Le binding CEN officiel EN16931-3-5 est basé sur D16B ; D22B est une extension rétrocompatible adoptée par les standards Factur-X et ZUGFeRD.
Invoice Ninja instancie un objet ZugferdDocumentBuilder et y injecte les données métier :
use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdProfiles;
$builder = ZugferdDocumentBuilder::CreateNew(
ZugferdProfiles::PROFILE_EN16931
);
$builder
->setDocumentInformation(
$invoice->number, // BT-1: numéro de facture
'380', // BT-3: type (380 = facture commerciale)
\DateTime::createFromFormat('Y-m-d', $invoice->date),
$invoice->client->currency()->code // BT-5: devise
)
->setDocumentSeller($company->name)
->setDocumentSellerAddress(
$company->address1,
'',
'',
$company->postal_code,
$company->city,
$company->country->iso_3166_2 // BT-40: code pays vendeur
);
Cette couche est responsable de la structure XML, des namespaces CII et du GuidelineID qui identifie le profil Factur-X. Toute erreur dans le mapping entre le modèle Laravel et les paramètres du builder se traduit par un XML structurellement incorrect.
Couche 3 — Profil et GuidelineID
Le GuidelineID est l’identifiant technique du profil Factur-X inscrit dans l’élément ExchangedDocumentContext/GuidelineSpecifiedDocumentContextParameter/ID du XML. C’est la première chose que vérifie un validateur.
Pour Factur-X 1.08 (CII D22B), les identifiants de profil sont :
| Profil | GuidelineID |
|---|---|
| MINIMUM | urn:factur-x.eu:1p0:minimum |
| BASIC WL | urn:factur-x.eu:1p0:basicwl |
| BASIC | voir note ci-dessous |
| EN16931 | urn:cen.eu:en16931:2017 |
| EXTENDED | voir note ci-dessous |
Les URNs complètes pour BASIC et EXTENDED incluent le préfixe CEN :
- BASIC :
urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic - EXTENDED :
urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
La bibliothèque horstoeko/zugferd sélectionne automatiquement le bon GuidelineID en fonction du profil passé au constructeur (ZugferdProfiles::PROFILE_EN16931). Mais si le profil configuré dans Invoice Ninja ne correspond pas au niveau de détail effectivement renseigné dans le XML, la validation Schematron échouera sur les champs obligatoires manquants pour ce profil.
Pour comprendre les implications de chaque profil, voir Profils Factur-X : MINIMUM, BASIC WL, BASIC, EN16931, EXTENDED.
Couche 4 — Embedding PDF/A-3
La dernière étape consiste à embarquer le fichier XML CII dans un conteneur PDF/A-3 (ISO 19005-3). Invoice Ninja utilise une bibliothèque PDF (typiquement snappdf ou wkhtmltopdf selon la configuration) pour générer le rendu visuel, puis horstoeko/zugferd s’occupe de l’attachement du XML avec les métadonnées AF (Associated Files) requises par la norme.
Le fichier XML est attaché avec le relationship Alternative et le MIME type text/xml. Le nom du fichier XML embarqué est conventionnellement factur-x.xml pour Factur-X ou zugferd-invoice.xml pour ZUGFeRD, mais la spécification ne rend pas ce nom normatif — c’est une convention largement suivie pour l’interopérabilité.
Configuration pour la conformité française
Invoice Ninja expose la configuration e-invoicing dans Settings > E-Invoice au niveau de chaque entreprise. Mais la conformité française exige des données qui ne sont pas toutes accessibles depuis cette interface. Voici la cartographie complète.
Identifiants légaux du vendeur
Le cadre EN16931 requiert BT-29, BT-30 ou BT-31 selon le contexte (au moins un identifiant vendeur doit être présent, cf. BR-CO-26). Les exigences précises dépendent du CIUS ou du profil d’échange utilisé. En pratique pour la France, les deux identifiants suivants sont attendus.
BT-30 — Identifiant légal du vendeur (SIRET)
Le SIRET à 14 chiffres de l’établissement émetteur doit figurer dans le champ SellerLegalRegistrationIdentifier. Dans le XML CII :
<ram:SpecifiedLegalOrganization>
<ram:ID schemeID="0009">12345678901234</ram:ID>
</ram:SpecifiedLegalOrganization>
Le code ICD 0009 (ISO 6523) identifie un numéro SIRET. Attention : le code 0002 correspond au SIREN (9 chiffres), pas au SIRET (14 chiffres). Pour la réforme française, le SIRET est attendu car il identifie l’établissement, pas seulement l’entité juridique.
Dans Invoice Ninja, cette donnée se configure dans Settings > Company Details. Le champ “ID Number” ou un champ personnalisé doit contenir le SIRET. Côté code, le mapping dans le service e-invoice :
// Injection du SIRET vendeur (BT-30)
$builder->setDocumentSellerLegalOrganisation(
$company->settings->id_number, // SIRET 14 chiffres
'0009', // ICD code SIRET (ISO 6523)
$company->settings->legal_entity_name ?? $company->name
);
BT-31 — Identifiant TVA du vendeur
Le numéro de TVA intracommunautaire (format FRXX999999999) doit figurer dans le champ SellerVATIdentifier :
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">FR32123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
Dans Invoice Ninja : Settings > Company Details > VAT Number. Ce champ est généralement bien mappé par défaut.
Identifiants de l’acheteur
Pour les transactions B2B soumises à la réforme, l’acheteur doit aussi être identifié.
BT-47 — Identifiant légal de l’acheteur
Le SIRET de l’acheteur se configure sur la fiche client dans Invoice Ninja (Clients > [Client] > Details). Le champ “ID Number” du client doit contenir le SIRET à 14 chiffres. Le mapping :
// Injection du SIRET acheteur (BT-47)
$builder->setDocumentBuyerLegalOrganisation(
$client->id_number, // SIRET client
'0009' // ICD code SIRET (ISO 6523)
);
BT-48 — Identifiant TVA de l’acheteur
Le numéro de TVA intracommunautaire de l’acheteur, renseigné dans le champ “VAT Number” de la fiche client.
Codes schemeID : la source d’erreurs silencieuses
Les codes schemeID (identifiants de schéma ISO 6523, dits codes ICD) identifient le type d’identifiant utilisé dans les champs BT-29, BT-30, BT-46, BT-47. Une erreur de schemeID ne déclenche pas toujours une erreur XSD — le schéma accepte n’importe quelle chaîne dans schemeID. C’est la validation Schematron qui rejette les codes invalides ou incohérents.
Les trois codes ICD pertinents pour la France :
| Code ICD | Identifiant | Usage |
|---|---|---|
0002 | SIREN (9 chiffres) | Identification de l’entité juridique |
0009 | SIRET (14 chiffres) | Identification de l’établissement |
9957 | TVA intracommunautaire FR | Identification fiscale |
L’erreur la plus courante : utiliser 0002 (SIREN) avec un numéro à 14 chiffres (SIRET), ou inversement. La règle Schematron vérifie la cohérence entre le code ICD et le format de l’identifiant.
Pour la cartographie complète des champs obligatoires et leurs codes, voir Champs obligatoires EN16931 : cartographie et mapping ERP.
Sélection du format et du profil
Dans Settings > E-Invoice, Invoice Ninja permet de choisir le standard de sortie :
- Factur-X (France) — profils MINIMUM à EXTENDED
- ZUGFeRD (Allemagne/Autriche) — profils équivalents
- XRechnung (Allemagne, secteur public) — CIUS allemand pouvant utiliser UBL ou CII comme syntaxe. La bibliothèque
horstoeko/zugferdsupporte exclusivement la syntaxe CII.
Pour la réforme française, le choix doit être Factur-X avec le profil EN16931 au minimum. Le profil MINIMUM est insuffisant pour les transactions B2B soumises à la réforme : il ne contient pas assez de champs pour satisfaire les règles Schematron FR.
Le profil EXTENDED est acceptable (il est un sur-ensemble d’EN16931), mais il ajoute des champs qui ne sont pas requis par les PDP françaises. Sauf besoin métier spécifique, EN16931 est le choix optimal.
Pour comprendre la différence entre Factur-X, UBL et CII et les implications de chaque format, voir Factur-X vs UBL vs CII : quel format choisir.
Erreurs de validation spécifiques au pipeline Invoice Ninja
Au-delà des erreurs génériques EN16931 documentées dans l’article compagnon, le pipeline Invoice Ninja + horstoeko/zugferd produit des erreurs qui trouvent leur cause dans l’architecture même de la stack.
BT-30 absent : configuration SIRET non propagée
Symptôme : Erreur Schematron sur l’absence de SellerLegalRegistrationIdentifier.
Cause racine : Le champ “ID Number” dans Invoice Ninja est optionnel. Si l’entreprise n’a pas renseigné son SIRET dans Settings > Company Details, le builder horstoeko/zugferd ne génère tout simplement pas le noeud SpecifiedLegalOrganization. Le XML est valide en XSD (le champ est optionnel dans le schéma CII), mais invalide en Schematron FR (le champ est requis pour les factures françaises).
Correction : Renseigner le SIRET dans les paramètres de l’entreprise. Vérifier ensuite que le service e-invoice d’Invoice Ninja passe bien cette valeur au builder :
// Vérification dans le service e-invoice
if (empty($company->settings->id_number)) {
throw new \RuntimeException(
'SIRET requis pour la facturation électronique française (BT-30)'
);
}
Incohérence schemeID : code ICD manquant ou incorrect
Symptôme : L’identifiant est présent dans le XML mais la validation Schematron signale une erreur sur le code scheme.
Cause racine : Invoice Ninja ne force pas le code schemeID (ICD) lors de l’injection des identifiants. Si le développeur ou l’intégrateur ne spécifie pas explicitement le schemeID dans l’appel au builder, horstoeko/zugferd peut omettre l’attribut ou utiliser une valeur par défaut qui ne correspond pas au contexte français.
Correction : Toujours passer le code ICD explicitement :
// Correct : schemeID ICD explicite
$builder->setDocumentSellerLegalOrganisation(
'12345678901234', // SIRET
'0009' // ICD code pour SIRET (ISO 6523)
);
// Incorrect : schemeID absent — horstoeko peut utiliser une valeur par défaut
$builder->setDocumentSellerLegalOrganisation(
'12345678901234'
);
GuidelineID incompatible avec le contenu
Symptôme : Échec Schematron multiple — plusieurs champs obligatoires manquants simultanément.
Cause racine : Le profil sélectionné dans Invoice Ninja (via Settings > E-Invoice) déclare un GuidelineID EN16931 dans le XML, mais les données effectivement renseignées dans le système ne couvrent que le niveau BASIC ou BASIC WL. Le validateur compare le contenu au profil déclaré et signale chaque champ manquant.
C’est un problème d’architecture : Invoice Ninja ne vérifie pas à la génération que toutes les données requises par le profil sélectionné sont effectivement disponibles. Le fichier est produit avec des trous, et c’est la validation externe qui les détecte.
Correction : Soit compléter toutes les données requises pour le profil EN16931 (identifiants légaux, conditions de paiement, coordonnées bancaires), soit abaisser le profil à un niveau correspondant aux données disponibles — en gardant à l’esprit que les profils inférieurs à EN16931 ne sont pas acceptés pour la réforme française.
Conditions de paiement : BT-20 vide ou mal formaté
Symptôme : Avertissement ou erreur sur PaymentTerms (BT-20).
Cause racine : Invoice Ninja stocke les conditions de paiement sous forme de jours (net_30, net_60, etc.) dans le modèle PaymentTerm. Le mapping vers le champ CII SpecifiedTradePaymentTerms/Description (BT-20) est parfois absent ou génère une chaîne vide si aucun terme n’est configuré sur la facture.
Pour le profil EN16931, BT-20 est conditionnel : il devient obligatoire si aucune date d’échéance (BT-9) n’est spécifiée. Si ni BT-9 ni BT-20 ne sont présents, la validation Schematron échoue.
Correction : S’assurer que chaque facture a soit une date d’échéance (due_date dans Invoice Ninja), soit des conditions de paiement textuelles :
// Mapping des conditions de paiement
if ($invoice->due_date) {
$builder->setDocumentPaymentTerm(
'', // description optionnelle si date présente
\DateTime::createFromFormat('Y-m-d', $invoice->due_date)
);
} elseif ($invoice->payment_terms) {
$builder->setDocumentPaymentTerm(
"Net {$invoice->payment_terms} jours"
);
} else {
// Fallback : date d'émission + 30 jours
$dueDate = (new \DateTime($invoice->date))
->modify('+30 days');
$builder->setDocumentPaymentTerm('', $dueDate);
}
Arrondis monétaires : divergence entre calcul Laravel et règles BR-CO
Symptôme : Erreur BR-CO-14 (total TVA) ou BR-CO-15 (total TTC) avec un écart de 0.01.
Cause racine : Invoice Ninja calcule les montants ligne par ligne avec arrondi à chaque étape. Les règles Schematron BR-CO-14 et BR-CO-15 vérifient que la somme des montants TVA par catégorie correspond au total TVA déclaré. Si les arrondis intermédiaires divergent, même d’un centime, la validation échoue.
Ce problème est documenté dans l’article sur la validation Invoice Ninja. La solution architecturale consiste à recalculer les totaux à partir des valeurs arrondies des lignes, et non à arrondir le total recalculé à partir des valeurs brutes.
Workflow de validation pré-envoi
Avant de raccorder Invoice Ninja à une PDP, il est indispensable de valider systématiquement les fichiers produits. La validation en amont évite les rejets en production et les retards de paiement qui en découlent.
Pipeline automatisé PHP
Le workflow recommandé s’intègre dans le cycle de vie de la facture Invoice Ninja, après la génération du PDF Factur-X et avant l’envoi :
use Illuminate\Support\Facades\Http;
class FacturxValidationService
{
private string $apiUrl = 'https://facturxapi.com/api/v1/validate';
private string $apiKey;
public function __construct(string $apiKey)
{
$this->apiKey = $apiKey;
}
/**
* Valide un fichier Factur-X et retourne le rapport.
*
* @param string $pdfPath Chemin vers le fichier PDF Factur-X
* @return array{valid: bool, errors: array, warnings: array}
* @throws \RuntimeException Si la validation échoue techniquement
*/
public function validate(string $pdfPath): array
{
$response = Http::withHeaders([
'X-API-Key' => $this->apiKey,
])->attach(
'file',
file_get_contents($pdfPath),
basename($pdfPath)
)->post($this->apiUrl);
if (!$response->successful()) {
throw new \RuntimeException(
"Validation API error: {$response->status()}"
);
}
return $response->json();
}
/**
* Valide et bloque l'envoi si des erreurs sont détectées.
*
* @param string $pdfPath Chemin vers le fichier PDF Factur-X
* @return array Le rapport de validation complet
* @throws \DomainException Si le fichier contient des erreurs
*/
public function validateOrFail(string $pdfPath): array
{
$report = $this->validate($pdfPath);
if (!$report['valid']) {
$errorIds = array_map(
fn($e) => $e['id'],
$report['errors']
);
throw new \DomainException(
'Facture non conforme EN16931 : '
. implode(', ', $errorIds)
);
}
return $report;
}
}
Intégration dans un Event Listener
Invoice Ninja utilise un système d’événements Laravel. Le point d’intégration naturel est l’événement de finalisation de facture :
use App\Events\Invoice\InvoiceWasCreated;
use App\Services\FacturxValidationService;
use Illuminate\Support\Facades\Log;
class ValidateFacturxListener
{
public function __construct(
private FacturxValidationService $validator
) {}
public function handle(InvoiceWasCreated $event): void
{
$invoice = $event->invoice;
$pdfPath = $invoice->getEInvoicePdfPath();
if (!$pdfPath || !file_exists($pdfPath)) {
return;
}
try {
$report = $this->validator->validate($pdfPath);
if (!$report['valid']) {
Log::warning('Factur-X validation failed', [
'invoice_id' => $invoice->id,
'invoice_number' => $invoice->number,
'errors' => $report['errors'],
]);
// Optionnel : marquer la facture comme non conforme
$invoice->e_invoice_status = 'validation_failed';
$invoice->save();
}
} catch (\Throwable $e) {
Log::error('Factur-X validation service error', [
'invoice_id' => $invoice->id,
'exception' => $e->getMessage(),
]);
}
}
}
Validation par lot (batch)
Pour valider un stock existant de factures avant la migration vers une PDP :
# Exporter les PDF Factur-X depuis Invoice Ninja
# puis valider en lot avec curl
for pdf in /path/to/invoices/*.pdf; do
echo "Validation de $(basename "$pdf")..."
result=$(curl -s -X POST https://facturxapi.com/api/v1/validate \
-H "X-API-Key: votre-cle-api" \
-F "file=@$pdf")
valid=$(echo "$result" | jq -r '.valid')
if [ "$valid" = "false" ]; then
echo " ECHEC: $(echo "$result" | jq -r '[.errors[].id] | join(", ")')"
else
echo " OK"
fi
done
Ce script produit un rapport texte qui liste les factures non conformes et les codes d’erreur associés. C’est le point de départ pour identifier les corrections de configuration à apporter dans Invoice Ninja avant le raccordement PDP.
Pour comprendre chaque code d’erreur BR-* retourné par la validation, voir Catalogue des erreurs BR-*.
Préparer l’intégration PDP
Le raccordement d’Invoice Ninja à une Plateforme de Dématérialisation Partenaire (PDP) est l’étape finale de la mise en conformité. Les PDP accréditées par la DGFiP sont les seuls intermédiaires autorisés pour l’acheminement des factures électroniques entre entreprises.
Ce que la PDP attend
Une PDP ne reçoit pas un simple fichier PDF par email. L’interface technique entre un logiciel de facturation et une PDP repose sur une API (REST ou AS4 selon les PDP) qui attend :
- Un fichier Factur-X conforme — PDF/A-3 avec XML CII D22B embarqué, profil EN16931 minimum, passant la validation XSD et Schematron
- Des métadonnées de routage — SIRET émetteur, SIRET destinataire, type de document (facture, avoir, etc.)
- Un identifiant d’annuaire — chaque entreprise est enregistrée dans l’annuaire centralisé géré par la DGFiP, qui associe un SIRET à sa PDP de réception
Les trois niveaux de préparation
Niveau 1 — Conformité technique du fichier
C’est le sujet de cet article : s’assurer que chaque facture produite par Invoice Ninja passe la validation EN16931 avant envoi. Sans cette base, aucune PDP n’acceptera le fichier.
Niveau 2 — Données de routage
Invoice Ninja doit fournir les SIRET vendeur et acheteur pour que la PDP puisse router la facture. Ces données doivent être renseignées dans les fiches entreprise et client comme décrit dans la section configuration ci-dessus. La PDP utilise le SIRET acheteur pour interroger l’annuaire et déterminer la PDP de réception du destinataire.
Niveau 3 — Connecteur PDP
À ce jour, Invoice Ninja ne fournit pas de connecteur natif vers les PDP françaises. L’intégration se fait via :
- Un module custom Laravel qui appelle l’API de la PDP choisie après validation
- Un connecteur tiers développé par un intégrateur ou un éditeur de PDP
- Un export manuel avec dépôt sur le portail web de la PDP (solution de transition)
L’architecture recommandée pour un connecteur automatisé :
Invoice Ninja
│
▼ (génération Factur-X)
horstoeko/zugferd → PDF/A-3
│
▼ (validation pré-envoi)
FacturX API → rapport validation
│
▼ (si conforme)
Connecteur PDP → API PDP
│
▼ (accusé de réception)
Mise à jour statut Invoice Ninja
Calendrier d’action
Pour une PME utilisant Invoice Ninja et ciblant la conformité réforme 2026 :
| Échéance | Action | Statut requis |
|---|---|---|
| Maintenant | Renseigner SIRET + TVA dans les paramètres entreprise et clients | Configuration |
| Maintenant | Sélectionner le profil EN16931 dans Settings > E-Invoice | Configuration |
| T-6 mois | Valider un lot de factures représentatives via l’API FacturX | Validation |
| T-4 mois | Corriger les erreurs de configuration identifiées | Correction |
| T-3 mois | Choisir une PDP et démarrer l’intégration technique | Développement |
| T-1 mois | Tester le flux complet en environnement de recette PDP | Recette |
| Sept. 2026 | Réception obligatoire — le flux entrant doit être opérationnel | Production |
Vérifications avant mise en production
Avant de basculer le flux de facturation en production via une PDP, valider ces points :
- Chaque facture générée passe la validation EN16931 (XSD + Schematron) sans erreur
- Le SIRET vendeur (BT-30) est présent avec le schemeID ICD
0009 - Le numéro de TVA vendeur (BT-31) est présent avec le scheme
VA - Le SIRET acheteur (BT-47) est renseigné pour chaque client B2B
- Les conditions de paiement (BT-20) ou la date d’échéance (BT-9) sont présentes
- Le profil déclaré (
GuidelineID) correspond au contenu effectif du XML - Le PDF est conforme PDF/A-3 (polices embarquées, pas de dépendances externes)
Pour la méthodologie de validation détaillée (XSD puis Schematron, debug des erreurs BR-*), voir Valider EN16931/Factur-X : XSD vs Schematron, erreurs BR-*.
Invoice Ninja offre une base solide pour la facturation électronique, mais la conformité française exige une configuration rigoureuse et une validation systématique. Le pipeline horstoeko/zugferd produit un XML techniquement correct — c’est la complétude des données métier et la cohérence des codes schemeID qui déterminent si le fichier passera la validation PDP. Mettre en place une validation pré-envoi via l’API FacturX est le moyen le plus direct de détecter et corriger les écarts avant qu’ils ne deviennent des rejets en production.