/developpeurs · API partenaire
L'API Billies — devis et factures conformes dans ton logiciel.
Ton produit génère des devis signables en ligne et des factures légales françaises — numérotation séquentielle, TVA, mentions obligatoires, PDF, e-facturation via une plateforme de dématérialisation — sans développer un moteur de facturation. En marque grise : tes utilisateurs restent dans ton interface, les documents portent la mention « propulsé par Billies ». Ou en mode connecté : ton utilisateur branche son propre compte Billies, ton logiciel y crée et émet ses documents avec son accord.
Deux offres, la même API
Tous les endpoints, sans engagement ni frais de mise en service dans les deux cas. Seule la façon de payer change.
Intégré
99 € par mois · 10 sous-comptes actifs inclus ·puis 5 € par sous-compte actif
Le prix par sous-compte actif est dégressif, par tranche : 5 € par mois du 11ᵉ au 100ᵉ, 4 € du 101ᵉ au 500ᵉ, 3 € au-delà. Un sous-compte est « actif » un mois donné s'il a émis au moins un document ce mois-là. Un sous-compte qui n'émet rien ne coûte rien : tu peux créer un sous-compte pour chacun de tes utilisateurs sans regarder le compteur.
À l'acte
0 € par mois · 0,50 € par document émis
Dégressif par tranche, remis à zéro chaque mois : 0,50 € par document jusqu'à 999 par mois, puis 0,30 € de 1 000 à 9 999, puis 0,20 € au-delà. Pas d'abonnement : un mois sans émission est un mois à 0 €. Une carte est demandée à l'activation.
Dans les deux offres, les documents émis sur des comptes connectés (voir mode connecté) ne te sont jamais facturés — c'est l'utilisateur qui paie son propre abonnement Billies. Le détail de ta consommation est disponible à tout moment via GET /usage.
Gros volumes ou partenariat ? Une offre sur mesure est possible — écris-nous à contact@billies.fr.
Démarrage rapide
Du compte vide au devis signable : cinq étapes, cinq appels.
- 1.
Active l'API dans tes réglages
Depuis ton compte Billies, active l'offre API (99 €/mois). Ton compte devient un compte partenaire.
- 2.
Crée ta clé
Toujours dans les réglages : génère une clé bil_live_…. Elle n'est affichée qu'une fois — stocke-la dans ton gestionnaire de secrets.
- 3.
Crée un sous-compte
Un sous-compte par entreprise cliente de ton logiciel. C'est lui qui porte l'identité légale des documents — donne-lui tout de suite son SIRET, son adresse, sa forme juridique et son régime de TVA : sans ces quatre-là, l'étape 5 refusera d'émettre.
- 4.
Crée un devis
Un client final + des lignes en centimes. Le document naît en brouillon, les totaux sont calculés pour toi.
- 5.
Émets
L'émission attribue le numéro légal et génère le PDF. Tu récupères l'URL du PDF et, si tu veux, un lien de signature en ligne.
# Ta clé API — créée dans Réglages → API (affichée une seule fois)
export BILLIES_API_KEY="bil_live_…"
BASE="https://billies.fr/api/partner/v1"
# 1. Crée un sous-compte pour ton client final.
# legalForm et vatRegime ne sont pas décoratifs : les fournir vaut
# confirmation, et sans confirmation l'émission (étape 4) répond 400.
curl -s -X POST "$BASE/companies" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Idempotency-Key: onboarding-martin-001" \
-H "Content-Type: application/json" \
-d '{
"name": "Plomberie Martin",
"siret": "73282932000074",
"legalForm": "ei",
"vatRegime": "reel_normal",
"addressLine1": "12 rue des Ateliers",
"postalCode": "69003",
"city": "Lyon"
}'
# → 201 { "company": { "id": "COMPANY_ID", … } }
# 2. Ajoute le client à facturer
curl -s -X POST "$BASE/companies/COMPANY_ID/clients" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "business", "displayName": "SCI Les Tilleuls", "email": "compta@lestilleuls.example" }'
# → 201 { "client": { "id": "CLIENT_ID", … } }
# 3. Crée un devis (brouillon)
curl -s -X POST "$BASE/companies/COMPANY_ID/documents" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Idempotency-Key: devis-sdb-tilleuls-01" \
-H "Content-Type: application/json" \
-d '{
"type": "quote",
"clientId": "CLIENT_ID",
"title": "Rénovation salle de bain",
"lines": [
{ "description": "Pose faïence murale", "quantity": 12, "unitPriceCents": 4500, "vatRate": 10, "unit": "m²" }
]
}'
# → 201 { "document": { "id": "DOCUMENT_ID", "status": "draft", "totalTtcCents": 59400, … } }
# 4. Émets : numéro légal + PDF, le document devient immuable
curl -s -X POST "$BASE/companies/COMPANY_ID/documents/DOCUMENT_ID/issue" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Idempotency-Key: emission-devis-sdb-01"
# → 200 { "number": "D-20260709-0001", "status": "sent", "pdfUrl": "https://…",
# "einvoice": { "pdpStatus": null, "pdpTransmitted": false, … } }
# einvoice.pdpStatus = null ici : un DEVIS n'est jamais transmis au réseau.
# Sur une facture, c'est ce bloc — pas le code HTTP — qui dit si elle est partie.
# 5. Lien de signature en ligne, à afficher dans TON interface
curl -s -X POST "$BASE/companies/COMPANY_ID/documents/DOCUMENT_ID/signature-link" \
-H "Authorization: Bearer $BILLIES_API_KEY"
# → 200 { "url": "https://billies.fr/q/…" }Les fondamentaux
Authentification
Chaque requête porte ta clé API en en-tête Authorization: Bearer bil_live_…. Tu crées tes clés dans tes réglages Billies ; la clé complète n'est affichée qu'une seule fois (on n'en stocke qu'une empreinte). Plusieurs clés peuvent être actives en parallèle, pour une rotation sans coupure : crée la nouvelle, bascule ton code, révoque l'ancienne.
curl "https://billies.fr/api/partner/v1/companies" \
-H "Authorization: Bearer bil_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Format des erreurs
Toute erreur renvoie le même objet JSON, avec un code stable sur lequel brancher ton code — le message est là pour les humains, il peut changer.
{
"error": {
"code": "conflict",
"message": "Ce document est déjà émis : il ne peut plus être modifié."
}
}| Code | HTTP | Quand |
|---|---|---|
| unauthorized | 401 | Clé absente, révoquée ou inconnue. |
| forbidden | 403 | La clé n'a pas le scope requis — OU ton plafond mensuel de documents est atteint sur POST …/issue — OU l'action n'appartient pas au partenaire sur un compte connecté (numérotation, réglages). |
| not_found | 404 | Ressource inexistante — ou rattachée à un autre partenaire. |
| invalid_request | 400 | Corps invalide, champ manquant, Idempotency-Key absent — ou garde-fou légal à l'émission (SIRET, adresse, forme juridique ou régime de TVA non confirmés côté émetteur, RCS et capital d'une société, SIRET d'un client entreprise). Le message porte alors un code FCT-… stable. |
| conflict | 409 | L'état du document interdit l'action (ex. modifier un document émis) ; une requête identique (même Idempotency-Key) est encore en cours ; un compteur de numérotation reculerait sous un numéro déjà émis ; une re-soumission au réseau est refusée (soumission encore vivante, sort inconnu, ou donnée engageante modifiée) ; l'activation de l'e-reporting n'a aucune chance d'aboutir ; une facture reçue ne peut pas être téléchargée faute de raccordement — ce dernier cas porte un en-tête x-billies-reason nommé. |
| rate_limited | 429 | Trop de requêtes sur la fenêtre en cours — réessaie après une pause. |
| internal | 500 | Erreur côté Billies. Rejouer la même Idempotency-Key est sans risque, mais ne réussit pas toujours — voir ci-dessous. |
Nuance sur le 500 : rejouer est toujours sans danger(rien n'a été committé, aucun numéro légal n'a été consommé), mais ce n'est pas toujours utile. Un échec de génération Factur-X — une règle de mapping que la facture ne satisfait pas — est déterministe : le rejeu échouera à l'identique tant que la donnée n'a pas changé.
Et le 500 ne te dira pas quoi corriger : son message est générique (« Émission impossible pour le moment »), le détail technique reste dans nos journaux. Seul le 400est actionnable — c'est lui qui porte le diagnostic, le remède sous forme d'appel API, et un code FCT-…stable. Donc : deux ou trois tentatives au maximum, puis arrête et écris-nous avec l'heure de l'appel et l'identifiant du document — c'est ce qui nous permet de retrouver la cause. Boucler n'apportera rien de plus.
Idempotence : jamais deux factures pour un retry
Un timeout réseau ne te dit pas si l'appel a abouti. Sans protection, le retry naturel de ton code créerait un deuxième document — et deux numéros légaux. C'est pourquoi l'en-tête Idempotency-Key est obligatoire (sinon 400) sur les trois POST qui créent quelque chose d'irréversible ou de numéroté : POST /companies, POST …/documents et POST …/documents/{id}/issue. Sur POST …/payments et POST …/credit-note, l'en-tête est facultatif — mais recommandé.
Choisis une clé qui identifie l'opération côté chez toi (ex. facture-cmd-8842) : rejouer la même requête avec la même clé renvoie la réponse d'origine, sans rien ré-exécuter. Deux requêtes simultanées avec la même clé ne s'exécutent jamais deux fois : la seconde reçoit un 409 conflict tant que la première est en vol — réessaie quelques secondes plus tard pour obtenir la réponse stockée. Seules les réponses définitives sont stockées : après un 500, la clé reste rejouable.
Montants en centimes
Tous les montants sont des entiers en centimes d'euro : "unitPriceCents": 4500= 45,00 €. Jamais de flottants, donc jamais d'erreur d'arrondi. Les taux de TVA sont des pourcentages (20, 10, 5.5, 2.1, 0) et les totaux sont toujours calculés côté Billies.
Mode connecté : ton utilisateur garde son compte Billies
La marque grise (sous-comptes) convient quand tes utilisateurs n'ont pas de compte Billies : tu portes tout. Le mode connecté couvre l'autre cas : ton utilisateur a — ou prend — son propre compte Billies, et il autorise ton logiciel à créer et émettre des documents dessus. Le cas type : un back-office de vente qui délègue devis, factures et e-facturation à Billies.
Le flux, en cinq temps
- 1.
« Connecter Billies », chez toi
Tu génères un state opaque (un nonce, stocké côté serveur), puis tu rediriges l'utilisateur vers https://billies.fr/connect/<ton-slug> avec redirect_uri et state en paramètres.
- 2.
Il se connecte — ou s'inscrit
Écran Billies co-brandé à tes couleurs (ton nom, ton logo). S'il n'a pas encore de compte Billies, il en crée un au passage : c'est le sien, pas un sous-compte à toi.
- 3.
Il autorise
L'écran de consentement liste précisément ce que ton logiciel pourra faire sur son compte — et ce qu'il ne pourra pas. S'il refuse, il revient chez toi avec ?error=access_denied.
- 4.
Retour chez toi
Billies le renvoie sur ton redirect_uri avec ?state=…&billies_company_id=…. Ces paramètres ne sont que de l'UX : ne t'y fie pas pour ouvrir un accès.
- 5.
Ton serveur vérifie
GET /api/partner/v1/links?state=… est la source de vérité : il renvoie le companyId et les scopes consentis. Stocke le companyId — tous tes appels /companies/{companyId}/… passent ensuite par lui.
# 1. Chez toi : bouton « Connecter Billies » → redirection de l'utilisateur
https://billies.fr/connect/ton-slug?redirect_uri=https://ton-app.example/billies/callback&state=n-4f7a2b9c
# 2-3. Chez Billies : il se connecte (ou crée son compte), puis autorise
# — écrans co-brandés à tes couleurs.
# 4. Retour chez toi :
# autorisé → https://ton-app.example/billies/callback?state=n-4f7a2b9c&billies_company_id=1f6f9c2e-…
# refusé → https://ton-app.example/billies/callback?state=n-4f7a2b9c&error=access_denied
# 5. Côté SERVEUR, vérifie avec ton state — c'est la source de vérité :
curl "https://billies.fr/api/partner/v1/links?state=n-4f7a2b9c" \
-H "Authorization: Bearer $BILLIES_API_KEY"
# → 200 { "link": { "companyId": "1f6f9c2e-…", "scopes": […], "revokedAt": null } }Le redirect_uridoit figurer dans la liste d'URLs de retour déclarée pour ton compte partenaire (protection contre la redirection ouverte) — sinon le flux répond 400avant même l'écran de connexion. Pour déclarer tes URLs : écris à contact@billies.fr.
Ce que ça change
- Il paie son abonnement Billies, rien pour toi.Les documents émis sur un compte connecté ne comptent ni dans tes sous-comptes actifs, ni dans tes documents à l'acte — ils ne te sont jamais facturés.
- Il gère lui-même sa connexion e-facturationdans ses réglages Billies (plateforme de dématérialisation, Factur-X). Tu n'as rien à configurer : le même
POST …/issuesuit sa configuration. - Ton accès est limité à ce qu'il a consenti : documents, clients, paiements — jamais ses réglages (
PATCH /companies/{id}répond403) ni son IBAN. Et il peut révoquer l'accès à tout moment, depuis ses réglages. - Les documents vivent aussi chez lui.Un devis créé via ton logiciel apparaît dans son app Billies, web et mobile — il le retrouve, le suit, l'archive comme les autres.
- L'habillage suit son plan Billies: sur un compte gratuit, les documents gardent la mention « Fait avec billies.fr », comme ceux qu'il crée lui-même.
Une fois le companyIdrécupéré, tout le reste de l'API fonctionne à l'identique. Les deux endpoints du mode connecté sont détaillés dans la référence.
Référence des endpoints
Base : https://billies.fr/api/partner/v1. Tous les endpoints, famille par famille : sous-comptes, clients finaux, documents, émission, facturation électronique, mode connecté, e-reporting, factures reçues, consommation.
Sous-comptes
Un sous-compte représente une entreprise cliente de ton logiciel : son identité légale, ses coordonnées bancaires, son habillage PDF. Chaque devis et chaque facture appartient à un sous-compte.
Créer un sous-compte
POST /api/partner/v1/companies
Scope companies:write · en-tête Idempotency-Key obligatoire
Crée l'entreprise cliente finale. Seul name est requis POUR CRÉER — mais un sous-compte réduit à son nom n'émettra rien : il manque le SIRET, l'adresse complète, la forme juridique et le régime de TVA (détail dans les notes). vatRegime vaut franchise_base, reel_simplifie ou reel_normal ; legalForm vaut auto_entrepreneur, ei, eurl, sarl, sas ou sasu. Fournir ces deux champs explicitement — ici ou au PATCH — vaut CONFIRMATION : c'est ce que l'émission exige. logoUrl (URL https publique) s'affiche en tête des PDF de facture. vatNumber est optionnel : à défaut, il est dérivé automatiquement du SIREN dès que le sous-compte est assujetti (régime ≠ franchise en base). Pour une société, tu peux poser rcsNumber, rcsCity et shareCapitalCents (capital en centimes) dès la création.
{
"name": "Plomberie Martin",
"logoUrl": "https://cdn.monlogiciel.fr/logos/plomberie-martin.png",
"siret": "73282932000074",
"legalForm": "ei",
"addressLine1": "12 rue des Ateliers",
"postalCode": "69003",
"city": "Lyon",
"country": "FR",
"vatRegime": "reel_normal"
}{
"company": {
"id": "1f6f9c2e-8d1a-4b6e-9f3d-2a7c5e8b1d40",
"name": "Plomberie Martin",
"logoUrl": "https://cdn.monlogiciel.fr/logos/plomberie-martin.png",
"legalForm": "ei",
"siret": "73282932000074",
"vatNumber": "FR44732829320",
"vatRegime": "reel_normal",
"addressLine1": "12 rue des Ateliers",
"addressLine2": null,
"postalCode": "69003",
"city": "Lyon",
"country": "FR",
"ibanLast4": null,
"einvoiceEnabled": false,
"createdAt": "2026-07-01T09:12:00.000Z"
}
}curl -X POST "https://billies.fr/api/partner/v1/companies" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Idempotency-Key: onboarding-martin-001" \
-H "Content-Type: application/json" \
-d '{ "name": "Plomberie Martin", "siret": "73282932000074", "legalForm": "ei", "vatRegime": "reel_normal", "addressLine1": "12 rue des Ateliers", "postalCode": "69003", "city": "Lyon" }'- Ce qu'il faut AVANT la première émission, sans quoi POST …/issue répond 400 : le SIRET, l'adresse complète (rue + code postal + ville), la forme juridique et le régime de TVA. Les deux derniers ne se devinent pas — une colonne remplie par défaut est indiscernable d'un vrai choix, donc l'émission exige un choix POSÉ. Envoie legalForm et vatRegime explicitement (ici ou au PATCH) : c'est ce qui vaut confirmation. Les envoyer une seconde fois avec la même valeur ne change rien.
- Sans ces quatre données, tu peux créer le sous-compte, créer ses clients, créer des brouillons : tout marche. C'est l'émission — et elle seule — qui refuse. Prends-les donc dans TON parcours d'inscription plutôt qu'au moment où ton utilisateur clique « envoyer la facture ».
- Un sous-compte ne coûte rien tant qu'il n'émet pas de document dans le mois.
- logoUrl doit être une URL http(s):// publique (500 caractères max), rendue telle quelle en tête des PDF émis — héberge le fichier chez toi et passe le lien. Les URL data: ou javascript: sont refusées.
- vatNumber est facultatif : fourni, il est normalisé (FR + clé + SIREN) ; omis, il est dérivé automatiquement du SIREN dès que le sous-compte est assujetti (reel_simplifie / reel_normal). En franchise_base aucun numéro n'est posé (art. 293 B du CGI).
- Piège à connaître : sans vatRegime, le sous-compte naît en franchise_base (TVA non applicable, art. 293 B du CGI) — ses documents sortent sans TVA, quel que soit le vatRate des lignes. Pour une entreprise qui facture la TVA, envoie reel_normal ou reel_simplifie.
- Sociétés (sarl, sas, sasu, eurl) : renseigne rcsNumber, rcsCity et shareCapitalCents (capital social en centimes) avant la première émission — sinon l'émission renvoie une erreur de conformité (art. R.123-237 Code commerce).
- Idempotence : seule une réponse de succès (2xx) est mémorisée puis rejouée à l'identique. Une erreur (ex. 400 SIRET invalide) n'est PAS mémorisée — corrige la donnée et rejoue la MÊME Idempotency-Key, la création aboutit.
Lister tes sous-comptes
GET /api/partner/v1/companies
Scope companies:read
Liste paginée de tous les sous-comptes rattachés à ta clé. Paramètres : limit (défaut 50) et offset.
{
"companies": [
{
"id": "1f6f9c2e-8d1a-4b6e-9f3d-2a7c5e8b1d40",
"name": "Plomberie Martin",
"siret": "73282932000074",
"city": "Lyon",
"createdAt": "2026-07-01T09:12:00.000Z"
}
],
"total": 1
}curl "https://billies.fr/api/partner/v1/companies?limit=20&offset=0" \
-H "Authorization: Bearer $BILLIES_API_KEY"Détail d'un sous-compte
GET /api/partner/v1/companies/{companyId}
Scope companies:read
Renvoie la fiche complète du sous-compte. 404 si l'identifiant n'existe pas ou n'appartient pas à ta clé.
{
"company": {
"id": "1f6f9c2e-8d1a-4b6e-9f3d-2a7c5e8b1d40",
"name": "Plomberie Martin",
"logoUrl": "https://cdn.monlogiciel.fr/logos/plomberie-martin.png",
"legalForm": "ei",
"siret": "73282932000074",
"vatNumber": "FR44732829320",
"vatRegime": "reel_normal",
"addressLine1": "12 rue des Ateliers",
"postalCode": "69003",
"city": "Lyon",
"country": "FR",
"ibanLast4": "0189",
"einvoiceEnabled": false,
"createdAt": "2026-07-01T09:12:00.000Z"
}
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-8d1a-4b6e-9f3d-2a7c5e8b1d40" \
-H "Authorization: Bearer $BILLIES_API_KEY"Modifier un sous-compte
PATCH /api/partner/v1/companies/{companyId}
Scope companies:write
Mise à jour partielle : n'envoie que les champs à changer. Éditables : name, logoUrl, siret (une seule fois), vatNumber, vatRegime, legalForm, rcsCity, rcsNumber, shareCapitalCents, addressLine1, addressLine2, postalCode, city, country, iban, bic, bankName, pdfTemplateKey, pdfBrandColor, pdfFooterText, legalMentionsCustom, einvoiceEnabled, einvoiceProfile (en16931 | extended), ereportingEnabled, hasVatOnDebits, vatFrequency (monthly | quarterly | null).
{
"logoUrl": "https://cdn.monlogiciel.fr/logos/plomberie-martin.png",
"vatRegime": "reel_normal",
"iban": "FR7630006000011234567890189",
"bic": "AGRIFRPP",
"bankName": "Crédit Agricole",
"pdfBrandColor": "#1d3a6b",
"pdfFooterText": "Merci pour votre confiance."
}{
"company": {
"id": "1f6f9c2e-8d1a-4b6e-9f3d-2a7c5e8b1d40",
"name": "Plomberie Martin",
"vatRegime": "reel_normal",
"ibanLast4": "0189",
"bankName": "Crédit Agricole",
"pdfBrandColor": "#1d3a6b",
"pdfFooterText": "Merci pour votre confiance."
}
}curl -X PATCH "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "vatRegime": "reel_normal", "pdfBrandColor": "#1d3a6b" }'- L'IBAN est chiffré au repos : il n'est jamais renvoyé en clair par l'API (ibanLast4 seulement).
- Le SIRET se pose une seule fois : le changer ensuite renvoie 409 (verrou de numérotation légale).
- Changer vatRegime n'affecte que les documents émis après le changement — les documents émis sont immuables.
- Sociétés (sarl, sas, sasu, eurl) : renseigne rcsNumber, rcsCity et shareCapitalCents (capital social en centimes) avant la première émission — sinon l'émission renvoie une erreur de conformité (art. R.123-237 Code commerce).
- logoUrl, pdfTemplateKey, pdfBrandColor et pdfFooterText pilotent l'habillage des PDF émis — c'est là que se joue la marque grise. Le logo se met à jour autant de fois que voulu (contrairement au SIRET) ; sans logo, le PDF affiche le nom de la société en texte. Attention : pdfBrandColor n'est appliquée que par les templates bold et elegant. bw, classic et modern ont une palette figée et l'ignorent. pdfBrandColor à null = chaque template reprend sa couleur d'origine.
- vatNumber : le poser explicitement écrase la valeur courante ; le laisser vide déclenche, sur un sous-compte assujetti, une dérivation automatique depuis le SIREN — et cette dérivation ne s'applique qu'une fois (elle n'écrase jamais un numéro déjà présent).
- Forme juridique et régime de TVA : envoyer legalForm ou vatRegime ici VAUT CONFIRMATION du choix, exactement comme à la création. C'est le fait de recevoir le champ qui confirme, pas sa valeur — renvoyer la valeur déjà en place reconfirme sans rien changer d'autre.
- Les omettre des deux côtés (création ET modification) laisse le sous-compte bloqué au PREMIER POST …/documents/{id}/issue : 400, avec FCT-LEGAL-LEGAL_FORM_UNCONFIRMED ou FCT-LEGAL-VAT_REGIME_UNCONFIRMED. Ce n'est pas un excès de zèle — les deux colonnes ont un défaut (ei, franchise_base) qu'on ne peut pas distinguer d'un vrai choix, et émettre dessus sortirait une facture portant « TVA non applicable, art. 293 B du CGI » alors que la TVA reste due. Le refus tombe AVANT l'allocation du numéro : rien n'est brûlé, le brouillon est intact, tu poses les deux champs et tu rejoues.
- Facturation électronique : einvoiceEnabled active le Factur-X sur les factures B2B, ereportingEnabled la déclaration e-reporting B2C. hasVatOnDebits et vatFrequency décrivent le régime de TVA — un changement est répercuté à la plateforme de dématérialisation (best-effort). L'objet company renvoie aussi pdpConnected et pdpMode (delegated | platform | none) : delegated = le sous-compte a autorisé Billies sur SON compte de plateforme (cf. POST …/pdp/connect-link), platform = repli sous l'identité de la plateforme, none = rien ne part. Activer einvoiceEnabled ne raccorde RIEN par lui-même.
- ereportingEnabled: true peut être REFUSÉ en 409 — on préfère refuser qu'écrire un true qui ne transmettrait jamais rien, parce qu'un partenaire ne relit pas la réponse champ par champ et croirait l'e-reporting actif. Trois cas, dans cet ordre : (1) le sous-compte est en mode démonstration — rien de ce qu'il émet ne part vers l'administration ; (2) l'e-reporting n'est pas ouvert côté serveur Billies pour l'instant (interrupteur global, rien à faire de ton côté) ; (3) le sous-compte n'est raccordé à aucune plateforme de dématérialisation — lis pdpConnected sur GET /companies/{companyId} avant d'activer, et raccorde-le avec POST …/pdp/connect-link. La DÉSACTIVATION (false) passe toujours, quel que soit l'état : c'est le kill-switch, il ne se bloque jamais.
- einvoiceProfile (en16931 | extended) ne change que le profil déclaré dans les métadonnées du PDF Factur-X (Factur-X-EN16931 ou Factur-X-Extended). Le XML envoyé à la plateforme, lui, porte toujours le CustomizationID EN 16931 (urn:cen.eu:en16931:2017) : passer en extended n'ouvre aucun champ supplémentaire côté réseau et ne change ni la validation ni le routage. Laisse en16931 sauf besoin identifié côté lecteur du PDF.
Lire la numérotation
GET /api/partner/v1/companies/{companyId}/numbering
Scope companies:read
Renvoie les préfixes de numérotation (devis / facture / avoir) et l'état des compteurs de séquence (par type et par année).
{
"numbering": {
"quotePrefix": "D",
"invoicePrefix": "F",
"creditNotePrefix": "AV",
"sequences": [
{ "type": "invoice", "year": 2026, "lastNumber": 42 },
{ "type": "credit_note", "year": 2026, "lastNumber": 3 }
]
}
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/numbering" \
-H "Authorization: Bearer $BILLIES_API_KEY"Configurer la numérotation
PUT /api/partner/v1/companies/{companyId}/numbering
Scope companies:write
Utilise le verbe PUT. Configure les préfixes et, surtout, AMORCE les compteurs pour continuer une série existante quand tu migres un client depuis un autre logiciel. Préfixes : 1 à 10 caractères A-Z, chiffres et tiret. seedCounters pose le dernier numéro atteint pour une année donnée : la prochaine facture repart à la valeur + 1.
{
"invoicePrefix": "F",
"creditNotePrefix": "AV",
"seedCounters": { "invoice": 42, "creditNote": 3, "year": 2026 }
}{
"numbering": {
"quotePrefix": "D",
"invoicePrefix": "F",
"creditNotePrefix": "AV",
"sequences": [
{ "type": "invoice", "year": 2026, "lastNumber": 42 },
{ "type": "credit_note", "year": 2026, "lastNumber": 3 }
]
}
}curl -X PUT "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/numbering" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "invoicePrefix": "F", "creditNotePrefix": "AV", "seedCounters": { "invoice": 42, "year": 2026 } }'- Amorcer un compteur FORCE le format annuel (F-2026-0043) sur le sous-compte, quel que soit son réglage précédent — sans ça, le compteur semé (indexé par année) serait ignoré au profit du compteur quotidien, qui est le format par défaut. Ce n'est pas une option : c'est la contrepartie de l'amorçage.
- Un compteur peut AVANCER (re-semer plus haut est toujours accepté), jamais RECULER : amorcer sous un numéro déjà émis renvoie 409 conflict, avec le dernier numéro émis dans le message. La borne est le plus grand numéro RÉELLEMENT émis, pas la valeur du compteur.
- 403 sur un compte connecté (mode connecté) : la numérotation appartient à son propriétaire, qui la règle sur billies.fr. Cet endpoint ne vaut que pour tes sous-comptes.
- Chaque valeur de seedCounters est le DERNIER numéro atteint : pose 42 pour que la prochaine facture soit la 43.
Clients finaux
Le carnet d'adresses d'un sous-compte : les particuliers et entreprises que ton utilisateur facture. Un document référence toujours un client de ce carnet.
Créer un client
POST /api/partner/v1/companies/{companyId}/clients
Scope clients:write
Seul displayName est requis. type vaut individual (particulier) ou business (professionnel). Pour un professionnel, le siret n'est pas une recommandation : dès que la facturation électronique est active sur le sous-compte, une facture ou un avoir adressé à un client business SANS SIRET est REFUSÉ à l'émission (400) — le brouillon survit, il suffit d'ajouter le SIRET. legalName apparaît sur le document à côté du displayName.
{
"type": "business",
"displayName": "SCI Les Tilleuls",
"legalName": "SCI Les Tilleuls",
"siret": "90123456700013",
"email": "compta@lestilleuls.example",
"phone": "0612345678",
"addressLine1": "8 avenue des Platanes",
"postalCode": "69006",
"city": "Lyon"
}{
"client": {
"id": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"type": "business",
"displayName": "SCI Les Tilleuls",
"legalName": "SCI Les Tilleuls",
"siret": "90123456700013",
"email": "compta@lestilleuls.example",
"addressLine1": "8 avenue des Platanes",
"postalCode": "69006",
"city": "Lyon",
"country": "FR"
}
}curl -X POST "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/clients" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "business", "displayName": "SCI Les Tilleuls", "email": "compta@lestilleuls.example" }'- Pourquoi le SIRET compte tant sur un client business : c'est lui qui rend la facture routable sur le réseau. Sans lui, la facture ne pourrait pas être transmise et sortirait en PDF simple sans que personne le sache — plutôt que de laisser ce silence, l'émission refuse. Même règle pour l'avoir : la facture d'origine, elle, est bien partie, un avoir non transmis n'annulerait rien.
- Le contrôle vaut aussi sans facturation électronique si le sous-compte a activé l'exigence de SIRET B2B. Dans les deux cas, c'est un 400 à l'émission, jamais à la création du client.
- btpSubcontractingDefault (booléen, accepté et renvoyé) marque un donneur d'ordre en sous-traitance BTP. ATTENTION : il n'est PAS recopié sur les documents créés par l'API — un brouillon créé ici naît sans autoliquidation et ses totaux COLLECTENT la TVA. Le champ ne sert aujourd'hui qu'à l'app Billies, quand l'utilisateur final change le client d'un document depuis son interface. Un document réellement en autoliquidation BTP (art. 283-2 nonies) est facturé en HT (TVA ramenée à 0) et sort du flux e-reporting : il est B2B par nature.
Lister les clients
GET /api/partner/v1/companies/{companyId}/clients
Scope clients:read
Liste paginée du carnet du sous-compte. q filtre sur le nom (recherche plein texte simple), limit et offset paginent.
{
"clients": [
{
"id": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"type": "business",
"displayName": "SCI Les Tilleuls",
"email": "compta@lestilleuls.example",
"city": "Lyon"
}
],
"total": 1
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/clients?q=tilleuls" \
-H "Authorization: Bearer $BILLIES_API_KEY"Détail d'un client
GET /api/partner/v1/companies/{companyId}/clients/{clientId}
Scope clients:read
Renvoie la fiche complète du client.
{
"client": {
"id": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"type": "business",
"displayName": "SCI Les Tilleuls",
"legalName": "SCI Les Tilleuls",
"siret": "90123456700013",
"email": "compta@lestilleuls.example",
"phone": "0612345678",
"addressLine1": "8 avenue des Platanes",
"postalCode": "69006",
"city": "Lyon",
"country": "FR"
}
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/clients/b8e64c1d-…" \
-H "Authorization: Bearer $BILLIES_API_KEY"Modifier un client
PATCH /api/partner/v1/companies/{companyId}/clients/{clientId}
Scope clients:write
Mise à jour partielle, mêmes champs qu'à la création. Les documents déjà émis ne sont pas retouchés : ils gardent les coordonnées du client au moment de l'émission.
{
"email": "facturation@lestilleuls.example",
"addressLine1": "10 avenue des Platanes"
}{
"client": {
"id": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"displayName": "SCI Les Tilleuls",
"email": "facturation@lestilleuls.example",
"addressLine1": "10 avenue des Platanes"
}
}curl -X PATCH "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/clients/b8e64c1d-…" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "facturation@lestilleuls.example" }'Documents — brouillons
Un document naît toujours en brouillon (status draft) : sans numéro, modifiable, supprimable. Les totaux (HT, TVA, TTC) sont recalculés côté Billies à partir des lignes — tu n'envoies jamais de total.
Créer un devis ou une facture (brouillon)
POST /api/partner/v1/companies/{companyId}/documents
Scope documents:write · en-tête Idempotency-Key obligatoire
type vaut quote (devis) ou invoice (facture). Chaque ligne porte sa quantité, son prix unitaire HT en centimes et son taux de TVA (en pourcentage : 20, 10, 5.5, 2.1 ou 0). Le title est optionnel : il n'existe pas de champ titre sur le document — il est converti en ligne d'en-tête (kind section) en position 0, et c'est là qu'il ressort dans la réponse (pas de champ title). La réponse contient le document complet avec les totaux calculés.
{
"type": "quote",
"clientId": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"title": "Rénovation salle de bain",
"notes": "Acompte de 30 % à la signature.",
"attachCgv": true,
"lines": [
{
"description": "Pose faïence murale",
"quantity": 12,
"unitPriceCents": 4500,
"vatRate": 10,
"unit": "m²"
},
{
"description": "Remplacement mitigeur thermostatique",
"quantity": 1,
"unitPriceCents": 18900,
"vatRate": 10,
"unit": "forfait"
}
]
}{
"document": {
"id": "7c9e4b2a-6d5f-4a1b-8e3c-9f0d1a2b3c4d",
"type": "quote",
"status": "draft",
"number": null,
"clientId": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"issuedAt": "2026-07-09",
"dueAt": null,
"expiresAt": "2026-08-08",
"totalHtCents": 72900,
"totalVatCents": 7290,
"totalTtcCents": 80190,
"globalDiscountPct": null,
"creditNoteOfId": null,
"createdAt": "2026-07-09T10:04:00.000Z",
"updatedAt": "2026-07-09T10:04:00.000Z",
"notes": "Acompte de 30 % à la signature.",
"paymentTermsText": "Paiement sous 30 jours",
"pdpStatus": null,
"attachCgv": true,
"lines": [
{ "id": "1a2b3c4d-…", "position": 0, "kind": "section", "description": "Rénovation salle de bain", "quantity": 0, "unit": "u", "unitPriceCents": 0, "vatRate": 0, "discountPct": 0, "totalHtCents": 0, "totalVatCents": 0, "totalTtcCents": 0 },
{ "id": "2b3c4d5e-…", "position": 1, "kind": "item", "description": "Pose faïence murale", "quantity": 12, "unit": "m²", "unitPriceCents": 4500, "vatRate": 10, "discountPct": 0, "totalHtCents": 54000, "totalVatCents": 5400, "totalTtcCents": 59400 },
{ "id": "3c4d5e6f-…", "position": 2, "kind": "item", "description": "Remplacement mitigeur thermostatique", "quantity": 1, "unit": "forfait", "unitPriceCents": 18900, "vatRate": 10, "discountPct": 0, "totalHtCents": 18900, "totalVatCents": 1890, "totalTtcCents": 20790 }
],
"pdfUrl": null
}
}curl -X POST "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Idempotency-Key: devis-sdb-tilleuls-01" \
-H "Content-Type: application/json" \
-d '{
"type": "quote",
"clientId": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"lines": [
{ "description": "Pose faïence murale", "quantity": 12, "unitPriceCents": 4500, "vatRate": 10, "unit": "m²" }
]
}'- Remise globale : globalDiscountPct, un pourcentage entre 0 et 100. C'est le seul format accepté — pas de remise en montant. Chaque ligne accepte aussi son propre discountPct (0-100).
- title n'est PAS stocké comme champ du document : il devient une ligne kind section en position 0. La réponse ne contient donc pas de champ title — il ressort dans lines.
- Chaque ligne accepte un kind optionnel : item (défaut, chiffrée), section (en-tête) ou comment (note) — les lignes section et comment n'ont aucun montant. Au moins une ligne item est requise.
- Mode TTC « en dedans » (facturation B2C) : une ligne item peut porter totalTtcCents (montant TTC exact en centimes, entier ≠ 0) + vatRate au lieu de quantity/unitPriceCents — la TVA est alors extraite du TTC, si bien que le TTC facturé est au centime égal à ce que tu encaisses. Un totalTtcCents négatif est accepté pour déduire un acompte déjà facturé, tant que le total du document reste positif et comporte au moins une ligne positive. La ligne renvoie enteredTtcCents (null pour une ligne HT classique).
- Conditions générales de vente : attachCgv est un booléen optionnel qui décide si les CGV de l'entreprise sont annexées en pages supplémentaires du PDF. Omis (ou null), le défaut dépend du type — un devis part AVEC les CGV, une facture et un avoir SANS. Envoie true pour joindre les CGV à une facture, false pour les retirer d'un devis. Le champ ressort tel quel (attachCgv, null quand tu n'as rien tranché) dans la réponse de GET /documents/{documentId}. Les mentions légales obligatoires (pénalités de retard, indemnité de 40 €, TVA) ne dépendent PAS de ce flag : elles sont générées à part et figurent toujours sur le document.
- Un PDF de CGV importé par l'entreprise (au lieu d'un texte) n'est annexé qu'aux documents adressés à un client professionnel (client.type = business).
- Les DATES sont posées par le serveur et ne se pilotent pas : issuedAt est le jour de la création, dueAt (facture) et expiresAt (devis) tombent à +30 jours, paymentTermsText suit. Aucun champ de date n'est accepté au corps — si ton produit a besoin d'une autre échéance, dis-le nous, ne construis pas de contournement.
- pdpStatus est présent dans la réponse et vaut null sur un brouillon : la transmission au réseau ne se joue qu'à l'émission. Ses valeurs et leur rythme sont détaillés dans « Facturation électronique — raccordement et suivi ».
- Le brouillon ne consomme pas de numéro et ne compte pas dans ta facturation : seul un document émis rend le sous-compte actif.
Lister les documents
GET /api/partner/v1/companies/{companyId}/documents
Scope documents:read
Liste paginée des documents du sous-compte. Filtres : type (quote, invoice ou credit_note), status (draft, sent, accepted, rejected, expired, partially_paid, paid, late ou cancelled), limit (1-100, défaut 50), offset.
{
"documents": [
{
"id": "7c9e4b2a-6d5f-4a1b-8e3c-9f0d1a2b3c4d",
"type": "quote",
"status": "sent",
"number": "D-20260709-0001",
"clientId": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"issuedAt": "2026-07-09",
"dueAt": null,
"expiresAt": "2026-08-08",
"totalHtCents": 72900,
"totalVatCents": 7290,
"totalTtcCents": 80190,
"globalDiscountPct": null,
"creditNoteOfId": null,
"createdAt": "2026-07-09T10:04:00.000Z",
"updatedAt": "2026-07-09T10:12:00.000Z"
}
],
"total": 1
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents?type=quote&status=sent" \
-H "Authorization: Bearer $BILLIES_API_KEY"- Il n'existe pas de statut signed : un devis signé passe en accepted.
- Le format du numéro suit les réglages de numérotation du sous-compte (par défaut : préfixe D pour les devis, F pour les factures, A pour les avoirs, date du jour puis compteur — ex. D-20260709-0001). number vaut null tant que le document est en brouillon.
Détail d'un document
GET /api/partner/v1/companies/{companyId}/documents/{documentId}
Scope documents:read
Document complet : lignes, totaux, statut, numéro. Si le document est émis, pdfUrl contient une URL signée temporaire vers le PDF (sinon null). pdpStatus est l'état de la facture SUR LE RÉSEAU de facturation électronique — à ne pas confondre avec status, qui est son état commercial.
{
"document": {
"id": "7c9e4b2a-6d5f-4a1b-8e3c-9f0d1a2b3c4d",
"type": "quote",
"status": "sent",
"number": "D-20260709-0001",
"clientId": "b8e64c1d-5a3f-4e2b-9c7d-1f0a2b3c4d5e",
"issuedAt": "2026-07-09",
"dueAt": null,
"expiresAt": "2026-08-08",
"totalHtCents": 72900,
"totalVatCents": 7290,
"totalTtcCents": 80190,
"globalDiscountPct": null,
"creditNoteOfId": null,
"createdAt": "2026-07-09T10:04:00.000Z",
"updatedAt": "2026-07-09T10:12:00.000Z",
"notes": "Acompte de 30 % à la signature.",
"paymentTermsText": "Paiement sous 30 jours",
"pdpStatus": null,
"attachCgv": true,
"lines": [
{ "id": "1a2b3c4d-…", "position": 0, "kind": "section", "description": "Rénovation salle de bain", "quantity": 0, "unit": "u", "unitPriceCents": 0, "vatRate": 0, "discountPct": 0, "totalHtCents": 0, "totalVatCents": 0, "totalTtcCents": 0 },
{ "id": "2b3c4d5e-…", "position": 1, "kind": "item", "description": "Pose faïence murale", "quantity": 12, "unit": "m²", "unitPriceCents": 4500, "vatRate": 10, "discountPct": 0, "totalHtCents": 54000, "totalVatCents": 5400, "totalTtcCents": 59400 },
{ "id": "3c4d5e6f-…", "position": 2, "kind": "item", "description": "Remplacement mitigeur thermostatique", "quantity": 1, "unit": "forfait", "unitPriceCents": 18900, "vatRate": 10, "discountPct": 0, "totalHtCents": 18900, "totalVatCents": 1890, "totalTtcCents": 20790 }
],
"pdfUrl": "https://…/D-20260709-0001.pdf?token=…"
}
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/7c9e4b2a-…" \
-H "Authorization: Bearer $BILLIES_API_KEY"- pdpStatus, la liste COMPLÈTE — huit valeurs plus null, prévois-les toutes dans ton switch : null (jamais passé par la facturation électronique : tous les devis, et toute facture d'un sous-compte qui ne l'a pas activée), submitted (déposée), validated (acceptée en forme), delivered (remise au destinataire — la bascule légale), rejected (refus du réseau sur le fond), paid (signal de paiement reçu par le réseau, rare), error (échec de transport, rattrapable), unknown et retrying.
- unknown et retrying ne viennent pas du réseau mais de ce que NOUS savons de la transmission, et c'est pour ça qu'on les oublie. unknown : le réseau a répondu sans identifiant de suivi exploitable — impossible de dire si la facture y est. C'est le seul état sans recours automatique, parce que l'API du réseau n'a pas de clé d'idempotence : un renvoi « au cas où » créerait une deuxième facture légale sous un numéro déjà brûlé. retrying : une re-soumission est en cours, l'état définitif arrive dans la minute.
- Cet état évolue de façon DIFFÉRÉE : un robot Billies relit le statut des factures non terminales toutes les heures. Ne fais donc jamais dépendre ta réponse HTTP à l'utilisateur de pdpStatus juste après l'émission — affiche-le, rafraîchis-le plus tard, ou lis GET …/documents/{id}/pdp qui porte en plus le motif de refus.
Supprimer un brouillon
DELETE /api/partner/v1/companies/{companyId}/documents/{documentId}
Scope documents:write
Supprime un document en brouillon. Un document émis n'est jamais supprimable (409 conflict) : c'est une exigence légale française — passe par un avoir.
HTTP/1.1 204 No Contentcurl -X DELETE "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/7c9e4b2a-…" \
-H "Authorization: Bearer $BILLIES_API_KEY"Documents — émission et suites
L'émission est le point de bascule : le document reçoit son numéro légal séquentiel, son PDF est généré, et il devient immuable. Tout ce qui suit — signature, paiement, avoir — s'applique à un document émis.
Émettre un document
POST /api/partner/v1/companies/{companyId}/documents/{documentId}/issue
Scope documents:issue · en-tête Idempotency-Key obligatoire
Attribue le numéro légal, génère le PDF et passe le document en sent. Aucun email n'est envoyé : c'est ton logiciel qui présente le document à l'utilisateur final. C'est cet appel qui rend le sous-compte actif pour le mois en cours. Le bloc einvoice dit ce qu'il est advenu de la facturation électronique : c'est la SEULE façon de distinguer une facture réellement transmise d'un PDF ordinaire — le code HTTP, lui, est 200 dans les deux cas.
{
"id": "7c9e4b2a-6d5f-4a1b-8e3c-9f0d1a2b3c4d",
"number": "DEV-2026-0042",
"status": "sent",
"pdfUrl": "https://billies.fr/…/DEV-2026-0042.pdf?token=…",
"einvoice": {
"pdpStatus": "submitted",
"pdpSubmissionId": "1593",
"pdpStatusMessage": null,
"pdpTransmitted": true,
"pdpSandbox": false,
"quotaExhausted": false,
"facturXXmlAvailable": true
}
}curl -X POST "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/7c9e4b2a-…/issue" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Idempotency-Key: emission-devis-sdb-01"- Rejouer l'appel avec la même Idempotency-Key renvoie la même réponse : jamais deux numéros pour un retry réseau. Seuls les succès (2xx) sont mémorisés — après une erreur, corrige et rejoue la même clé.
- Facturation électronique : la facture part au format Factur-X sur le réseau quand QUATRE conditions tiennent — einvoiceEnabled actif sur le sous-compte, document de type facture ou avoir, client porteur d'un SIRET, et sous-compte raccordé à sa plateforme (pdpMode: delegated).
- Les trois premières conditions et la quatrième ne se lisent PAS de la même façon, et c'est le piège. Si l'une des trois premières manque, le document ne relève pas de la facturation électronique : einvoice.pdpStatus vaut null, tout va bien, il n'y a rien à faire. Si seul le raccordement manque, le document AURAIT DÛ partir : le Factur-X est généré, aucun crédit n'est consommé, mais einvoice.pdpStatus vaut "error" avec un pdpStatusMessage qui nomme l'absence de raccordement. Ne confonds pas les deux — le second appelle un geste (raccorder, puis POST …/pdp/resubmit), le premier non.
- LIS einvoice.pdpTransmitted, pas le code HTTP. Un échec de transmission ne change NI le statut HTTP (200), NI le statut du document (sent) : la facture est valablement émise, seule la remise au réseau a raté. Sans ce bloc, tu ne peux pas faire la différence — donc tu ne peux rien rattraper. Le rattrapage est POST …/documents/{id}/pdp/resubmit.
- PLAFOND : 150 factures électroniques par mois et par sous-compte. Au 151ᵉ, l'émission RÉUSSIT quand même — PDF ordinaire, aucune transmission, réponse 200 à l'identique — et einvoice.quotaExhausted vaut true, pdpStatus null. C'est le silence le plus coûteux de l'API : surveille ce champ, sinon un sous-compte gros émetteur sort du réseau sans que personne le voie. Le compteur repart au 1ᵉʳ du mois ; pour relever le plafond, écris-nous.
- 403 : soit ta clé n'a pas le scope documents:issue, soit ton PLAFOND MENSUEL de documents (toutes émissions confondues, sous-comptes et comptes connectés) est atteint. Le message distingue les deux.
- 400 : un garde-fou légal a refusé AVANT l'allocation du numéro (aucun numéro brûlé, le brouillon est intact). Le message porte un code FCT-… stable : SIRET, adresse, forme juridique ou régime de TVA manquant côté émetteur, RCS ou capital pour une société, SIRET absent sur un client entreprise.
Récupérer le PDF
GET /api/partner/v1/companies/{companyId}/documents/{documentId}/pdf
Scope documents:read
Renvoie une URL signée valable environ 1 heure vers le PDF du document émis. 409 si le document est encore en brouillon. Regénère une URL à chaque besoin plutôt que de la stocker.
{
"url": "https://billies.fr/…/DEV-2026-0042.pdf?token=…"
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/7c9e4b2a-…/pdf" \
-H "Authorization: Bearer $BILLIES_API_KEY"Créer un lien de signature
POST /api/partner/v1/companies/{companyId}/documents/{documentId}/signature-link
Scope documents:write
Pour un devis émis, génère une page publique de signature en ligne (lecture du devis, acceptation, signature tracée avec horodatage). Tu présentes ce lien dans ton interface ou tu l'envoies toi-même au client final. 409 si le document n'est pas un devis émis.
{
"url": "https://billies.fr/q/9f8e7d6c5b4a3f2e1d0c"
}curl -X POST "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/7c9e4b2a-…/signature-link" \
-H "Authorization: Bearer $BILLIES_API_KEY"- Client avec co-titulaires (coHolders) : la réponse contient en plus un tableau signatories [{ name, url }] — un lien nominatif PAR signataire, et le devis ne passe en accepted qu'à la dernière signature. Distribue chaque lien à la bonne personne ; url reste celui du titulaire principal. Champ absent pour un client mono-titulaire.
Enregistrer un paiement
POST /api/partner/v1/companies/{companyId}/documents/{documentId}/payments
Scope payments:write
Enregistre un règlement reçu sur une facture émise. Quand le cumul des paiements atteint le TTC, la facture passe en paid. Les paiements partiels sont acceptés. method accepte transfer, check, card ou other — cash est refusé (Billies est un logiciel de facturation, il n'enregistre pas d'encaissements en espèces).
{
"amountCents": 80190,
"method": "transfer",
"paidAt": "2026-07-15",
"reference": "VIR-2026-4821"
}{
"document": {
"id": "3a2b1c0d-9e8f-4a5b-8c7d-6e5f4a3b2c1d",
"type": "invoice",
"status": "paid",
"number": "FAC-2026-0117",
"totalTtcCents": 80190
}
}curl -X POST "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/3a2b1c0d-…/payments" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "amountCents": 80190, "method": "transfer", "paidAt": "2026-07-15" }'Créer un avoir
POST /api/partner/v1/companies/{companyId}/documents/{documentId}/credit-note
Scope documents:write
Le seul verbe correctif sur une facture émise : l'avoir annule comptablement la facture d'origine, avec son propre numéro légal et des montants négatifs (il crédite ce que la facture avait débité). Sans corps, il crédite la totalité. Avec un corps { amountCents } (> 0), il crée un avoir PARTIEL : le montant TTC est ventilé au prorata des taux de TVA de la facture, plafonné à ce qu'il reste à créditer. L'avoir est créé et émis en un seul appel.
{
"amountCents": 40000
}{
"creditNote": {
"id": "e4d3c2b1-a0f9-4e8d-9c7b-6a5f4e3d2c1b",
"type": "credit_note",
"status": "sent",
"number": "AV-2026-0007",
"creditNoteOfId": "3a2b1c0d-9e8f-4a5b-8c7d-6e5f4a3b2c1d",
"totalTtcCents": -40000,
"pdfUrl": "https://billies.fr/…/AV-2026-0007.pdf?token=…"
}
}curl -X POST "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/3a2b1c0d-…/credit-note" \
-H "Authorization: Bearer $BILLIES_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "amountCents": 40000 }'- L'avoir est créé ET émis dans le même appel : les règles d'émission s'appliquent donc ici aussi (garde-fous légaux, plafond de 150 factures électroniques par mois et par sous-compte). Un avoir sur une facture partie sur le réseau doit lui aussi y partir, sinon l'administration continue de voir une facture jamais créditée. La réponse porte pdpStatus ; pour le motif de refus et l'identifiant de suivi, lis GET …/documents/{id}/pdp sur l'avoir.
- amountCents absent → avoir total (montants exactement opposés à la facture). amountCents présent → avoir partiel : une ligne par taux de TVA, TVA « en dedans ».
- Plafond : la somme des avoirs d'une facture ne peut pas dépasser son TTC. Un amountCents qui dépasse le restant créditable renvoie 409.
- Idempotency-Key optionnelle mais recommandée : elle évite un double avoir sur un retry réseau.
Facturation électronique — raccordement et suivi
Trois choses se jouent ici, et elles sont distinctes. Le RACCORDEMENT : tant que le sous-compte n'a pas autorisé Billies à agir sur son compte de plateforme de dématérialisation (PDP), rien ne part sur le réseau et rien n'arrive dans sa boîte — la facture est bien émise et parfaitement légale, mais elle ressort avec pdpStatus: "error" et un motif qui dit exactement ça. Le SUIVI : une facture transmise change d'état sur le réseau bien après ton appel d'émission. La RE-SOUMISSION : quand la transmission a échoué, il existe un chemin de rattrapage — étroit et gardé, parce que le réseau n'a aucune clé d'idempotence.
Créer le lien de raccordement PDP
POST /api/partner/v1/companies/{companyId}/pdp/connect-link
Scope companies:write
Rend un lien d'autorisation à durée limitée, que TU présentes à ton utilisateur final : il l'ouvre dans son navigateur, se connecte à sa plateforme de dématérialisation et autorise Billies à émettre et relever ses factures sous SON identité. C'est le prérequis de tout le reste — émission Factur-X sous la bonne identité, réception des factures fournisseurs, e-reporting en mode delegated.
{
"url": "https://…/oauth2/authorize?client_id=…&state=…",
"expiresAt": "2026-07-09T10:14:00.000Z"
}curl -X POST "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/pdp/connect-link" \
-H "Authorization: Bearer $BILLIES_API_KEY"- Pourquoi cet endpoint existe : le propriétaire d'un sous-compte marque grise est un compte technique qui ne peut JAMAIS ouvrir de session sur billies.fr. Il ne peut donc pas passer par l'écran de raccordement du site — ce lien est son seul chemin.
- Le lien est à usage humain : c'est l'utilisateur final qui doit l'ouvrir, dans son navigateur. Ton serveur ne peut pas le suivre à sa place (l'écran d'autorisation appartient à la plateforme).
- Il vit 30 minutes (expiresAt) — le temps de le transmettre et de consentir, pas davantage. Passé ce délai, redemande-en un : ne le stocke pas, ne le mets pas en cache, ne l'envoie pas dans un e-mail qui sera lu demain. Qui détient le lien peut raccorder SON compte de plateforme à CE sous-compte, exactement comme un lien de signature de devis — transmets-le au seul artisan concerné.
- Trois refus possibles, tous définitifs pour l'appel en cours. 403 : le sous-compte est un compte CONNECTÉ (mode /connect) — son propriétaire a sa propre session Billies et raccorde sa plateforme lui-même depuis ses réglages, tu n'as pas la main dessus. 409 : le sous-compte est en mode démonstration (rien de ce qu'il émet ne part, lui faire signer un consentement réel serait un écran mensonger), ou l'instance Billies n'a pas la facturation électronique armée. 500 : le lien n'a pas pu être fabriqué — écris-nous, retenter n'y changera rien.
- Vérifier le résultat : pdpConnected passe à true sur GET /companies/{companyId} et pdpMode y vaut delegated. Tant que pdpMode vaut none, aucune facture ne part sur le réseau et l'inbox du sous-compte n'est pas relevée.
- À quoi ressemble une émission NON raccordée, parce qu'il faut savoir la reconnaître : POST …/issue répond 200 comme d'habitude, le document passe en sent, le Factur-X est bien généré et son XML archivé, et aucun crédit de facture électronique n'est consommé — rien n'a été tenté, rien n'est facturé. Mais le bloc einvoice porte pdpStatus: "error", pdpSubmissionId: null, pdpTransmitted: false, et pdpStatusMessage dit noir sur blanc que le compte n'est raccordé à aucune plateforme.
- Ce n'est PAS null, et la nuance vaut de l'argent : null signifie « ce document ne relève pas de la facturation électronique » (un devis, un client sans SIRET, l'option désactivée) — situation normale, rien à faire. error avec ce motif signifie « ce document AURAIT DÛ partir et n'est pas parti » — il y a un geste à faire. Branche-toi sur pdpStatusMessage, pas seulement sur pdpStatus.
- Le rattrapage existe et il est propre : une fois le compte raccordé, POST …/documents/{id}/pdp/resubmit renvoie la facture telle quelle. C'est même le cas pour lequel ce verbe a été écrit — rien n'ayant été déposé, rien ne peut être doublé.
- Attention au libellé de pdpStatusMessage : il renvoie vers « Réglages > Entreprise », un écran écrit pour l'artisan devant son navigateur. Le propriétaire d'un sous-compte marque grise ne peut jamais ouvrir de session Billies — pour lui le chemin est le lien ci-dessus. Affiche ton propre message plutôt que de recopier celui-là.
- Il existe un troisième pdpMode, platform : un dépôt sous l'identité de la PLATEFORME Billies au lieu de celle du sous-compte. Il est fermé par défaut et ne s'ouvre que sur confirmation écrite de la plateforme de dématérialisation — déposer sous le mauvais SIREN fait refuser la facture, le réseau comparant l'identité du jeton au vendeur déclaré dans le XML. Ne construis rien dessus : delegated est le seul mode sur lequel s'appuyer.
Suivi de transmission d'un document
GET /api/partner/v1/companies/{companyId}/documents/{documentId}/pdp
Scope documents:read
L'état de la facture SUR LE RÉSEAU, séparément de son état commercial (status). Répond pour TOUT document, y compris ceux qui ne relèvent pas de la facturation électronique — pdpStatus vaut alors null, et c'est une information, pas une erreur. pdpStatusMessage porte le motif brut renvoyé par le réseau : c'est la seule chose qui rend un refus corrigeable.
{
"documentId": "3a2b1c0d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"pdpStatus": "rejected",
"pdpSubmissionId": "1593",
"pdpStatusMessage": "fr:213 — destinataire identique à l'émetteur",
"pdpSandbox": false,
"pdpTransmitted": false,
"pdpUpdatedAt": "2026-07-09T11:42:07.000Z",
"facturXXmlUrl": "https://…/F-20260709-0007.factur-x.xml?token=…",
"facturXXmlAvailable": true
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/3a2b1c0d-…/pdp" \
-H "Authorization: Bearer $BILLIES_API_KEY"- Les neuf valeurs de pdpStatus, sans en oublier : null (jamais passé par la facturation électronique), submitted, validated, delivered, rejected, paid, error, unknown, retrying. Détail de chacune dans « Documents — brouillons », sur GET …/documents/{documentId}.
- L'état est DIFFÉRÉ : un robot Billies relit le statut des factures non terminales toutes les heures. Juste après l'émission, pdpStatus vaut le plus souvent submitted — ce n'est pas un échec, c'est le début du parcours.
- États terminaux : delivered (remise au destinataire, c'est la bascule légale), rejected (refus du réseau) et paid (signal de paiement, rare). Ils ne sont plus ré-interrogés.
- unknown : le réseau a répondu sans identifiant de suivi exploitable. La facture n'est pas suivie et ne peut PAS être renvoyée — le réseau n'a aucune clé d'idempotence, un rejeu créerait une seconde facture sous un numéro déjà brûlé. Cas à traiter à la main avec le support.
- pdpTransmitted résume les quatre statuts où le réseau a PRIS EN CHARGE la facture (submitted, validated, delivered, paid). rejected en est exclu à dessein : le réseau a dit non et ne l'a jamais remise — le compter comme transmis ferait croire l'obligation remplie alors qu'il reste un avoir à émettre.
- pdpUpdatedAt est l'horodatage de dernière écriture sur le document, pas un horodatage propre au réseau : c'est le repère le plus proche dont on dispose, il bouge à l'émission comme à la re-soumission. Ne t'en sers pas comme d'une date de remise légale.
- facturXXmlUrl est l'URL signée du XML CII (~1 h, à ne pas stocker) ; elle vaut null quand le XML n'existe pas, mais AUSSI quand sa signature a échoué. facturXXmlAvailable, lui, dit sans ambiguïté si le fichier existe — c'est ce champ-là qu'il faut lire pour décider d'afficher un bouton de téléchargement.
- pdpSandbox=true : le document est parti sous les identités du bac à sable, pas sous celles du sous-compte. Ne le présente jamais comme une transmission réelle.
Re-soumettre une facture au réseau
POST /api/partner/v1/companies/{companyId}/documents/{documentId}/pdp/resubmit
Scope documents:issue
Rattrapage d'une transmission qui a échoué en TRANSPORT (délai dépassé, 5xx, jeton expiré) ou d'un refus de fond corrigé depuis. Le XML est REGÉNÉRÉ depuis l'état actuel du document — c'est ce qui capte les correctifs de format posés entre-temps. Aucun numéro n'est alloué. Côté crédit de facture électronique : un échec (error, rejected) rend le crédit de la tentative précédente, et la relance en consomme un nouveau seulement si elle aboutit — rendu à son tour si elle échoue encore. Un document émis avant fin août 2026, dont le crédit n'a jamais été rendu, se relance gratuitement.
{
"documentId": "3a2b1c0d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"resubmitted": true,
"pdpStatus": "submitted",
"pdpSubmissionId": "1741",
"pdpStatusMessage": null,
"pdpSandbox": false,
"pdpTransmitted": true,
"pdpUpdatedAt": "2026-07-09T14:03:22.000Z",
"facturXXmlUrl": null,
"facturXXmlAvailable": true
}curl -X POST "https://billies.fr/api/partner/v1/companies/1f6f9c2e-…/documents/3a2b1c0d-…/pdp/resubmit" \
-H "Authorization: Bearer $BILLIES_API_KEY"- LIS resubmitted, pas le code HTTP. 200 veut seulement dire que la transmission a été TENTÉE : elle peut avoir de nouveau échoué, et ce n'est ni ton erreur ni un incident serveur. resubmitted vaut true quand le réseau a rendu un identifiant de suivi, false sinon — et dans ce cas un champ message dit ce qui s'est passé, pendant que pdpStatus et pdpStatusMessage portent l'état à jour du document. Un 200 avec resubmitted: false n'est pas à rejouer en boucle.
- Le corps est celui de GET …/documents/{id}/pdp, relu APRÈS la tentative, avec documentId et resubmitted en plus. facturXXmlUrl y vaut toujours null : signer l'URL est un aller-retour de plus qu'un appel d'écriture n'a pas à payer. Lis facturXXmlAvailable, et demande l'URL au GET si tu en veux une.
- 409 quand une soumission VIVANTE existe déjà (submitted, validated, delivered ou paid) : renvoyer ferait deux factures sur le réseau légal, sans moyen d'en retirer une. Seul un rejet est mort, donc rejouable.
- 409 aussi sur un état unknown, et sur un rejet dont le XML régénéré serait identique à celui qui a été refusé — rien n'a changé, le renvoi échouerait à l'identique. Une exception : le rejet pour destinataire absent de l'annuaire (pdpStatusMessage du type « receiver address … does not exist in peppol directory »). Là, rien n'est à corriger dans le document — c'est le client qui n'a pas encore choisi sa plateforme — et le renvoi tel quel est accepté, à faire quand il est raccordé. Ce rejet n'appelle PAS d'avoir : la facture est juste.
- 409 si une donnée ENGAGEANTE a bougé depuis l'émission : le XML est régénéré depuis l'état ACTUEL du sous-compte et du client, et s'il contredit le PDF déjà entre les mains du client, on refuse. Sont surveillés le numéro du document, les totaux HT / TVA / TTC, la date d'émission et l'échéance, le SIRET, la raison sociale, le code postal et la ville de l'émetteur, les mêmes du côté client, et l'IBAN. Le message nomme le champ qui a bougé. Le remède est alors l'avoir, pas le renvoi.
- 409 enfin sur les refus de cadre : le document n'est ni une facture ni un avoir, il est en brouillon, annulé ou supprimé, il a été émis en mode test (pdpSandbox), son XML n'est plus en Storage, ou l'instance Billies n'a pas la facturation électronique armée. Dans TOUS les cas de 409, rien n'est parti et l'état du document n'a pas bougé d'un octet.
- Deux appels simultanés : un seul passe (verrou de ligne Postgres), l'autre reçoit 409. Une tentative interrompue en plein vol reste reprenable au bout de 15 minutes — avant ça, la première peut encore être en route et on refuse.
- Pas d'Idempotency-Key sur cet endpoint, et c'est délibéré : le garde-fou anti double-envoi est dans les verrous ci-dessus, qui protègent aussi bien un POST rejoué qu'un double-clic. Mémoriser la réponse ferait pire — une tentative ratée resterait figée sous sa clé, et tu ne pourrais plus jamais relancer cette facture.
Mode connecté
Les connexions accordées par des utilisateurs Billies à ton logiciel via le flux /connect (voir la section Mode connecté plus haut). Chaque lien donne accès à une seule company — celle de l'utilisateur — avec les scopes qu'il a consentis. Une fois le companyId récupéré, tous les endpoints /companies/{companyId}/… fonctionnent à l'identique.
Lister tes connexions
GET /api/partner/v1/links
Scope companies:read
Liste paginée des comptes Billies connectés à ton logiciel, les plus récents d'abord. Paramètres : limit (défaut 50) et offset. Chaque lien expose la company accessible (companyId), les scopes consentis par l'utilisateur, et revokedAt — non null si l'utilisateur a coupé l'accès depuis ses réglages Billies.
{
"links": [
{
"id": "7c2d51e4-3b8f-4a19-b6c0-9e4d2f7a8c31",
"companyId": "1f6f9c2e-8d1a-4b6e-9f3d-2a7c5e8b1d40",
"scopes": [
"companies:read",
"clients:read",
"clients:write",
"documents:read",
"documents:write",
"documents:issue",
"received-invoices:read",
"payments:write"
],
"state": "n-4f7a2b9c",
"authorizedAt": "2026-07-09T14:03:00.000Z",
"revokedAt": null
}
],
"total": 1
}curl "https://billies.fr/api/partner/v1/links?limit=20&offset=0" \
-H "Authorization: Bearer $BILLIES_API_KEY"- Un lien révoqué (revokedAt non null) ferme l'accès : les endpoints /companies/{companyId}/… répondent alors 404, comme si le compte n'existait pas.
- Si l'utilisateur refait le flux /connect, le même lien est réactivé (revokedAt repasse à null, authorizedAt est rafraîchi) — pas de doublon.
Vérifier une connexion après le retour
GET /api/partner/v1/links?state={state}
Scope companies:read
L'étape serveur du flux /connect : quand l'utilisateur revient chez toi, appelle cet endpoint avec le state que tu avais généré avant la redirection. C'est la source de vérité — le billies_company_id présent dans l'URL de retour n'est que de l'UX et ne doit jamais être cru tel quel. Stocke le companyId renvoyé : c'est lui que tu utilises ensuite sur tous les endpoints /companies/{companyId}/….
{
"link": {
"id": "7c2d51e4-3b8f-4a19-b6c0-9e4d2f7a8c31",
"companyId": "1f6f9c2e-8d1a-4b6e-9f3d-2a7c5e8b1d40",
"scopes": [
"companies:read",
"clients:read",
"clients:write",
"documents:read",
"documents:write",
"documents:issue",
"received-invoices:read",
"payments:write"
],
"state": "n-4f7a2b9c",
"authorizedAt": "2026-07-09T14:03:00.000Z",
"revokedAt": null
}
}curl "https://billies.fr/api/partner/v1/links?state=n-4f7a2b9c" \
-H "Authorization: Bearer $BILLIES_API_KEY"- 404 si aucun lien ne porte ce state : l'utilisateur n'a pas terminé le flux, le state ne vient pas de chez toi, OU l'autorisation date de plus de 24 h (le state expire — anti-rejeu). Vérifie le state juste après le retour ; passé ce délai, retrouve le compte via GET /links (liste).
- Le lien est renvoyé même si l'utilisateur a révoqué entre-temps (revokedAt non null) — à toi de le traiter comme inactif.
- Génère un state opaque et unique par tentative de connexion (nonce), stocké côté serveur avant la redirection : c'est lui qui relie le retour à la bonne session chez toi.
E-reporting (ventes aux particuliers)
Journal des données de transaction et de paiement B2C destinées à l'administration (réforme e-facturation, obligatoire TPE/PME au 1ᵉʳ septembre 2027). La mise en file est automatique dès l'émission ou l'encaissement d'une facture à un particulier — aucun appel à faire de ton côté. Ce que le serveur fait ensuite de cette file dépend d'un réglage global (mode) que l'API expose mais ne pilote pas : lis-le avant de promettre quoi que ce soit à ton utilisateur. Inclus, sans surcoût par déclaration.
Journal e-reporting d'un sous-compte
GET /api/partner/v1/companies/{companyId}/ereporting
Scope documents:read
Lecture seule : l'état d'activation et les déclarations du sous-compte, avec leur statut de transmission (pending → sending → sent, ou error). Une entrée transaction est créée à l'émission d'une facture ou d'un avoir à un particulier français (montants HT/TVA agrégés par taux — jamais le nom du client) ; une entrée payment à chaque encaissement (TTC ventilé). mode dit ce que le serveur fait RÉELLEMENT de cette file : lis-le avant d'interpréter les statuts. Filtres : status (pending, sending, sent, error) et kind (transaction, payment) ; pagination limit (≤ 100) et offset.
{
"mode": "live",
"ereportingEnabled": true,
"pdpConnected": true,
"pdpMode": "delegated",
"entries": [
{
"id": "a1b2c3d4-…",
"documentId": "d4c3b2a1-…",
"documentNumber": "F-20260710-0012",
"kind": "transaction",
"status": "pending",
"providerId": null,
"errorMessage": null,
"date": "2026-07-10",
"categoryCode": "TPS1",
"amounts": { "htCents": 90000, "vatCents": 18000, "ttcCents": 108000 },
"createdAt": "2026-07-10T14:02:11.000Z",
"sentAt": null
},
{
"id": "e5f6a7b8-…",
"documentId": "d4c3b2a1-…",
"documentNumber": "F-20260710-0012",
"kind": "payment",
"status": "pending",
"providerId": null,
"errorMessage": null,
"date": "2026-07-10",
"categoryCode": null,
"amounts": { "htCents": null, "vatCents": null, "ttcCents": 50000 },
"createdAt": "2026-07-10T15:11:40.000Z",
"sentAt": null
}
],
"limit": 50,
"offset": 0
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-8a4b-4c5d-9e0f-2a3b4c5d6e7f/ereporting?status=pending&kind=transaction" \
-H "Authorization: Bearer $BILLIES_API_KEY"- mode est un réglage SERVEUR, hors de portée de l'API : tu le lis, tu ne le changes pas. off (valeur par défaut) = la file est inerte, rien ne part et rien ne bouge. dry_run = Billies COMPTE ce qui partirait, sans rien transmettre : les entrées restent pending, providerId et sentAt restent null — ce n'est pas une panne. live = transmission réelle, et c'est le seul mode où une entrée atteint sent. Depuis le 30/08/2026 la production tourne en live : une entrée d'une société opt-in ET raccordée atteint bien sent. Attention à ce que tu promets pour autant — une société qui n'a pas raccordé son compte est SAUTÉE (skippedNotConnected), jamais transmise sous une autre identité.
- Rien à déclencher : l'émission d'une facture B2C (POST /documents + issue) et l'enregistrement d'un paiement (POST /payments) alimentent la file automatiquement. Cet endpoint sert à VÉRIFIER, pas à déclarer.
- Périmètre : factures et avoirs aux particuliers français uniquement. Les factures B2B (client avec SIRET) passent par le flux Factur-X, jamais par l'e-reporting — aucun double comptage possible. Un document en autoliquidation BTP en est également exclu : il est B2B par nature.
- ereportingEnabled=false ou pdpConnected=false → aucune donnée ne part. pdpMode indique le raccordement effectif. delegated : le sous-compte a autorisé Billies sur SON compte de plateforme (cf. POST …/pdp/connect-link) — c'est le cas nominal, et le seul sur lequel construire. platform : repli sous l'identité de la plateforme, qui dépend d'un réglage serveur fermé par défaut ET reste refusé au moment de transmettre si le SIRET du sous-compte ne correspond pas à celui que porte le jeton — ce qui est le cas de tous tes sous-comptes. Ne compte pas dessus. none : rien ne part.
- categoryCode (transactions) : TPS1 prestations de services, TNT1 non taxable (TVA 0), norme AFNOR XP Z12-013. Les montants sont en centimes, négatifs pour un avoir ou une annulation d'encaissement.
- status=error : la déclaration a été refusée (définitif) — le détail est dans errorMessage. status=sending : transmission en cours de vérification, ne pas rejouer. status=pending : en file. Normal et transitoire en live ; état PERMANENT tant que mode vaut off ou dry_run, ou tant que la société n'a pas raccordé son compte à la plateforme.
Factures reçues (réception e-facturation)
Les factures électroniques que le sous-compte REÇOIT de ses fournisseurs via le réseau de dématérialisation (réception obligatoire pour toutes les entreprises au 1ᵉʳ septembre 2026). Billies relève l'inbox de chaque sous-compte raccordé — rien à déclencher de ton côté, MAIS il y a un prérequis : le sous-compte doit avoir raccordé son compte de plateforme. Tant qu'il ne l'a pas fait, il n'a pas d'inbox à relever et cette liste reste vide, sans erreur. Le raccordement se fait avec POST /companies/{companyId}/pdp/connect-link, qui rend un lien d'autorisation à faire ouvrir par ton utilisateur final. Ces deux endpoints te permettent ensuite d'afficher et de servir les factures reçues dans ton back-office.
Factures reçues d'un sous-compte
GET /api/partner/v1/companies/{companyId}/received-invoices
Scope received-invoices:read
Liste des factures reçues, plus récentes d'abord. read indique si l'utilisateur l'a déjà ouverte sur billies.fr — cet appel ne modifie JAMAIS cet état (lecture non destructive, le badge de l'utilisateur reste intact). Filtre : unread=true|false (autre valeur → 400) ; pagination limit (≤ 100) et offset.
{
"einvoiceEnabled": true,
"pdpConnected": true,
"invoices": [
{
"id": "9c1e4f7a-5b2d-4e8f-a1c3-7d6e9f0b2a41",
"status": "delivered",
"senderName": "SANITAIRES DU RHÔNE",
"senderSiret": "80123456700028",
"receivedAt": "2026-07-09T08:14:52.000Z",
"read": false,
"createdAt": "2026-07-09T09:00:03.000Z"
}
],
"limit": 50,
"offset": 0
}curl "https://billies.fr/api/partner/v1/companies/1f6f9c2e-8a4b-4c5d-9e0f-2a3b4c5d6e7f/received-invoices?unread=true" \
-H "Authorization: Bearer $BILLIES_API_KEY"- Scope dédié received-invoices:read : les connexions (mode connecté) autorisées AVANT son introduction ne l'ont pas — l'appel répond 403 « non consenti » tant que l'utilisateur n'a pas ré-autorisé ton application via /connect (l'écran de consentement mentionne désormais les factures reçues). Même chose pour une clé API créée avant : recrée une clé.
- Prérequis côté sous-compte : facturation électronique activée (einvoiceEnabled) ET compte raccordé à la plateforme (pdpConnected) — les deux champs sont renvoyés pour que tu puisses guider l'utilisateur. Sans raccordement, rien n'est relevé et la liste reste vide, sans erreur : c'est le cas de départ de TOUT sous-compte que tu viens de créer. Le lien de raccordement s'obtient avec POST /companies/{companyId}/pdp/connect-link ; l'utilisateur final doit l'ouvrir lui-même. Les factures déjà reçues avant une déconnexion restent listées.
- pdpConnected n'a PAS le même sens ici que sur GET /companies. Ici, il vaut vrai uniquement quand le sous-compte a un accès délégué à sa propre plateforme — c'est le seul raccordement avec lequel une inbox existe. Sur /companies (et sur le journal e-reporting), le même nom couvre aussi le repli sous l'identité de la plateforme, qui TENTE l'émission mais n'ouvre aucune boîte de réception. Pour lever le doute, lis pdpMode sur GET /companies : seul delegated ouvre la réception.
- L'inbox est relevée périodiquement par Billies : une facture apparaît ici quelques minutes à quelques heures après son dépôt sur le réseau, pas en temps réel.
- senderName et senderSiret sont renseignés AU MIEUX, et une seule fois : au moment où la facture entre dans la boîte, Billies télécharge son XML et y lit le tiers émetteur (nom + SIREN ou SIRET). Trois raisons légitimes de rester à null — un dépôt en PDF Factur-X (le XML est enfermé dans le PDF, on ne le déplie pas), un fichier au-delà de 2 Mo, une forme de XML qu'on ne sait pas lire — plus un budget par passage (25 lectures, ou 20 secondes) : au-delà, les factures du même passage gardent leurs deux champs vides. Rien ne repasse ensuite les combler, et les factures entrées avant la mise en service de cette lecture restent nulles pour toujours.
- Conséquence pratique : traite null comme un cas NORMAL et pas comme une anomalie. Prévois un repli d'affichage, ne branche aucun rapprochement comptable automatique sur senderSiret, et télécharge la facture quand tu as besoin de son émetteur avec certitude.
Télécharger une facture reçue
GET /api/partner/v1/companies/{companyId}/received-invoices/{receivedInvoiceId}/download
Scope received-invoices:read
Renvoie le document lui-même en binaire (Content-Type: application/pdf pour un Factur-X, application/xml ou text/xml pour un CII pur — selon ce que le fournisseur a déposé), avec un Content-Disposition: attachment. Contrairement à …/documents/{id}/pdf qui renvoie une URL signée, la réponse est ici le flux binaire : le document vit chez la plateforme de dématérialisation, pas dans le Storage Billies.
— binaire —
Content-Type: application/pdf
Content-Disposition: attachment; filename="facture-recue-9c1e4f7a-….pdf"curl -OJ "https://billies.fr/api/partner/v1/companies/1f6f9c2e-8a4b-4c5d-9e0f-2a3b4c5d6e7f/received-invoices/9c1e4f7a-5b2d-4e8f-a1c3-7d6e9f0b2a41/download" \
-H "Authorization: Bearer $BILLIES_API_KEY"- Le téléchargement est fait côté serveur Billies avec l'accès de la société — son jeton plateforme ne transite jamais par toi. Ne mets pas cette URL en cache : sers le flux à ton utilisateur ou re-télécharge à la demande.
- 409 = la facture est bien chez nous, mais on ne peut PAS aller la chercher, et retenter n'y changera rien. Un en-tête x-billies-reason nomme le cas, pour que ton code route dessus sans lire le français : pdp_not_connected (le sous-compte n'a jamais raccordé sa plateforme — demande un lien avec POST /companies/{companyId}/pdp/connect-link et fais-le ouvrir par ton utilisateur final), pdp_delegation_expired (le raccordement a existé, l'accès est révoqué ou périmé — même remède, il faut reconnecter), reception_unavailable (cette instance Billies ne sert pas la réception ; tu ne le verras pas en production).
- 404 : l'id n'existe pas, n'appartient pas à ce sous-compte (isolation stricte par société), ou la plateforme ne rend plus le document. 500 : uniquement ce qui peut passer tout seul (panne, 5xx du réseau) — c'est le seul code sur lequel il est utile de retenter, et jamais une preuve que la facture n'existe pas.
Consommation
Suis en temps réel ce que ton abonnement va facturer : sous-comptes actifs du mois et documents émis. Un sous-compte est actif dès qu'il a émis au moins un document dans le mois.
Consommation du mois
GET /api/partner/v1/usage
Scope usage:read
Paramètre month au format YYYY-MM (défaut : mois en cours). bill.mode reflète ton offre (flat, per_document ou custom) et bill.amountCents est l'estimation totale du mois. En offre à l'acte, la facturation porte sur billableDocuments (factures ET avoirs — un avoir consomme un numéro légal et un PDF, il compte comme un document émis). documentsIssued/creditNotesIssued détaillent les deux. Les documents des comptes connectés (linkedDocuments) ne te sont jamais facturés.
{
"month": "2026-07",
"activeCompanies": 14,
"documentsIssued": 82,
"ereportingDeclarationsSent": 31,
"creditNotesIssued": 5,
"billableDocuments": 87,
"linkedDocuments": 0,
"pricingMode": "flat",
"includedCompanies": 10,
"pricePerExtraCompanyCents": 500,
"bill": {
"mode": "flat",
"baseCents": 9900,
"extraCompanies": 4,
"extraAmountCents": 2000,
"amountCents": 11900
}
}curl "https://billies.fr/api/partner/v1/usage?month=2026-07" \
-H "Authorization: Bearer $BILLIES_API_KEY"- Offre Intégrée (mode flat) : le barème est dégressif par tranche de sous-comptes actifs — 5 € du 11ᵉ au 100ᵉ, 4 € du 101ᵉ au 500ᵉ, 3 € au-delà. includedCompanies (10) et pricePerExtraCompanyCents ne sont renseignés qu'en flat.
- Offre à l'acte (mode per_document) : bill.amountCents est calculé sur billableDocuments (factures + avoirs), pas sur documentsIssued seul. Réconcilie ta facturation sur billableDocuments.
- ereportingDeclarationsSent est purement INFORMATIF : les déclarations e-reporting (ventes aux particuliers) ne sont jamais facturées — ni au sous-compte actif, ni à l'acte. Le compteur porte sur les déclarations transmises dans le mois pour tes sous-comptes.
- Ce que cet endpoint ne dit PAS : chaque sous-compte a son propre plafond de 150 factures ÉLECTRONIQUES par mois, et il n'apparaît nulle part ici. Au-delà, l'émission continue de réussir — PDF ordinaire, aucune transmission au réseau, réponse 200 inchangée. La seule alerte est le champ einvoice.quotaExhausted de la réponse d'émission : compte-le de ton côté, sous-compte par sous-compte. Ce plafond n'a rien à voir avec la facturation de ton offre, qui porte sur billableDocuments.
Règles métier à connaître
L'API applique le droit français de la facturation. Quatre règles structurent tout le reste — les connaître t'évite la plupart des 409.
Une facture émise est immuable
C'est une obligation légale française, pas un choix d'API : une facture émise ne se modifie pas et ne se supprime pas. Le seul verbe correctif est l'avoir (POST …/credit-note), qui annule la facture d'origine avec son propre numéro — puis tu refactures proprement. Toute mutation sur un document émis renvoie 409 conflict.
Cycle de vie : brouillon → émis → accepté / payé
Un document naît en draft: modifiable et supprimable à volonté, sans numéro. L'émission le fige en sent. Ensuite, un devis signé via le lien de signature passe accepted(il n'y a pas de statut signed), une facture passe partially_paid puis paid quand les paiements enregistrés couvrent le TTC.
La numérotation est séquentielle et légale
Le numéro est attribué au moment de l'émission, dans une séquence chronologique continue et sans trou, propre à chaque sous-compte. Tu ne peux ni choisir un numéro, ni en réserver un à l'avance — c'est ce qui rend les documents opposables en cas de contrôle.
E-facturation via plateforme de dématérialisation
Quand la e-facturation est activée sur un sous-compte et qu'il a raccordé son compte de plateforme, ses factures B2B partent au format Factur-X et sont transmises via la plateforme de dématérialisation à laquelle Billies est raccordé — sans rien changer à tes appels : c'est le même POST …/issue. Le raccordement, lui, n'a rien d'automatique : il passe par POST …/pdp/connect-link, un lien que ton utilisateur final ouvre lui-même.
Trois conditions supplémentaires, silencieuses si elles manquent : le client doit porter un SIRET, le document doit être une facture ou un avoir (jamais un devis), et le sous-compte ne doit pas avoir dépassé ses 150 factures électroniques du mois. Dans tous ces cas l'émission réussit — 200, PDF valide, numéro légal — mais rien ne part sur le réseau. Le bloc einvoice de la réponse est le seul endroit où ça se voit.
À voir aussi
Prêt à facturer depuis ton logiciel ?
Active l'API dans tes réglages, crée ta clé, émets ton premier devis dans l'heure. À l'acte (0 € par mois, 0,50 € par document) ou intégré (99 €/mois, 10 sous-comptes actifs inclus) — sans engagement.
Un sous-compte qui n'émet rien ne coûte rien
