Documentation

API de factures électroniques

Un appel POST valide une facture UBL, CII, XRechnung ou Factur-X selon les règles officielles EN 16931 et renvoie chaque erreur expliquée en langage clair. Un autre construit une facture Factur-X depuis du JSON et la valide avant de vous la rendre. Cette page est toute la référence.

Démarrage rapide

Créez une clé d'API gratuite (100 validations plus 20 factures générées par mois, sans carte), puis validez votre première facture depuis le terminal :

Obtenir une clé d'API gratuite

curl https://api.einvoicekit.com/v1/validate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/xml" \
  --data-binary @invoice.xml

Authentification

Votre clé voyage dans l'en-tête Authorization. Seul POST /v1/validate répond aussi sans clé, sur le pool gratuit décrit dans sa section ; la génération exige toujours une clé :

Authorization: Bearer eik_live_...

Les clés commencent par eik_live_ et ne s'affichent qu'une fois, à la création, sur votre tableau de bord. Traitez-les comme des mots de passe : variables d'environnement, jamais un dépôt.

Paquets

La même validation sous forme de paquet, sans code HTTP à écrire : une fonction et une commande, sur npm et sur PyPI. Sans clé, ils tournent sur le pool gratuit décrit sous POST /v1/validate ; avec EINVOICEKIT_API_KEY définie, sur votre compte.

Node.js 20+, depuis npm

npx @einvoicekit/einvoicekit invoice.pdf

npm install @einvoicekit/einvoicekit

Python 3.10+, depuis PyPI

pipx run einvoicekit invoice.pdf

pip install einvoicekit

La commande affiche chaque règle enfreinte et sort avec le code 0 si tous les fichiers sont valides, 2 si un fichier n'a pas pu être validé du tout, et 1 si le reste contient au moins une facture non conforme : elle s'insère telle quelle dans un pipeline CI. La fonction validate renvoie le même verdict depuis votre code : en JavaScript le JSON de cette API tel qu'il arrive, en Python un résultat typé qui conserve ce JSON dans .raw. Les deux paquets sont de simples clients : le fichier est envoyé en HTTPS, traité en mémoire puis abandonné, jamais stocké. Aucune dépendance, licence MIT, code source et README complets sur GitHub.

POST /v1/validate

Valide un document de facture et renvoie le verdict avec chaque règle enfreinte. Les deux syntaxes (UBL et CII) et les deux familles de profils (XRechnung, Factur-X/ZUGFeRD) sont détectées automatiquement.

Requête

Envoyez le XML en corps brut avec Content-Type: application/xml, ou en multipart/form-data avec un unique champ file. Un PDF Factur-X / ZUGFeRD est accepté tel quel : le XML embarqué est extrait avant validation. Taille maximale : 5 Mo.

Réponse

Toujours du JSON. valid est le verdict ; errors liste chaque règle enfreinte avec son identifiant EN 16931, le message officiel de la règle (qui nomme les termes métier en cause, BT-x) et le XPath de l'élément concerné. warnings a la même forme et ne change pas le verdict.

{
  "valid": false,
  "syntax": "cii",
  "profile": "urn:cen.eu:en16931:2017",
  "errors": [
    {
      "rule": "BR-CO-15",
      "message": "[BR-CO-15]-Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).",
      "path": "/Q{urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100}CrossIndustryInvoice[1]"
    }
  ],
  "warnings": []
}

Sans clé

Envoyez la même requête sans en-tête Authorization et elle s'exécute sur le pool anonyme des pages d'outils gratuites : 10 validations par jour et par adresse IP, partagées entre la page du validateur, la page du générateur et les appels API sans clé, remises à zéro à minuit UTC. Seul un verdict livré compte ; un 400, un 422 ou une erreur de notre côté rend le crédit. L'appel qui suit le dernier est un 429 pool_exhausted qui indique l'heure de remise à zéro et la clé gratuite, qui donne 100 validations par mois. Une clé présente mais invalide est un 401, jamais un repli sur le pool.

curl -s -X POST \
  --data-binary @invoice.xml \
  https://api.einvoicekit.com/v1/validate
{
  "error": "pool_exhausted",
  "message": "10 free validations a day per IP address without a key. A free key gives 100 a month: https://einvoicekit.com/get-started?from=api-pool",
  "resetsAt": "2026-09-04T00:00:00.000Z",
  "upgrade": "https://einvoicekit.com/get-started?from=api-pool"
}

Appeler depuis votre langage

curl

curl https://api.einvoicekit.com/v1/validate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/xml" \
  --data-binary @invoice.xml

Python

# pip install requests
import requests

with open("invoice.xml", "rb") as f:
    response = requests.post(
        "https://api.einvoicekit.com/v1/validate",
        headers={
            "Authorization": "Bearer eik_live_...",
            "Content-Type": "application/xml",
        },
        data=f.read(),
    )

result = response.json()
print(result["valid"], result["errors"])

Node.js

// Node.js 18+ (built-in fetch), run as an ES module
import { readFile } from "node:fs/promises";

const response = await fetch("https://api.einvoicekit.com/v1/validate", {
  method: "POST",
  headers: {
    Authorization: "Bearer eik_live_...",
    "Content-Type": "application/xml",
  },
  body: await readFile("invoice.xml"),
});

const result = await response.json();
console.log(result.valid, result.errors);

PHP

<?php
$ch = curl_init("https://api.einvoicekit.com/v1/validate");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer eik_live_...",
        "Content-Type: application/xml",
    ],
    CURLOPT_POSTFIELDS => file_get_contents("invoice.xml"),
]);

$result = json_decode(curl_exec($ch), true);
echo $result["valid"] ? "valid" : "invalid", PHP_EOL;

C#

// .NET 6+ (top-level statements)
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "eik_live_...");

var content = new ByteArrayContent(File.ReadAllBytes("invoice.xml"));
content.Headers.ContentType = new MediaTypeHeaderValue("application/xml");

var response = await http.PostAsync("https://api.einvoicekit.com/v1/validate", content);
using var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine(result.RootElement.GetProperty("valid"));

Java

// Java 11+
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;

public class ValidateInvoice {
    public static void main(String[] args) throws Exception {
        var client = HttpClient.newHttpClient();
        var request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.einvoicekit.com/v1/validate"))
            .header("Authorization", "Bearer eik_live_...")
            .header("Content-Type", "application/xml")
            .POST(HttpRequest.BodyPublishers.ofFile(Path.of("invoice.xml")))
            .build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());
    }
}

Go

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

func main() {
	xml, err := os.ReadFile("invoice.xml")
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost, "https://api.einvoicekit.com/v1/validate", bytes.NewReader(xml))
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer eik_live_...")
	req.Header.Set("Content-Type", "application/xml")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(body))
}

Rust

// cargo add reqwest --features blocking,json
// cargo add serde_json
use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let xml = fs::read("invoice.xml")?;
    let result: serde_json::Value = reqwest::blocking::Client::new()
        .post("https://api.einvoicekit.com/v1/validate")
        .header("Authorization", "Bearer eik_live_...")
        .header("Content-Type", "application/xml")
        .body(xml)
        .send()?
        .json()?;

    println!("{} {}", result["valid"], result["errors"]);
    Ok(())
}

Swift

// Swift 5.7+, macOS 12+ — run as main.swift or 'swift validate.swift'
// (on Linux, add: import FoundationNetworking)
import Foundation

var request = URLRequest(url: URL(string: "https://api.einvoicekit.com/v1/validate")!)
request.httpMethod = "POST"
request.setValue("Bearer eik_live_...", forHTTPHeaderField: "Authorization")
request.setValue("application/xml", forHTTPHeaderField: "Content-Type")
request.httpBody = try Data(contentsOf: URL(fileURLWithPath: "invoice.xml"))

let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONSerialization.jsonObject(with: data) as! [String: Any]
print(result["valid"] ?? "?", result["errors"] ?? [])

Règles françaises (BR-FR)

La France ajoute ses propres règles métier au-dessus d'EN 16931 : le référentiel BR-FR que les plateformes agréées appliquent dans le cadre de la réforme 2026, publié par le FNFE-MPE. L'activation se fait par requête, avec le paramètre target :

curl -s -X POST \
  -H "Authorization: Bearer eik_live_..." \
  --data-binary @invoice.xml \
  "https://api.einvoicekit.com/v1/validate?target=france"

Le paramètre est opt-in car les règles françaises ne peuvent pas être détectées automatiquement : une facture EN 16931 française est structurellement identique à n'importe quelle autre. Un appel avec target=france coûte le même document unique que toute validation.

Documents acceptés : factures EN 16931 en UBL (Invoice et CreditNote) ou CII, et les profils Factur-X BASIC WL, EN 16931 et EXTENDED. Tout autre profil renvoie 422 au lieu de sauter silencieusement l'étape française : un verdict positif signifie toujours que les règles françaises ont réellement tourné.

La réponse renvoie target en écho et liste les règles françaises enfreintes parmi les autres constats. Les messages des règles sont en français, tels que le FNFE-MPE les publie :

{
  "valid": false,
  "syntax": "cii",
  "profile": "urn:cen.eu:en16931:2017",
  "target": "france",
  "errors": [
    {
      "rule": "BR-FR-05_BT-22_PMT",
      "message": "BR-FR-05/BT-22 : La mention relative aux frais de recouvrement (code PMT) est absente. Elle est obligatoire dans les notes (BG-1).",
      "path": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ExchangedDocument[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]"
    }
  ],
  "warnings": []
}

POST /v1/generate

Construit une facture Factur-X à partir d'un simple JSON. Nous calculons nous-mêmes tous les totaux, nous passons le document fini dans le jeu de règles officiel qu'utilise /v1/validate, et nous refusons l'appel plutôt que de vous remettre un fichier qu'une plateforme destinataire rejetterait.

Cette version génère du Factur-X, au profil en16931 ou extended, sous forme de XML CII ou de PDF contenant ce XML. Concrètement, format accepte facturx, output accepte xml ou pdf, profile accepte en16931 (la valeur par défaut) ou extended, et toute autre valeur est refusée par un 400 qui la nomme. language accepte fr, en ou de et détermine la langue des libellés imprimés sur le PDF ; vos propres textes n'y sont jamais touchés : raisons sociales, désignations de lignes, conditions de paiement et notes s'impriment exactement comme vous les avez envoyés. logo est dessiné sur le PDF et ignoré pour xml.

Requête

Envoyez la facture en JSON, via POST. Le schéma est fermé : une clé inconnue est refusée par un 400 accompagné d'un pointeur JSON qui la désigne, au lieu d'être ignorée en silence ; un champ défini par EN 16931 mais non pris en charge dans cette version est refusé par un 400 qui le nomme, au lieu d'être traité comme si vous ne l'aviez jamais envoyé.

curl https://api.einvoicekit.com/v1/generate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/json" \
  --data-binary @invoice.json
{
  "format": "facturx",
  "profile": "en16931",
  "output": "xml",
  "language": "fr",
  "invoice": {
    "number": "F-2026-0042",
    "issueDate": "2026-09-15",
    "typeCode": "380",
    "currency": "EUR",
    "dueDate": "2026-10-15",
    "paymentTerms": "Paiement a 30 jours.",
    "buyerReference": "04011000-1234512345-06",
    "purchaseOrderReference": "PO-889",
    "notes": [{ "text": "Penalites de retard: 3x le taux legal." }],
    "seller": {
      "name": "Atelier Dupont",
      "vatId": "FR12345678901",
      "legalRegistrationId": "12345678900012",
      "legalInformation": "SARL au capital de 10 000 EUR - RCS Paris B 123 456 789",
      "electronicAddress": { "value": "12345678900012", "scheme": "0009" },
      "address": {
        "line1": "3 rue des Lilas",
        "city": "Paris",
        "postCode": "75011",
        "country": "FR"
      },
      "contact": {
        "name": "Marie Dupont",
        "phone": "+33 1 23 45 67 89",
        "email": "compta@atelier-dupont.fr"
      }
    },
    "buyer": {
      "name": "Beispiel GmbH",
      "vatId": "DE123456789",
      "address": {
        "line1": "Hauptstrasse 12",
        "city": "Berlin",
        "postCode": "10115",
        "country": "DE"
      }
    },
    "delivery": { "date": "2026-09-10", "country": "FR" },
    "payment": {
      "meansCode": "58",
      "iban": "FR7630006000011234567890189",
      "bic": "AGRIFRPP",
      "remittanceInformation": "F-2026-0042"
    },
    "allowances": [
      {
        "amount": "100.00",
        "reason": "Remise commerciale",
        "vat": { "category": "S", "rate": "20" }
      }
    ],
    "charges": [
      {
        "percentage": "1.50",
        "baseAmount": "7800.00",
        "reason": "Frais de dossier",
        "vat": { "category": "S", "rate": "20" }
      }
    ],
    "lines": [
      {
        "id": "1",
        "name": "Prestation de developpement",
        "quantity": "12",
        "unit": "HUR",
        "unitPrice": "650.00",
        "vat": { "category": "S", "rate": "20" }
      }
    ]
  }
}

Les montants, quantités, taux et pourcentages sont des chaînes de caractères : "650.00", "12", "5.5". C'est la forme canonique : un nombre JSON est un double IEEE, et un montant d'argent n'en est pas un. Les nombres envoyés sans guillemets restent tolérés en entrée ; ils sont lus sous leur écriture décimale la plus courte, si bien que 650.00 arrive comme 650, et une valeur qui dépasse alors la limite de son champ est refusée par un 400. Les montants acceptent au plus 2 décimales, les quantités et prix unitaires jusqu'à 6, les taux et pourcentages jusqu'à 2.

Vous n'envoyez pas les totaux, vous ne pouvez donc pas vous tromper dessus : nous les calculons tous, avec un arrondi au plus proche où les demis s'éloignent de zéro, une fois par ligne et une fois par taux de TVA. Si vous envoyez malgré tout un objet totals, nous le comparons au nôtre et, en cas d'écart, l'appel est refusé avec un 422 totals_mismatch qui affiche les deux montants. Le chiffre de votre comptabilité reste le vôtre : nous ne l'écrasons jamais en silence.

Réponse

Du JSON par défaut : le document CII en base64, les totaux que nous avons calculés, et le verdict de chaque étape du pipeline. pdfa3 vaut skipped parce qu'avec output à xml aucun PDF n'est produit, il n'y avait donc rien à contrôler ; demandez un PDF et cette étape porte un vrai verdict. Les constats que le jeu de règles classe en avertissement plutôt qu'en erreur sont renvoyés dans un tableau warnings et ne changent pas le verdict. Une clé rounding apparaît à l'intérieur de totals uniquement lorsque votre roundingAmount est différent de zéro.

{
  "format": "facturx",
  "profile": "en16931",
  "xml": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4...",
  "totals": {
    "lineNet": "7800.00",
    "allowances": "100.00",
    "charges": "117.00",
    "taxBasis": "7817.00",
    "vat": "1563.40",
    "grossTotal": "9380.40",
    "prepaid": "0.00",
    "amountDue": "9380.40"
  },
  "validation": {
    "valid": true,
    "stages": { "xsd": "pass", "schematron": "pass", "pdfa3": "skipped" }
  }
}

Récupérer le XML lui-même

Envoyez Accept: application/xml et la réponse est le document CII lui-même, sans enveloppe JSON ni base64 à décoder avec jq et base64 -d. Accept: application/pdf fait la même chose pour le PDF. L'en-tête et le corps doivent s'accorder : demander un format alors que output en désigne un autre est refusé par un 400 output_conflict, car deviner lequel des deux vous vouliez est le plus court chemin vers un fichier envoyé dans le mauvais pipeline.

curl https://api.einvoicekit.com/v1/generate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/xml" \
  -H "Idempotency-Key: F-2026-0042" \
  --data-binary @invoice.json \
  -o invoice.xml

La sortie PDF

Demandez output à pdf et vous obtenez une facture hybride Factur-X : un PDF que l'on lit, avec la même facture en XML CII attachée à l'intérieur, sous le nom factur-x.xml que la spécification impose. C'est un PDF/A-3B, le format d'archivage exigé par la norme, et chaque fichier est contrôlé avant d'être facturé : l'étape pdfa3 de la réponse porte ce verdict. En cas d'échec, l'appel deviendrait un 500 qui nous incombe et ne vous coûterait rien.

Le corps de la requête est celui que vous avez déjà. Seul output change, et le XML contenu dans le PDF est identique, octet pour octet, à celui que la même requête renvoie avec output à xml : rien ne vous oblige donc à choisir entre les deux intégrations.

curl https://api.einvoicekit.com/v1/generate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/pdf" \
  -H "Idempotency-Key: F-2026-0042" \
  --data-binary @invoice.json \
  -o invoice.pdf
{
  "format": "facturx",
  "profile": "en16931",
  "xml": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4...",
  "pdf": "JVBERi0xLjcKJYGBgYEKCjEgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi...",
  "totals": {
    "lineNet": "7800.00",
    "allowances": "100.00",
    "charges": "117.00",
    "taxBasis": "7817.00",
    "vat": "1563.40",
    "grossTotal": "9380.40",
    "prepaid": "0.00",
    "amountDue": "9380.40"
  },
  "validation": {
    "valid": true,
    "stages": { "xsd": "pass", "schematron": "pass", "pdfa3": "pass" }
  }
}

Un texte que la facture ne peut pas dessiner

Un PDF doit dessiner chaque caractère avec une police embarquée, et aucune police ne couvre tout Unicode. Plutôt que d'imprimer un rectangle vide sur une facture que vous êtes sur le point d'envoyer, nous refusons : un 422 dont les issues portent la règle unrenderable_text, un pointeur vers le champ exact et les caractères en cause. Le contournement figure dans le message même, car output à xml n'a aucune contrainte de police et accepte tout Unicode. Seule la sortie PDF fait l'objet de ce contrôle.

{
  "error": "invalid_invoice",
  "message": "this invoice does not satisfy the rules it has to satisfy; see issues.",
  "issues": [
    {
      "rule": "unrenderable_text",
      "field": "/invoice/lines/0/name",
      "message": "the invoice font cannot draw \"😀\". Use output \"xml\", which has no font constraint."
    }
  ]
}

Tous les champs

Les noms de champs sont en anglais courant, et le terme métier EN 16931 correspondant figure à côté de chacun : la norme reste consultable sans vous être imposée. Ce tableau est généré à partir des règles mêmes que l'endpoint applique ; il ne peut donc pas s'en écarter.

Toujours signifie que l'appel est refusé si le champ manque. Conditionnel signifie qu'une règle de gestion peut le rendre obligatoire : la catégorie de TVA E exige un motif d'exonération, un groupe de paiement exige un code de moyen de paiement, un virement exige un IBAN, la catégorie K exige une date ou une période de livraison ainsi qu'un pays de livraison, et toute facture exige une date d'échéance ou des conditions de paiement. Facultatif signifie qu'aucune règle ne l'exige jamais. Pour une ligne qui décrit une entrée de tableau (un chemin se terminant par []), la colonne reste vide : c'est le tableau lui-même, une ligne au-dessus, qui est obligatoire ou non.

unit (BT-130) est obligatoire sur chaque ligne et n'a pas de valeur par défaut. La règle BR-23 rend le code d'unité obligatoire et bloquant : une ligne sans unité produirait un document que notre propre jeu de règles rejetterait. Utilisez les codes de la recommandation 20 de l'UN/ECE, avec l'extension de la recommandation 21 : HUR pour une heure, DAY, KGM, MTR, ou C62 pour une unité dénombrable simple.

Les motifs d'exonération se renseignent sur l'objet vat de chaque ligne, remise ou frais, mais ce ne sont pas des termes de ligne : BT-120 et BT-121 appartiennent à la ventilation de TVA du document (BG-23), et nous les y regroupons pour vous. Pour les catégories AE, K, G et O, nous insérons la formulation standard si vous n'en fournissez aucune. La catégorie E n'a pas de formulation standard : le motif y est donc obligatoire, et son absence entraîne un 422 qui pointe la ligne exacte.

La requête

Champ BT / BG Obligatoire Type et limites
format facultatif parmi : facturx
profile facultatif parmi : en16931, extended
output facultatif parmi : xml, pdf
language facultatif parmi : fr, en, de
invoice toujours object
totals facultatif object
logo facultatif string, 2 000 000 caractères max

En-tête de facture

Champ BT / BG Obligatoire Type et limites
number BT-1 toujours string, 100 caractères max
issueDate BT-2 toujours string, 10 caractères max
typeCode BT-3 toujours parmi : 380, 381, 384, 389
currency BT-5 toujours string, 3 caractères max
dueDate BT-9 conditionnel string, 10 caractères max
paymentTerms BT-20 conditionnel string, 2 000 caractères max
buyerReference BT-10 facultatif string, 200 caractères max
purchaseOrderReference BT-13 facultatif string, 200 caractères max
notes BG-1 facultatif array
notes[] BG-1 object
notes[]/text BT-22 toujours string, 1 000 caractères max
precedingInvoices facultatif array
precedingInvoices[] object
precedingInvoices[]/number BT-25 toujours string, 100 caractères max
precedingInvoices[]/issueDate BT-26 facultatif string, 10 caractères max
prepaidAmount BT-113 facultatif decimal, 2 décimales max
roundingAmount BT-114 facultatif decimal, 2 décimales max

Vendeur (BG-4)

Champ BT / BG Obligatoire Type et limites
/invoice/seller toujours object
name BT-27 toujours string, 200 caractères max
vatId BT-31 conditionnel string, 50 caractères max
taxRegistrationId BT-32 conditionnel string, 50 caractères max
legalRegistrationId BT-30 conditionnel string, 50 caractères max
legalInformation BT-33 facultatif string, 1 000 caractères max
electronicAddress facultatif object
electronicAddress/value BT-34 facultatif string, 200 caractères max
electronicAddress/scheme BT-34-1 facultatif string, 10 caractères max
address toujours object
address/line1 BT-35 facultatif string, 200 caractères max
address/line2 BT-36 facultatif string, 200 caractères max
address/city BT-37 facultatif string, 100 caractères max
address/postCode BT-38 facultatif string, 20 caractères max
address/country BT-40 toujours string, 2 caractères max
contact facultatif object
contact/name BT-41 facultatif string, 200 caractères max
contact/phone BT-42 facultatif string, 50 caractères max
contact/email BT-43 facultatif string, 200 caractères max

Acheteur

Champ BT / BG Obligatoire Type et limites
/invoice/buyer toujours object
name BT-44 toujours string, 200 caractères max
vatId BT-48 conditionnel string, 50 caractères max
taxRegistrationId facultatif string, 50 caractères max
legalRegistrationId BT-47 conditionnel string, 50 caractères max
legalInformation facultatif string, 1 000 caractères max
electronicAddress facultatif object
electronicAddress/value BT-49 facultatif string, 200 caractères max
electronicAddress/scheme BT-49-1 facultatif string, 10 caractères max
address toujours object
address/line1 BT-50 facultatif string, 200 caractères max
address/line2 BT-51 facultatif string, 200 caractères max
address/city BT-52 facultatif string, 100 caractères max
address/postCode BT-53 facultatif string, 20 caractères max
address/country BT-55 toujours string, 2 caractères max
contact facultatif object
contact/name BT-56 facultatif string, 200 caractères max
contact/phone BT-57 facultatif string, 50 caractères max
contact/email BT-58 facultatif string, 200 caractères max

Livraison

Champ BT / BG Obligatoire Type et limites
/invoice/delivery conditionnel object
date BT-72 conditionnel string, 10 caractères max
periodStart BT-73 conditionnel string, 10 caractères max
periodEnd BT-74 conditionnel string, 10 caractères max
country BT-80 conditionnel string, 2 caractères max

Instructions de paiement (BG-16)

Champ BT / BG Obligatoire Type et limites
/invoice/payment facultatif object
meansCode BT-81 conditionnel string, 10 caractères max
iban BT-84 conditionnel string, 34 caractères max
bic BT-86 facultatif string, 11 caractères max
accountName BT-85 facultatif string, 200 caractères max
remittanceInformation BT-83 facultatif string, 200 caractères max
mandateReference BT-89 facultatif string, 70 caractères max
creditorId BT-90 facultatif string, 35 caractères max

Lignes de facture (BG-25)

Champ BT / BG Obligatoire Type et limites
/invoice/lines toujours array
/invoice/lines[] object
id BT-126 toujours string, 50 caractères max
name BT-153 toujours string, 200 caractères max
description BT-154 facultatif string, 1 000 caractères max
quantity BT-129 toujours decimal, 6 décimales max
unit BT-130 toujours string, 10 caractères max
unitPrice BT-146 toujours decimal, 6 décimales max
priceBaseQuantity BT-149 facultatif decimal, 6 décimales max
vat toujours object
vat/category BT-151 toujours string, 2 caractères max
vat/rate BT-152 conditionnel decimal, 2 décimales max
vat/exemptionReason BT-120 conditionnel string, 200 caractères max
vat/exemptionReasonCode BT-121 facultatif string, 30 caractères max

Remises au niveau document (BG-20)

Champ BT / BG Obligatoire Type et limites
/invoice/allowances facultatif array
/invoice/allowances[] object
amount BT-92 conditionnel decimal, 2 décimales max
percentage BT-94 conditionnel decimal, 2 décimales max
baseAmount BT-93 conditionnel decimal, 2 décimales max
reason BT-97 conditionnel string, 200 caractères max
reasonCode BT-98 conditionnel string, 10 caractères max
vat toujours object
vat/category BT-95 toujours string, 2 caractères max
vat/rate BT-96 conditionnel decimal, 2 décimales max
vat/exemptionReason BT-120 conditionnel string, 200 caractères max
vat/exemptionReasonCode BT-121 facultatif string, 30 caractères max

Frais au niveau document (BG-21)

Champ BT / BG Obligatoire Type et limites
/invoice/charges facultatif array
/invoice/charges[] object
amount BT-99 conditionnel decimal, 2 décimales max
percentage BT-101 conditionnel decimal, 2 décimales max
baseAmount BT-100 conditionnel decimal, 2 décimales max
reason BT-104 conditionnel string, 200 caractères max
reasonCode BT-105 conditionnel string, 10 caractères max
vat toujours object
vat/category BT-102 toujours string, 2 caractères max
vat/rate BT-103 conditionnel decimal, 2 décimales max
vat/exemptionReason BT-120 conditionnel string, 200 caractères max
vat/exemptionReasonCode BT-121 facultatif string, 30 caractères max

Remises de ligne (BG-27)

Champ BT / BG Obligatoire Type et limites
/invoice/lines[]/allowances facultatif array
/invoice/lines[]/allowances[] object
amount BT-136 conditionnel decimal, 2 décimales max
percentage BT-138 conditionnel decimal, 2 décimales max
baseAmount BT-137 conditionnel decimal, 2 décimales max
reason BT-139 conditionnel string, 200 caractères max
reasonCode BT-140 conditionnel string, 10 caractères max

Frais de ligne (BG-28)

Champ BT / BG Obligatoire Type et limites
/invoice/lines[]/charges facultatif array
/invoice/lines[]/charges[] object
amount BT-141 conditionnel decimal, 2 décimales max
percentage BT-143 conditionnel decimal, 2 décimales max
baseAmount BT-142 conditionnel decimal, 2 décimales max
reason BT-144 conditionnel string, 200 caractères max
reasonCode BT-145 conditionnel string, 10 caractères max

Totaux, si vous les envoyez pour vérification

Champ BT / BG Obligatoire Type et limites
lineNet BT-106 facultatif decimal, 2 décimales max
allowances BT-107 facultatif decimal, 2 décimales max
charges BT-108 facultatif decimal, 2 décimales max
taxBasis BT-109 facultatif decimal, 2 décimales max
vat BT-110 facultatif decimal, 2 décimales max
grossTotal BT-112 facultatif decimal, 2 décimales max
prepaid BT-113 facultatif decimal, 2 décimales max
amountDue BT-115 facultatif decimal, 2 décimales max

Limites

Corps de requête limité à 2 Mio, 413 au-delà. 500 lignes par facture au maximum, 400 au-delà, ce qui représente neuf pages de PDF. Chaque champ texte a la longueur maximale indiquée dans le tableau ci-dessus. Le logo doit être une data URI PNG ou JPEG d'au plus 2000 par 2000 pixels, reconnue à ses octets magiques et non au type MIME qu'elle déclare.

Ce que cela coûte

Vous payez l'appel, pas ce qui en sort. Demander un PDF coûte exactement ce que coûte demander le XML : un PDF Factur-X est une facture qui embarque son propre XML, c'est la définition même de la norme, et non deux livrables. Un compte gratuit dispose de 20 factures générées par mois, dans une enveloppe qui lui est propre, distincte de ses 100 validations : un mois de validations n'entame donc jamais vos générations, ni l'inverse. Un compte pro met tout en commun : 1 000 crédits par mois, où une validation coûte un crédit et une facture générée deux, à répartir comme vous le souhaitez. Cela fait 1 000 factures validées, ou 500 générées, ou n'importe quel dosage entre les deux.

Les appels refusés ne sont pas facturés, pas plus que ce qui relève de notre responsabilité. L'appel n'est décompté qu'une fois que le document a passé le XSD, le jeu de règles complet et, pour un PDF, le contrôle PDF/A, et qu'il est en route vers vous. Un appel qui ne peut pas payer son coût entier est refusé avant tout ce travail et laisse votre enveloppe intacte : en pro, s'il ne vous reste qu'un crédit, une validation passe encore et une génération est refusée par un <code>429</code>, plutôt que de facturer à moitié une génération que vos crédits ne couvrent pas.

Réessayer sans risque

Envoyez un en-tête Idempotency-Key de 1 à 255 caractères ; il est propre à votre clé d'API et conservé 24 heures. Sa garantie est précise et limitée : la même clé avec le même corps de requête n'est jamais facturée deux fois. Les réponses ne sont pas stockées : un nouvel essai rejoue donc tout le pipeline au lieu de resservir une réponse enregistrée. Vous obtenez malgré tout le même document, car le XML est construit de façon déterministe à partir de votre requête : un même corps produit toujours les mêmes octets.

La même clé avec un corps différent est refusée par un 409 idempotency_key_reuse, jamais remplacée en silence : une clé est un engagement portant sur une seule requête. Un 422 libère la clé : la facture corrigée peut donc être renvoyée sous la même clé.

Un rejeu est gratuit pour vous, mais pas sans coût pour nous : il est donc plafonné à cinq rejeux d'un document déjà facturé, après quoi l'appel est refusé avec un 429 replay_limit_exceeded. Ce plafond couvre les formes que prend un vrai nouvel essai (connexion interrompue, délai dépassé côté client, file de messages en livraison at-least-once) et arrête la boucle qui transformerait sinon un document payé en une journée de calcul gratuit.

Erreurs

De vrais codes de statut HTTP, toujours : jamais un 200 qui cache un échec. Chaque refus est une réponse JSON portant un code error en snake_case, sur lequel votre code peut s'appuyer, et un message destiné aux humains, sur lequel il ne doit pas s'appuyer.

Statut error Signification
400 unknown_parameter Cet endpoint n'accepte aucun paramètre de requête. Le paramètre en cause est nommé dans la réponse.
400 invalid_json Le corps de la requête n'est pas du JSON valide.
400 invalid_request Le corps n'a pas la forme attendue par cet endpoint : clé inconnue, champ non pris en charge dans cette version, valeur hors énumération, trop de décimales, chaîne trop longue, plus de 500 lignes, ou une remise qui donne à la fois un montant et un pourcentage.
400 invalid_idempotency_key L'en-tête Idempotency-Key est présent mais ne fait pas de 1 à 255 caractères.
400 output_conflict L'en-tête Accept et le champ output désignent deux représentations différentes. Demandez celle que le corps annonce, ou omettez l'en-tête.
401 invalid_api_key Clé d'API absente ou inconnue.
405 method_not_allowed Seul POST existe.
409 idempotency_key_reuse Cette Idempotency-Key a déjà été utilisée avec un corps de requête différent.
413 payload_too_large Le corps de la requête dépasse 2 Mio.
422 invalid_invoice La facture a bien été lue, mais elle enfreint une règle qu'elle doit respecter : soit l'une des nôtres, appliquée avant le pipeline, soit une règle EN 16931 relevée par le jeu de règles officiel. Le tableau issues indique laquelle.
422 totals_mismatch Les totaux que vous avez envoyés ne correspondent pas à ceux que nous avons calculés. Les deux sont renvoyés dans la réponse.
429 quota_exceeded Enveloppe mensuelle épuisée. La réponse contient un champ upgrade avec l'URL adaptée à votre compte.
429 replay_limit_exceeded Cette Idempotency-Key a déjà rejoué cinq fois son document facturé.
500 generation_failed L'erreur est de notre côté, pas du vôtre : notre propre XML a échoué à notre propre validation XSD, le validateur était injoignable, ou une règle arithmétique a signalé un calcul que nous avons pourtant fait nous-mêmes. Rien n'est facturé et nous sommes alertés.
504 generation_timeout L'étape de validation n'a pas répondu dans les 20 secondes. Rien n'est facturé, nous sommes alertés, et un nouvel essai aboutit normalement.

Lire le tableau issues

Les refus qui portent sur vos données contiennent un tableau issues. rule nomme la règle enfreinte : l'un de nos codes pour un problème de schéma, ou l'identifiant de règle EN 16931 lorsque c'est le jeu de règles officiel qui l'a relevée. field est un pointeur JSON dans le corps que vous avez envoyé ; message reprend le texte de la règle. D'autres API renvoient un XPath pointant dans un XML que vous n'avez jamais écrit ; ici, nous maîtrisons les deux bouts de la correspondance, et le pointeur désigne votre propre JSON. Quand un constat ne se rattache à aucun champ précis, field vaut null, avec le véritable identifiant de règle à côté : jamais un pointeur inventé.

{
  "error": "invalid_invoice",
  "message": "this invoice does not satisfy the rules it has to satisfy; see issues.",
  "issues": [
    {
      "rule": "BR-CL-23",
      "field": "/invoice/lines/0/unit",
      "message": "[BR-CL-23]-Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension"
    }
  ]
}

Codes possibles dans rule pour un 400 : unknown_field, unsupported_field, unsupported_value, too_many_decimals, too_long, invalid_value, too_many_lines, invalid_amount_shape. Pour un 422 : missing_field, missing_exemption_reason, exemption_reason_not_allowed, rate_not_allowed, invalid_vat_rate, invalid_vat_category_mix, missing_reason, missing_seller_identification, missing_buyer_identification, missing_due_date_or_payment_terms, negative_unit_price, invalid_price_base_quantity, invalid_logo, unrenderable_text, conflicting_exemption_reasons, ainsi que les identifiants de règles EN 16931 eux-mêmes.

Le premier appel après une période d'inactivité

Le moteur de validation se met en veille après dix minutes sans appel. Le premier appel de génération qui suit peut passer l'intégralité du budget de 20 secondes à attendre son réveil et se terminer en 504 generation_timeout. Réessayez : le second appel aboutit. À chaud, l'appel médian prend environ 230 ms en XML et environ 750 ms en PDF. Une facture de 500 lignes est le cas le plus lent : quelques secondes en PDF. L'appel échoué n'est pas facturé et nous sommes alertés à chaque occurrence. Nous préférons vous le dire ici plutôt que de vous le laisser découvrir en production.

Un avertissement connu

Au profil extended, un document sans données de livraison revient avec l'avertissement PEPPOL-EN16931-R008, « Document MUST not contain empty elements ». Le XSD CII rend l'élément de livraison obligatoire (minOccurs 1) alors que le jeu de règles interdit les éléments vides : le schéma et le jeu de règles se contredisent, et c'est le schéma qui l'emporte. Ce n'est qu'un avertissement, le verdict reste valide, et il n'y a rien à corriger de votre côté.

Erreurs & quotas

Toute réponse non-200 est du JSON avec un champ error. Les statuts existants :

Statut Signification
400 Corps vide, multipart sans champ file, valeur de target inconnue, ou document qu'aucun parseur ne reconnaît.
401 Clé d'API absente ou inconnue.
405 Seul POST existe.
413 Document au-delà de 5 Mo sur /v1/validate. /v1/generate plafonne plutôt le corps de requête à 2 Mio.
422 Le document se parse mais n'est pas une syntaxe de facture électronique prise en charge.
429 Quota mensuel épuisé. La réponse contient un champ upgrade avec l'URL adaptée à votre compte : les tarifs pour un compte gratuit, le formulaire volume pour un compte pro.
502 Le moteur de validation est brièvement indisponible. Réessayez avec backoff.

Les enveloppes sont mensuelles, par compte, toutes clés confondues. Un compte gratuit dispose de 100 validations plus 20 factures générées à part ; un compte pro dispose d'une enveloppe unique de 1 000 crédits couvrant les deux, où une validation coûte un crédit et une facture générée deux. La fenêtre est ancrée à la date d'abonnement (ou d'inscription en gratuit), pas au mois calendaire. Seules les requêtes que nous avons acceptées et servies sont comptées : un appel refusé, comme tout ce qui relève de notre faute, n'est jamais facturé.

À venir

L'endpoint d'extraction, qui transforme n'importe quelle facture électronique en JSON normalisé, est encore en développement. Sa forme prévue est dans la section API de la page d'accueil. Tout le reste de cette page est appelable aujourd'hui.