Le parcours en un coup d'œil
Un document Henrri suit toujours la même chaîne. Les étapes en pointillés sont optionnelles.
Le principe à retenir : tant qu'un document n'est pas finalisé, il est un brouillon. Il n'a pas de numéro définitif, il ne compte pas en comptabilité, et vous pouvez le modifier ou le supprimer librement. La finalisation est le seul geste qui engage, et c'est aussi le seul qui consomme un crédit. C'est l'endroit idéal pour insérer un contrôle humain, une validation Telegram ou une revue, entre la préparation automatique et l'émission.
Créer un client, un document, ses lignes, les relire, les corriger : aucun crédit. Vous construisez et vérifiez autant que vous voulez sans rien dépenser. Voir ce que ça coûte.
0.S'authentifier
Un jeton, valable 10 minutes, à obtenir avant tout appel.
POST https://api-sandbox.henrri.io/v1/users/authenticate (test)
POST https://api.henrri.io/v1/users/authenticate (production)
curl -X POST "https://api-sandbox.henrri.io/v1/users/authenticate" \
-H "Content-Type: application/json" \
-d '{"clientId":"VOTRE_CLIENT_ID","clientSecret":"VOTRE_CLIENT_SECRET"}'
La réponse contient votre jeton :
{
"access_token": "eyJhbGci...",
"expires_in": 600,
"token_type": "Bearer",
"refresh_token": "C5B217...",
"scope": "Customer_Read Customer_Write Document_Read ..."
}
Tous les appels suivants portent l'en-tête Authorization: Bearer <access_token>. Passé 10 minutes, l'API répond 401 : réauthentifiez-vous, ou utilisez POST /v1/users/refresh-token avec le refresh_token.
1. L'en-tête Content-Type est obligatoire : sans lui, réponse 415, même avec un corps correct.
2. Le nom des champs dépend du format. En JSON, clientId / clientSecret. En x-www-form-urlencoded, client_id / client_secret. Ne les mélangez pas.
3. La documentation est sur une adresse en /scalar, mais les appels ne doivent jamais contenir /scalar : sinon 404.
1.Le client
Un document se rattache à un client. On le crée, ou on retrouve un client existant.
curl -X POST "https://api-sandbox.henrri.io/v1/customers" \
-H "Authorization: Bearer VOTRE_TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "DEMO CLIENT SARL",
"type": "professional",
"accountingNumber": "CRM-0042",
"contacts": [ { "lastName": "Martin", "firstName": "Claire", "email": "claire@demo.fr" } ]
}'
| Champ | Règle |
|---|---|
name | obligatoire |
type | obligatoire : la chaîne professional ou individual (pas un identifiant numérique) |
contacts | obligatoire : 1 à 4 contacts pour un professionnel, exactement 1 pour un particulier. Chaque contact exige au moins lastName |
accountingNumber | facultatif, mais précieux : voir ci-dessous |
accountingNumber
Ce champ peut porter votre identifiant client (celui de votre CRM, de votre agenda, de votre tableur), et il est indexé par la recherche. Vous retrouvez alors une fiche de façon fiable, sans rapprochement approximatif sur le nom :
GET /v1/customers?search=CRM-0042
Le contact ne se supprime pas après coup (l'API refuse de laisser un client sans contact), et le lastName s'imprime sur les documents : si vous n'avez pas le nom de la personne, mettez-y la raison sociale, jamais une valeur de remplissage.
2.Le document brouillon
Devis, facture, avoir : c'est le même appel, seul le type change.
curl -X POST "https://api-sandbox.henrri.io/v1/documents" \
-H "Authorization: Bearer VOTRE_TOKEN" -H "Content-Type: application/json" \
-d '{ "documentTypeId": 4, "customerId": 5001, "title": "Prestation de septembre" }'
Les types les plus courants :
documentTypeId | Document | Coût à la finalisation |
|---|---|---|
| 4 | Devis | 2 crédits |
| 1 | Facture | 8 crédits |
| 2 | Avoir | 8 crédits |
| 3 | Bon de livraison | gratuit |
Le document naît en brouillon, avec un numéro provisoire (par exemple I-26-09-2). La réponse vous renvoie son id : c'est lui que consomment toutes les étapes suivantes. La liste complète des types s'obtient avec GET /v1/documenttypes.
3.Les lignes
Une ligne par appel. Le corps attend l'objet à plat, avec typeId à la racine.
Une ligne d'article ou de prestation (typeId: 3)
Deux façons de faire, selon que la prestation existe déjà à votre catalogue ou non.
Si vous avez un catalogue de prestations, référencez l'article par son itemId et imposez le prix, la description et la TVA de votre côté :
POST /v1/documents/{id}/lines
{
"typeId": 3,
"itemId": 1042,
"description": "Forfait accompagnement - septembre",
"quantity": 2,
"sellingPriceWithoutTax": 150,
"vatPercent": 20
}
Quand une ligne référence un itemId mais porte aussi son propre prix, sa description et sa TVA, ce sont les valeurs de la ligne qui sont retenues, pas celles de l'article. Vérifié jusque sur le PDF final. Vous gardez donc un catalogue de formules stable et un prix dynamique à chaque devis.
Si vous ne voulez rien créer à l'avance, passez un objet item en ligne. Il doit porter une itemCategoryId (une catégorie de votre compte, via GET /v1/itemcategories : 1 = Produits, 2 = Services, 3 = Mixte par défaut) :
{
"typeId": 3, "description": "Nettoyage medium",
"quantity": 1, "sellingPriceWithoutTax": 73.33, "vatPercent": 20,
"item": { "description": "Nettoyage medium", "sellingPriceWithoutTax": 73.33,
"vatPercent": 20, "itemCategoryId": 2 }
}
À savoir : sellingPriceWithoutTax est un prix hors taxes. Si votre grille est en TTC, divisez par 1 + taux avant de l'envoyer. Et l'objet item inline crée un article au catalogue à chaque appel : pratique pour démarrer, mais le catalogue gonfle. Pour un usage répété, préférez la première méthode.
Les autres types de ligne
typeId | Ligne | Statut |
|---|---|---|
| 3 | Article / prestation (porte les montants) | OK |
| 6 | Texte (commentaire libre, sans montant) | OK |
| 5 | Sous-total | OK |
| 4 | Total | OK |
| 11 | Interligne | OK |
| 13 | Trait de séparation | OK |
| 1 | Titre | indisponible |
| 2 | Sous-titre | indisponible |
Les lignes typeId: 1 (Titre) et typeId: 2 (Sous-titre) sont refusées avec un message trompeur : 400 "The specific line you are trying to operate on does not exist or has been removed". En attendant, pour intituler une section, utilisez une ligne de texte (typeId: 6). Une ligne de texte accepte du HTML simple (<strong>, <em>) pour la mise en forme.
4.Relire et contrôler
Avant de finaliser, vérifiez le document et ses totaux.
| Appel | Ce qu'il donne |
|---|---|
GET /v1/documents/{id}/with-all | le document et ses lignes en un seul appel |
GET /v1/documents/{id}/lines | les lignes seules |
GET /v1/documents/{id}/tax-details | la ventilation de TVA, montant par taux |
Exemple de ventilation, sur un document à 300 € HT :
{ "taxDetailArray": [
{ "rate": 20, "priceBeforeTax": 300, "taxAmount": 60, "priceAfterTax": 360 }
]}
lines de la fiche document est vide
GET /v1/documents/{id} ne renvoie pas les lignes dans son corps. Pour les obtenir, passez par /lines ou /with-all. C'est un piège fréquent quand on croit avoir perdu ses lignes.
5.Finaliser
L'étape qui rend le document définitif, lui attribue son numéro officiel, et le rend non modifiable.
POST /v1/documents/{id}/finalize
Aucun corps de requête. Le document passe à finalized: true et reçoit son numéro définitif (la mention « provisoire » disparaît). C'est ici, et seulement ici, que le crédit est décompté : 2 pour un devis, 8 pour une facture.
Une pièce finalisée ne peut plus être ni modifiée, ni supprimée. Une facture erronée se corrige par un avoir, pas par une suppression. Contrôlez à l'étape 4 avant de finaliser. Un devis finalisé, lui, reste supprimable.
Un garde-fou utile : si vous finalisez une pièce à une date antérieure à la dernière pièce déjà émise, l'API refuse avec 400 Invalid date sequence, pour préserver la continuité de votre numérotation.
6.Valider un devis optionnel
Enregistrer qu'un devis a été accepté par le client.
POST /v1/documents/{id}/validate
{
"email": "claire@demo.fr",
"firstName": "Claire",
"lastName": "Martin",
"validationDate": "2026-09-16T10:00:00Z",
"timeOffset": 7200,
"ip": "0.0.0.0"
}
Les six champs sont obligatoires, y compris timeOffset (décalage avec UTC, en secondes) et ip. L'appel répond 200 avec un corps vide, et la relecture du document montre validated: true, la validationDate et l'identité du signataire. La validation d'un devis coûte 2 crédits.
validated plutôt que finalized
Ne confondez pas les deux : finalized dit seulement que le devis est émis en version définitive, validated dit que le client l'a accepté. Pour lister vos devis acceptés en attente de facturation, c'est validated qu'il faut regarder.
7.Transformer un devis en facture optionnel
À partir d'un devis finalisé, créer la facture correspondante.
POST /v1/documents/{id}/transform-to-invoice
Le devis doit être finalisé. La réponse vous renvoie une nouvelle facture en brouillon, reprenant le client et les lignes, avec son propre id. Vous la finalisez à votre tour pour lui donner son numéro (et c'est cette finalisation-là qui coûte 8 crédits ; la transformation elle-même est gratuite).
Il n'existe aucun lien exposé entre la facture et le devis dont elle est issue : la facture ne porte aucun champ pointant vers sa source. Si vous avez besoin de ce rattachement, stockez le couple devis / facture de votre côté, au moment de la transformation : c'est le seul instant où vous connaissez les deux identifiants.
8.Retrouver ses pièces
Lister, filtrer, paginer.
GET /v1/documents?documentTypes=Quotation&finalized=true&limit=100
Quotation pour un devis, Invoice pour une facture, CreditNote pour un avoir. Avec documentTypes=4 (l'identifiant numérique), vous obtenez un 200 et une liste vide, sans erreur. C'est le piège qui fait croire qu'aucun document n'existe.
Filtres utiles : finalized, fromDate et toDate (sur la date de la pièce), search (sur le numéro), customerId, page et limit (100 maximum), et minId qui ne renvoie que les éléments d'identifiant supérieur : c'est votre curseur pour ne récupérer que les nouveautés d'un passage à l'autre.
9.Le PDF
Récupérer le document imprimable.
| Appel | Résultat |
|---|---|
GET /v1/documents/{id}/pdf | le fichier PDF directement dans la réponse |
POST /v1/documents/{id}/pdf/url | une URL de téléchargement valable 10 minutes |
Juste après la création ou la finalisation, le PDF peut n'être pas encore généré : l'appel répond alors 404 "PDF not available" ou 406 pendant quelques secondes. Ce n'est pas une erreur de votre appel : réessayez deux ou trois secondes plus tard. Et l'URL renvoyée par /pdf/url réclame elle aussi le jeton Authorization, ce n'est pas un lien public.
Ce que ça coûte
Le décompte est simple : seule l'émission d'un document compte.
| Action | Coût |
|---|---|
| Finaliser une facture ou un avoir | 8 crédits |
| Finaliser ou valider un devis | 2 crédits |
| Tout le reste : lectures, création de brouillons, lignes, modifications, PDF, authentification | gratuit |
La formule d'entrée offre 200 crédits par mois, gratuitement, soit environ 25 factures ou 100 devis. Un appel en erreur (code ≥ 400) n'est jamais décompté, et le quota se réinitialise le 1er de chaque mois. La sandbox, elle, est illimitée : toute votre phase de développement ne coûte rien. Le suivi se lit en direct dans les en-têtes x-credits-used, x-credits-limit et x-credits-remaining de chaque réponse.
Les pièges, en un tableau
| Constat | Ce qu'il faut savoir |
|---|---|
Une URL contenant /scalar | répond 404. Les appels vont sur /v1/..., jamais /scalar/v1/... |
documentTypes=1 renvoie une liste vide | le filtre attend le libellé anglais (Invoice, Quotation), pas l'identifiant numérique |
Le champ lines de la fiche est vide | utilisez /lines ou /with-all |
typeId: 1 et 2 (Titre, Sous-titre) sont refusés | utilisez une ligne de texte (typeId: 6) en attendant |
Le champ paid est en lecture seule | il est calculé à partir des règlements ; un PUT qui le force répond 200 mais reste sans effet |
| Un champ inconnu est accepté sans erreur | l'API répond 200 et l'ignore en silence : relisez toujours après écriture |
| Aucun mécanisme d'idempotence | un appel rejoué crée un second document. En cas de doute sur une réponse perdue, relisez plutôt que de recréer |
| Le jeton expire au bout de 10 minutes | renouvelez-le avant un traitement long, pour éviter un 401 en cours de route |
Le lastName d'un contact s'imprime | n'y mettez pas de valeur de remplissage, elle partirait sur les documents |
Écrivez au support en indiquant votre Client ID (jamais votre secret), la route appelée, le corps envoyé et la réponse obtenue. Avec ces quatre éléments, un cas se reproduit du premier coup.