Développeur
Valider une facture avec curl
Un terminal, curl et jq suffisent pour contrôler une facture électronique avant de
l'envoyer. Voici l'appel en une ligne, puis un script shell complet, prêt pour une intégration continue :
envoi du fichier, lecture du verdict, et un code de sortie pour chaque cas.
Avant de commencer
- Une clé API, créée sur votre page de clés API. Elle ne s'affiche qu'une fois : copiez-la dès sa création.
-
curl, présent sur la plupart des systèmes, etjq, qui lit le JSON de la réponse. Sur Debian ou Ubuntu :apt install curl jq; sur Alpine :apk add curl jq; sur macOS :brew install jq. - Une facture : un PDF Factur-X, ou un XML CII ou UBL.
Rangez la clé dans une variable d'environnement, jamais dans le script lui-même ni dans
l'historique de votre shell. Dans bash ou zsh, la commande
read -s la lit sans l'afficher :
read -rs VALIDE_FACTURE_API_KEY && export VALIDE_FACTURE_API_KEY
Dans une intégration continue, déclarez-la comme variable masquée du projet (GitLab) ou comme secret du dépôt (GitHub) : elle arrive dans l'environnement du job sous ce même nom.
L'appel en une ligne
Pour voir la réponse sans rien écrire d'autre, depuis le répertoire où se trouve
facture.pdf :
curl -sS -H "Authorization: Bearer $VALIDE_FACTURE_API_KEY" -F "file=@facture.pdf" https://valide-facture.fr/api/validate | jq .verdict
Elle affiche "CONFORME", "NON_CONFORME" ou "NON_EVALUABLE".
Retirez .verdict pour lire le rapport entier. Sur un refus, elle affiche
null : la réponse n'a pas de membre verdict, c'est un corps
d'erreur. Le script ci-dessous fait la différence.
Le script complet
Enregistrez-le sous le nom valider.sh. Il n'utilise que le shell POSIX, il tourne donc
aussi bien sous sh, dash, bash ou dans une image
Alpine.
#!/bin/sh
# Valide une facture : sh valider.sh facture.pdf
# La clé API est lue dans la variable d'environnement VALIDE_FACTURE_API_KEY.
# Sortie : 0 conforme, 1 autre verdict, 3 refus ou erreur technique.
set -u
[ "$#" -eq 1 ] || { echo "Usage : VALIDE_FACTURE_API_KEY=... sh valider.sh <facture>" >&2; exit 3; }
command -v jq >/dev/null 2>&1 || { echo "jq est introuvable : installez-le." >&2; exit 3; }
fichier=$1
[ -n "${VALIDE_FACTURE_API_KEY:-}" ] || { echo "VALIDE_FACTURE_API_KEY est vide." >&2; exit 3; }
[ -r "$fichier" ] || { echo "Fichier illisible : $fichier" >&2; exit 3; }
corps=$(mktemp) && entetes=$(mktemp) || exit 3
trap 'rm -f "$corps" "$entetes"' EXIT
statut=$(curl --silent --show-error --max-time 35 \
--header "Authorization: Bearer $VALIDE_FACTURE_API_KEY" \
--form "file=@$fichier" \
--dump-header "$entetes" --output "$corps" --write-out '%{http_code}' \
https://valide-facture.fr/api/validate) || {
echo "Pas de réponse du service : réessayez plus tard." >&2
exit 3
}
if [ "$statut" = 200 ]; then
verdict=$(jq -r '.verdict' "$corps")
echo "Verdict : $verdict"
jq -r '.findings[] | "- [\(.severity)] \(.code) : \(.messageFr)"' "$corps"
[ "$verdict" = CONFORME ] && exit 0
exit 1
fi
# Un refus du service est en application/problem+json. Un refus en amont (proxy, limite
# de débit du frontal) peut arriver en HTML : on ne lit pas son corps.
if ! grep -qi '^content-type: *application/problem+json' "$entetes"; then
echo "Refus HTTP $statut, sans détail lisible : réessayez dans quelques instants." >&2
exit 3
fi
type=$(jq -r '.type' "$corps")
echo "Refus HTTP $statut : $(jq -r '.detail' "$corps")" >&2
case "$type" in
https://valide-facture.fr/problems/rate-limit-exceeded)
attente=$(grep -i '^retry-after:' "$entetes" | tr -dc '0-9')
echo "Trop de requêtes : réessayez dans $attente seconde(s)." >&2
;;
https://valide-facture.fr/problems/quota-exceeded)
echo "Quota de la période atteint : réessayer ne servira à rien avant la période suivante." >&2
;;
*)
echo "Type : $type" >&2
;;
esac
exit 3
Lancez-le sur une facture :
sh valider.sh facture.pdf
L'envoi du fichier : -F, et rien d'autre
L'API attend un envoi multipart/form-data, avec le fichier dans le champ
file. C'est exactement ce que produit --form "file=@facture.pdf" :
le @ dit à curl de joindre le contenu du fichier, et curl pose lui-même
l'en-tête Content-Type, avec la frontière qui sépare les parties de l'envoi.
-d et --data-binary ne conviennent pas : ils envoient le fichier
brut, sans champ, et l'API répond par un 400. Un autre nom de champ que
file donne le même refus. Le type du document est reconnu sur ses octets : inutile
d'ajouter ;type=application/pdf derrière le nom du fichier.
Le code HTTP, l'en-tête et le corps, lus séparément
Le script demande à curl trois choses distinctes : le code HTTP sur la sortie standard
(--write-out '%{http_code}'), les en-têtes dans un fichier (--dump-header)
et le corps dans un autre (--output). Chaque branche lit ce dont elle a besoin.
-
Un
200porte toujours un rapport, y compris pour une facture non conforme : elle a été jugée. Le verdict se lit dans le membreverdict, pas dans le code HTTP. -
Tout autre code est un refus : le service n'a pas pu juger. Le script vérifie
d'abord l'en-tête
Content-Type, puis branche sur le membretypedu corps, jamais sur le seul code : deux refus de sens opposés partagent le429.
N'utilisez pas --fail : sur un code d'erreur, curl jette alors le
corps de la réponse, c'est-à-dire précisément le type et le detail qui disent
quoi faire. --max-time 35 borne l'appel entier, envoi compris, à la durée que recommande
la documentation de l'API ; au-delà, curl sort en erreur et le script rend 3.
Les deux 429, et le troisième
-
rate-limit-exceeded: la clé envoie trop de requêtes rapprochées. L'en-têteRetry-Afterdonne le nombre de secondes à attendre. Le script l'affiche et s'arrête ; c'est à votre chaîne de décider de relancer. -
quota-exceeded: les validations de la période sont consommées. Il n'y a pas deRetry-After, et relancer ne sert à rien : changez d'abonnement ou attendez la période suivante. -
Un
429sansapplication/problem+json: c'est la limite de débit du serveur frontal, qui répond par une page HTML.jqéchouerait sur ce corps ; le script s'arrête avant de le lire.
Si vous relancez dans une boucle, bornez le nombre d'essais et attendez au moins la valeur de
Retry-After. Une boucle sans borne sur un refus de quota ne fait que remplir vos
journaux.
Dans une intégration continue
Les codes de sortie sont faits pour une étape de pipeline : 0 quand la facture est
CONFORME, 1 pour tout autre verdict, 3 pour un refus ou une
erreur technique. Le job échoue donc sur une facture non conforme, et vous distinguez dans le
journal un document à corriger d'un service à rappeler plus tard. Le jeu de factures d'essai se
contrôle fichier par fichier, avec une boucle for qui garde le pire code.
Chaque fichier validé consomme une unité de quota, à chaque exécution du pipeline. Un refus d'entrée (fichier vide, trop volumineux, ni PDF ni XML) et une indisponibilité du service n'en consomment pas. Gardez un petit jeu de factures représentatives plutôt que de contrôler tout un historique à chaque commit.
Les erreurs fréquentes avec curl
-
Le
@oublié.-F "file=facture.pdf"envoie le texte « facture.pdf » dans un champ ordinaire, au lieu du fichier : l'API répond par un400, faute d'avoir reçu un fichier. -
Un chemin avec des espaces. Gardez les guillemets autour de
"file=@$fichier": sans eux, le shell coupe le chemin en plusieurs arguments. Un nom de fichier qui contient une virgule ou un point-virgule est mal lu par-F: renommez-le ou copiez-le sous un nom simple. -
Un
Content-Typeforcé.-Fconstruit lui-même l'en-tête multipart, frontière comprise. Un-H "Content-Type: application/pdf"le remplace : le service ne retrouve plus le fichier dans l'envoi, et répond par un400. -
Une clé hors de l'environnement. Si
VALIDE_FACTURE_API_KEYn'est pas exportée, curl envoieBearersuivi de rien, et la réponse est un401. Dans un script lancé parsh, une variable définie sansexportn'arrive pas. -
Une sortie
jqnon brute. Sans-r,jqrend"CONFORME"avec ses guillemets, et la comparaison[ "$verdict" = CONFORME ]échoue toujours.
Aller plus loin
La liste complète des refus et ce qu'il faut faire pour chacun est sur la page des guides du développeur. Chaque champ du rapport est décrit dans la documentation de l'API, et chaque code de règle a sa page parmi les codes d'erreur du validateur. Pour voir un rapport sans rien installer, déposez une facture dans le validateur.
Portée de cette page
Ce script est un exemple à adapter, pas un outil maintenu : il envoie une facture et lit la réponse, sans rien relancer de lui-même. Le verdict porte sur la conformité technique du document au format et aux règles appliquées par le validateur ; il ne présume pas de l'acceptation de la facture par la plateforme de votre destinataire.
Développeur