Cette API REST permet à vos outils (site web, CRM, ERP, logiciel métier, scripts) de lire et de créer vos clients, vos factures et vos devis dans FactuPilot, d'émettre vos factures, et de les faire transmettre à votre plateforme agréée. Des webhooks vous préviennent ensuite des événements de chaque facture. Elle est réservée au plan Business (49 EUR par mois).
- Passez votre entreprise au plan Business depuis l'onglet « Abonnement » de votre tableau de bord.
- Ouvrez Paramètres, section Clés API, créez une clé et cochez les portées dont vous avez besoin. La clé complète (
fp_live_ suivi de 32 caractères) n'est affichée qu'une seule fois : copiez-la aussitôt. - Appelez l'API en HTTPS, avec la clé dans l'en-tête
Authorization.
curl "https://factupilot.fr/api/v1/clients?limit=5" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Créer un client puis lui ouvrir une facture brouillon :
curl -X POST "https://factupilot.fr/api/v1/clients" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"name": "Boulangerie Dupont", "email": "contact@boulangerie-dupont.fr"}'
curl -X POST "https://factupilot.fr/api/v1/invoices" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"clientId": "<id renvoyé ci-dessus>", "items": [{"description": "Création du site", "quantity": 1, "unitPriceHt": 650, "vatRate": 20}]}'
Les corps de requête sont en JSON, les réponses aussi (sauf le PDF d'une facture).
Toute requête porte l'en-tête Authorization: Bearer fp_live_.... La clé est vérifiée à chaque appel, y compris l'accès au plan Business de votre entreprise : si l'entreprise quitte le plan Business, ses clés cessent immédiatement de fonctionner (réponse 403) et reprennent dès le retour au plan.
Ne placez jamais une clé dans une page web, une application mobile ou un dépôt public : appelez l'API depuis votre serveur. Une clé compromise se supprime depuis Paramètres > Clés API.
Chaque clé porte des portées choisies à sa création. Chaque opération de ce document indique celle qu'elle exige (champ x-scope).
| Portée | Opérations |
|---|
clients:read | GET /clients |
clients:write | POST /clients |
invoices:read | GET /invoices, GET /invoices/{id}/pdf |
invoices:write | POST /invoices, POST /invoices/{id}/emettre |
quotes:read | GET /quotes |
quotes:write | POST /quotes, POST /quotes/create |
Les routes réservées aux applications connectées au hub (clés fp_app_) ne sont pas décrites ici.
Toute erreur renvoie un corps JSON {"error": "message"} et un code HTTP :
| Code | Signification |
|---|
400 | Requête invalide : corps qui n'est pas du JSON, champ obligatoire absent, date illisible, ligne invalide, paramètre de liste hors limites. Le message nomme la valeur en cause et dit quoi corriger. |
401 | Clé absente, inconnue, expirée ou mal formée (Authorization: Bearer ...). |
403 | Soit la clé n'a pas la portée exigée (Permission refusée · clients:read requise), soit l'entreprise n'a plus l'accès API (plan Business). La clé est alors reconnue, d'où un 403 et non un 401. |
404 | Ressource introuvable dans votre entreprise (client, facture). |
409 | Conflit d'état (POST /invoices/{id}/emettre) : la facture n'est plus un brouillon, ou c'est un brouillon saisi dans FactuPilot, qui s'émet depuis FactuPilot. |
429 | Trop de requêtes. L'en-tête Retry-After indique le délai d'attente en secondes. |
500 | Erreur interne. Le message est générique (Erreur interne du serveur) ; réessayez, puis contactez le support si elle persiste. |
Ordre des contrôles : authentification (401), accès au plan (403), limite de débit (429), portée (403), validation de la requête (400), puis existence de la ressource (404) et état (409).
- Lecture (listes) : 120 requêtes par minute.
- Écriture (création de client, de facture, de devis, émission d'une facture) et PDF : 30 requêtes par minute.
Le décompte se fait par fenêtre de 60 secondes et est commun à toutes les clés utilisateur de l'entreprise ; chaque route applique son propre seuil. Au-delà, l'API répond 429 avec Retry-After. Ces limites sont appliquées par le serveur qui traite l'appel : traitez-les comme un plafond à ne pas approcher plutôt que comme un quota garanti.
Les listes acceptent page (à partir de 1) et limit (20 par défaut, 100 au maximum) et renvoient un bloc pagination : page, limit, total et pages. Les listes sont triées du plus récent au plus ancien.
- Montants en euros, nombres décimaux :
123.45 pour 123,45 EUR. Jamais en centimes, ni en entrée (unitPriceHt) ni en sortie (totalHt, totalVat, totalTtc). - Totaux calculés par FactuPilot : vous envoyez des lignes, jamais de totaux. Le prix unitaire est arrondi au centime à l'enregistrement.
- Dates au format ISO 8601, en UTC (
2026-10-16T09:12:44.318Z). En entrée, une date seule (2026-11-15) vaut minuit UTC. - Taux de TVA en pourcentage (
20, 10, 5.5, 2.1, 0). Une ligne sans vatRate est à 0 % : précisez toujours le taux. - Ligne à 0 % : le motif est obligatoire à l'émission. Pour une entreprise assujettie à la TVA, chaque ligne à 0 % doit porter un
motifTvaZero, choisi dans le catalogue officiel dont les identifiants sont listés dans le schéma MotifTvaZero. Le motif est imprimé sur le document et transmis dans le Factur-X. L'API accepte un brouillon sans motif, mais FactuPilot refuse de l'émettre tant qu'une ligne à 0 % n'en porte pas : renseignez-le dès la création. Une entreprise en franchise en base de TVA n'a pas de motif à fournir. Certains motifs (export, livraison intracommunautaire, autoliquidation) exigent en plus un numéro de TVA intracommunautaire, ou un SIREN, sur la fiche du client.
Un devis créé par l'API naît en brouillon (DRAFT) avec son numéro légal déjà attribué (par exemple DEV-2026-0018), pris dans la même séquence que les documents créés depuis le tableau de bord. Il n'est pas envoyé au client : l'envoi se fait depuis FactuPilot.
Une facture naît elle aussi en brouillon avec son numéro légal (par exemple FA-2026-0043), sauf si vous demandez son émission : voir la section suivante.
Créer un document consomme un numéro de la séquence. Factures : envoyez un en-tête Idempotency-Key (1 à 100 caractères visibles, par exemple un UUID) ; rejouer la requête avec la même clé rend la facture déjà créée (200, en-tête Idempotent-Replayed: true), sans doublon ni nouveau numéro. Devis et clients : leurs créations ne sont pas idempotentes ; si une requête échoue de façon incertaine (délai dépassé, 500), vérifiez avec la liste correspondante avant de la rejouer, pour ne pas créer de doublon.
Émettre. Un logiciel métier qui facture sans intervention humaine n'a pas besoin de brouillon. Deux façons d'émettre une facture, avec les mêmes exigences qu'à l'émission depuis FactuPilot :
- À la création :
POST /invoices avec "emettre": true. La facture naît émise (SENT), numérotée, en un seul appel. - Plus tard :
POST /invoices/{id}/emettre sur un brouillon créé par l'API. Seuls ces brouillons s'émettent ainsi : un brouillon saisi dans FactuPilot répond 409 et s'émet depuis FactuPilot. Une facture déjà émise ou annulée répond 409, une facture inconnue ou d'une autre entreprise 404.
curl -X POST "https://factupilot.fr/api/v1/invoices" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: commande-2026-0042" \
-d '{"clientId": "<id du client>", "emettre": true, "items": [{"description": "Transport Perpignan - Toulouse", "quantity": 1, "unitPriceHt": 420, "vatRate": 20}]}'
Une facture émise est intangible : elle ne se modifie plus, une correction passe par un avoir, depuis FactuPilot. Pour une entreprise assujettie à la TVA, une ligne à 0 % sans motifTvaZero est refusée (400) : avec "emettre": true aucune facture n'est créée et aucun numéro n'est consommé, avec POST /invoices/{id}/emettre le brouillon reste en brouillon.
Transmettre. FactuPilot numérote la facture, produit son PDF Factur-X et la remet à la plateforme agréée de l'entreprise (Super PDP, Iopole...). Pour les factures émises par l'API, cette remise est automatique si l'entreprise a activé le réglage « transmission automatique des factures de l'API » dans FactuPilot ; sinon la facture reste à transmettre depuis FactuPilot. Un brouillon n'est jamais transmis.
La transmission est attendue avant la réponse : l'appel peut donc durer plus longtemps qu'une simple création. Si votre client HTTP abandonne avant la réponse, rejouez la requête avec la même Idempotency-Key : vous obtenez la facture déjà émise et l'état enregistré de sa transmission, sans double émission ni double transmission. Son résultat figure dans l'objet data.transmission de la réponse (201 de POST /invoices, 200 de POST /invoices/{id}/emettre) :
| Champ | Contenu |
|---|
statut | transmise : remise à la plateforme. refusee : une règle l'interdit, rien n'est parti (plan, plateforme en dépôt manuel, adresse électronique de l'acheteur manquante). echec : incident, à relancer depuis FactuPilot. desactivee : transmission automatique non activée pour l'entreprise (ou plan sans l'API), ou coupée sur ce serveur. inconnue : réponse d'un rejeu dont la transmission n'est pas encore enregistrée ; rien n'est retransmis. |
plateforme | superpdp, iopole ou manuel (dépôt manuel), null si elle n'a pas pu être lue. |
statutPlateforme | Statut brut renvoyé par la plateforme à la remise, null si rien n'a été remis. |
message | Explication lisible, en français. |
Un refus ou un échec de transmission ne fait jamais échouer l'appel : la facture est créée et émise, la réponse reste 201 (ou 200). Testez donc data.transmission.statut, pas seulement le code HTTP. Le suivi de la facture auprès de la plateforme (acceptée, rejetée...) vous parvient ensuite par le webhook facture.statut_plateforme.
Plutôt que d'interroger l'API pour savoir ce qu'il est advenu d'une facture, laissez FactuPilot prévenir votre serveur. Un webhook est une adresse HTTPS de votre serveur, à laquelle FactuPilot envoie une requête POST JSON à chaque événement auquel vous l'avez abonné. Les webhooks se créent depuis l'onglet API des Paramètres (cinq au plus par entreprise) : vous y choisissez l'adresse et les événements, et FactuPilot affiche le secret de signature, whsec_ suivi de 32 caractères, une seule fois. L'adresse doit être publique, en https et sur le port standard 443 (pas de réseau local, d'adresse IP privée, ni d'autre port). Le bouton « Envoyer un test » envoie un événement webhook.test.
Comme l'API, les webhooks sont réservés au plan Business : une entreprise qui le quitte ne reçoit plus d'événements. La clé racine webhooks de ce document OpenAPI décrit chaque requête reçue.
| Événement | Quand |
|---|
facture.creee | Une facture vient d'être créée (par l'API, le tableau de bord, un devis accepté, un acompte, un avoir...). |
facture.emise | Une facture vient d'être émise (brouillon passé en SENT, ou facture née émise : elle déclenche alors facture.creee puis facture.emise). |
facture.payee | Une facture vient d'être entièrement payée, y compris par un paiement en ligne. Un paiement partiel ne déclenche rien. |
facture.statut_plateforme | Le suivi de la facture par la plateforme agréée a changé (téléversée, acceptée, rejetée, erreur de transmission...). Un rejeu du même statut ne déclenche rien. |
webhook.test | Envoyé par le bouton « Envoyer un test », même si le webhook est désactivé. On ne s'y abonne pas. |
Requête reçue
En-têtes :
| En-tête | Valeur |
|---|
Content-Type | application/json |
User-Agent | FactuPilot-Webhooks/1 |
X-FactuPilot-Evenement | Le type de l'événement, par exemple facture.emise. |
X-FactuPilot-Livraison | L'identifiant de l'événement (champ id du corps). Identique à chaque nouvelle tentative : c'est votre clé d'idempotence. |
X-FactuPilot-Signature | t=<horodatage en secondes>,v1=<signature hexadécimale> |
Corps (les noms de champs sont ceux des webhooks, en français) :
{
"id": "evt_9f2c4a7e1b3d58606a1c0e2f",
"type": "facture.emise",
"creeLe": "2026-10-16T09:12:44.318Z",
"donnees": {
"facture": {
"id": "jd7c4k9x2p6m8v1q3n5w0r7t4y9b2zhe",
"numero": "FA-2026-0043",
"statut": "SENT",
"typeDocument": "STANDARD",
"totalTtc": 1032,
"client": { "id": "k5738x2vq9m4c1nw6d0h8rtb5y3zj7ae", "nom": "Boulangerie Dupont" },
"plateforme": null
}
}
}
totalTtc est en euros. plateforme vaut null tant que la facture n'a jamais été présentée à une plateforme agréée, puis { "nom", "statut", "code", "statutTexte" } (statut est l'étape de suivi : televersee, en_transit, acceptee, rejetee, erreur...).
Vérifier la signature
Chaque requête est signée avec le secret du webhook : vérifiez la signature avant de traiter l'événement, pour être sûr qu'il vient de FactuPilot et n'a pas été modifié.
- Lisez
X-FactuPilot-Signature : t est l'horodatage de l'envoi (secondes), v1 la signature. - Refusez la requête si
t s'écarte de plus de 5 minutes de votre horloge (protection contre le rejeu). L'horodatage est renouvelé à chaque tentative. - Calculez le HMAC-SHA256, en hexadécimal, de la chaîne
"<t>.<corps>" : l'horodatage, un point, puis le corps brut tel que reçu, octet pour octet. La clé est le secret entier, préfixe whsec_ compris. - Comparez avec
v1 à temps constant (pas avec ===).
Le corps doit être celui reçu, avant tout JSON.parse : re-sérialiser le JSON change les octets et la signature ne correspond plus.
const { createHmac, timingSafeEqual } = require("node:crypto");
const TOLERANCE_SECONDES = 300;
// corpsBrut : le corps tel que reçu (Buffer ou chaîne), avant tout JSON.parse.
// enTete : la valeur de X-FactuPilot-Signature.
// secret : le secret complet du webhook, préfixe whsec_ compris.
function signatureValide(corpsBrut, enTete, secret) {
const parties = {};
for (const morceau of String(enTete || "").split(",")) {
const i = morceau.indexOf("=");
if (i > 0) parties[morceau.slice(0, i)] = morceau.slice(i + 1);
}
const t = Number(parties.t);
if (!Number.isInteger(t) || !parties.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDES) return false;
const attendu = createHmac("sha256", secret).update(t + ".").update(corpsBrut).digest("hex");
const a = Buffer.from(attendu, "hex");
const b = Buffer.from(parties.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
Avec Express, lisez le corps brut pour la route du webhook :
app.post("/webhooks/factupilot", express.raw({ type: "application/json" }), (req, res) => {
if (!signatureValide(req.body, req.get("X-FactuPilot-Signature"), process.env.FACTUPILOT_WEBHOOK_SECRET)) {
return res.status(400).send("Signature invalide");
}
const evenement = JSON.parse(req.body.toString("utf8"));
// Ignorez un evenement.id déjà traité, puis traitez et répondez vite.
res.sendStatus(200);
});
Répondre et nouvelles tentatives
Répondez par un code 2xx en moins de 10 secondes : le corps de votre réponse est ignoré. Traitez l'événement ensuite, de façon asynchrone si c'est long. Tout autre résultat (code 3xx, 4xx ou 5xx, délai dépassé, connexion impossible) est un échec. Une redirection n'est suivie que vers la même origine ; une redirection vers un autre domaine est un échec.
Après un échec, FactuPilot réessaie jusqu'à 6 tentatives au total, avec le même id d'événement :
| Tentative | Délai après l'échec précédent |
|---|
| 1 | Immédiate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 heures |
| 6 | 12 heures |
Après la sixième tentative, la livraison est abandonnée et l'incident est signalé à FactuPilot. Elle reste consultable, avec son code de réponse et son erreur, dans l'historique du webhook ; quand votre serveur est injoignable, l'erreur affichée est « Connexion impossible », sans détail réseau. Pour rattraper des événements manqués, relisez les factures avec GET /invoices.
Quelques règles de robustesse : dédupliquez sur id (une même livraison peut arriver plus d'une fois), ne comptez pas sur l'ordre des événements (une nouvelle tentative de facture.emise peut arriver après facture.payee : fiez-vous au statut de la charge ou relisez la facture avec GET /invoices?ids=...), et régénérez le secret s'il a fuité (l'ancien cesse de fonctionner aussitôt).
- Les clients archivés, les factures archivées et les anciennes versions de devis révisés ne figurent pas dans les listes.
- Un paramètre ou un champ illisible (corps qui n'est pas du JSON, date,
status hors de la liste, limit inférieur à 1, description de ligne vide) donne une erreur 400 dont le message nomme la valeur en cause. Une erreur 500 est un incident de FactuPilot : réessayez, puis contactez le support.