Ce guide refait, en curl, ce que l'écran de facturation fait à la souris. Chaque appel est le même que dans Démarrer avec l'API, avec la même clé et la même base URL.
BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...1. Prérequis
Prenez dans GET /me l'id d'une société dont modules.invoicing est activé, et gardez-le sous la main :
COMPANY=<companyId>2. Créer le client
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "name": "Muster AG", "email": "billing@muster.example",
"address": { "kind": "swiss", "country": "CH", "street": "Rue du Lac",
"streetNumber": "12", "postalCode": "1700", "city": "Fribourg" },
"details": { "kind": "company", "contactPersons": [] } }' \
$BASE/companies/$COMPANY/clientsLa réponse 201 contient le client avec son id. name, address et details sont obligatoires : details.kind vaut company (avec contactPersons, une liste qui peut être vide, et en option ideNumber et vatNumber) ou individual. Un client appartient à une société : utiliser sur une facture le client d'une autre société répond 403 ACCESS_DENIED. En Business, les clients sont illimités ; 403 CLIENT_LIMIT_REACHED n'apparaît que si votre abonnement ne couvre pas le module Facturation.
Importer des clients par lot
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "rows": [
{ "name": "Muster AG", "email": "billing@muster.example",
"address": { "kind": "swiss", "country": "CH", "city": "Fribourg" },
"details": { "kind": "company", "contactPersons": [] } },
{ "name": "Anna Beispiel",
"address": { "kind": "foreign", "country": "FR", "city": "Annemasse" },
"details": { "kind": "individual" } }
] }' \
$BASE/companies/$COMPANY/clients/batchLe lot accepte de 1 à 200 clients et il est atomique : tout est écrit ou rien ne l'est, et un lot qui dépasserait le quota de clients est refusé en entier (403 CLIENT_LIMIT_REACHED). Une ligne dont l'e-mail existe déjà dans la société, ou apparaît plus haut dans le lot, est ignorée sans erreur ; une ligne sans e-mail n'est jamais considérée comme un doublon. La réponse 201 donne les deux compteurs, inserted et skippedDuplicates. La lecture du fichier (CSV, tableur) reste de votre côté.
3. Choisir un modèle
curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/invoice-templatesLa liste des modèles n'est pas paginée et ne prend aucun paramètre. Celui qui porte isDefault: true est le modèle sur lequel une facture se rabat quand vous ne précisez rien : templateId est facultatif à la création. Dans le JSON, hasLogo est en lecture seule : le logo passe par sa propre route, en octets bruts (PNG ou JPEG, 2 Mo au maximum).
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: image/png" \
--data-binary @logo.png $BASE/companies/$COMPANY/invoice-templates/<templateId>/logoPUT /companies/{companyId}/logo fait de même pour le logo de la société (rôle propriétaire ou admin requis), que le logo d'un modèle remplace sur ses documents. DELETE sur ces deux routes retire le logo.
4. Créer la facture
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "clientId": "<clientId>", "lang": "fr", "currency": "CHF",
"issueDate": "2026-03-01", "dueDate": "2026-03-31",
"items": [{ "description": "Design", "quantity": 2, "unitPrice": 150, "vatRate": 8.1 }] }' \
$BASE/companies/$COMPANY/invoicesclientId, lang, issueDate, dueDate et currency sont obligatoires ; templateId, discount, shippingCosts, notes, paymentTerms, showQrBill et items sont facultatifs. Le numéro vient du compteur de la société et la facture démarre en brouillon (draft).
En Business, les factures sont illimitées et les devises autres que CHF sont ouvertes. 403 INVOICE_LIMIT_REACHED et 403 CURRENCY_PRO_ONLY n'apparaissent que si votre abonnement ne couvre pas le module Facturation : ce sont alors les limites du plan Starter.
Réutiliser le catalogue de produits
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "name": "Conseil design", "unitPrice": 150, "currency": "CHF", "vatRate": 8.1,
"unit": "h", "reference": "DES-01" }' \
$BASE/companies/$COMPANY/products
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "name": "Pack lancement", "items": [{ "productId": "<productId>", "quantity": 2 }] }' \
$BASE/companies/$COMPANY/product-listsUn produit exige name, unitPrice, currency et vatRate ; description, unit et reference sont facultatifs. Une liste garde ses produits dans l'ordre du tableau (500 au maximum, chacun une seule fois, sinon 422 PRODUCT_LIST_DUPLICATE_PRODUCT). Les deux ressources se listent avec pagination, se lisent, se modifient en PATCH (items remplace toute la liste) et se suppriment ; un produit supprimé disparaît des listes qui le contenaient.
L'API ne déplie pas une liste en lignes de facture : envoyez toujours description, quantity, unitPrice et vatRate sur chaque ligne, et ajoutez productId pour garder le lien avec le produit. Si le catalogue est désactivé sur la société, ces routes répondent 403 PRODUCT_CATALOG_DISABLED : voir le guide du catalogue pour l'activer.
5. Finaliser et envoyer
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "event": "FINALIZE" }' $BASE/companies/$COMPANY/invoices/<invoiceId>/transition
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "kind": "invoice" }' $BASE/companies/$COMPANY/invoices/<invoiceId>/sendLe cycle de vie est celui de l'app : draftready (FINALIZE)sentpaid (MARK_PAID, avec un paidDate facultatif). CANCEL annule depuis draft, ready, sent ou overdue ; REOPEN ramène une facture annulée en brouillon. Toute autre paire répond 409 INVALID_TRANSITION, avec la paire refusée dans message.
L'envoi par e-mail passe par POST …/send : kind: "invoice" seulement depuis ready (409 BILL_EMAIL_INVALID_STATUS sinon), kind: "reminder" seulement depuis overdue (409 BILL_REMINDER_INVALID_STATUS sinon). Le PDF est joint et le mail part dans la langue de la facture ; un client sans adresse e-mail répond 422 CLIENT_EMAIL_MISSING. L'événement SEND de …/transition existe aussi, mais il se contente de marquer une facture ready comme sent, sans e-mail.
Annuler un paiement
curl -X POST -H "Authorization: Bearer $KEY" \
$BASE/companies/$COMPANY/invoices/<invoiceId>/cancel-paymentSans corps, réponse 204 : la date de paiement est effacée et la facture retrouve son statut précédent (sent ou overdue). Une facture qui n'est pas payée répond 409 BILL_NOT_PAID. Un paiement posé par le rapprochement bancaire répond 409 BILL_PAYMENT_RECONCILED : il s'annule en défaisant le rapprochement dans l'app.
6. Télécharger le PDF
curl -L -H "Authorization: Bearer $KEY" \
$BASE/companies/$COMPANY/invoices/<invoiceId>/pdf -o invoice.pdf
curl -L -H "Authorization: Bearer $KEY" \
$BASE/companies/$COMPANY/invoices/<invoiceId>/qr-bill-pdf -o qr-bill.pdfLes deux routes répondent 302 vers une URL de courte durée (15 minutes) : suivez toujours la redirection avec -L et ne mettez jamais le Location en cache. Les fichiers sont générés à la demande, le premier appel après une modification rend le document. 503 PDF_UNAVAILABLE signifie que le rendu a échoué : réessayez. Une facture importée d'un autre logiciel n'est jamais rendue : …/pdf sert son fichier d'origine, qui peut être une image, lisez le Content-Type.
7. Modifier une facture émise
Une fois sortie de draft, une facture ne change qu'avec une raison : chaque modification d'en-tête ou de ligne doit porter un modificationReason non vide, consigné dans le journal de la facture. Sans lui, la réponse est 422 MODIFICATION_REASON_REQUIRED et rien n'est écrit.
curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "dueDate": "2026-04-30", "modificationReason": "Client asked for a longer term" }' \
$BASE/companies/$COMPANY/invoices/<invoiceId>Les lignes ont leurs propres routes (POST …/items, PATCH …/items/{itemId}, DELETE …/items/{itemId}), toutes renvoient la facture entière avec ses totaux recalculés. Comme un DELETE n'a pas de corps, la raison voyage en paramètre de requête :
curl -X DELETE -H "Authorization: Bearer $KEY" \
"$BASE/companies/$COMPANY/invoices/<invoiceId>/items/<itemId>?modificationReason=Line%20billed%20twice"Une facture payée refuse tout changement de montant (409 BILL_PAID_LOCKED), et une facture importée tout changement de ce qui est imprimé (409 BILL_IMPORTED_LOCKED).
Tout enregistrer d'un bloc
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "expectedVersion": 1772323200000,
"header": { "clientId": "<clientId>", "lang": "fr", "currency": "CHF",
"issueDate": "2026-03-01", "dueDate": "2026-04-30" },
"items": [
{ "id": "<itemId>", "description": "Design", "quantity": 3, "unitPrice": 150, "vatRate": 8.1 },
{ "description": "Support", "quantity": 1, "unitPrice": 90, "vatRate": 8.1 }
],
"modificationReason": "Scope extended" }' \
$BASE/companies/$COMPANY/invoices/<invoiceId>PUT enregistre l'en-tête et toutes les lignes dans une seule transaction, et répond la facture entière. expectedVersion est le updatedAt de votre dernière lecture : si la facture a changé entre-temps, la réponse est 409 BILL_VERSION_CONFLICT et rien n'est écrit. header reprend les champs obligatoires de la création ; sans discount ou shippingCosts, les valeurs enregistrées restent, envoyez 0 pour les effacer.
items est la liste complète, dans l'ordre : une ligne absente est supprimée. Un id qui n'appartient pas à la facture, ou répété, répond 422 BILL_ITEM_NOT_FOUND. Les identifiants des lignes changent quand le contenu de la facture change : relisez-les dans la réponse, comme le nouveau updatedAt. Sur une facture sortie de draft, modificationReason est obligatoire dès que ce qui est imprimé change.
8. Devis et factures récurrentes
Les devis (/companies/{companyId}/quotes) ont la forme des factures, avec validUntil à la place de dueDate et une règle en plus : un devis ne se modifie qu'en draft ou expired. Un devis envoyé répond 409 QUOTE_NOT_EDITABLE ; repassez-le en brouillon avec REVERT_TO_DRAFT ou dupliquez-le. Aucun quota ne s'applique aux devis, seule la conversion consomme une facture du quota :
curl -X POST -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/quotes/<quoteId>/convertLa conversion crée une facture en brouillon et répond 201 avec invoiceId et invoiceNumber. Un devis se convertit une fois (409 QUOTE_ALREADY_CONVERTED), et un devis refusé ou expiré ne se convertit pas (409 QUOTE_NOT_CONVERTIBLE).
Pour envoyer un devis au client, POST …/send ne prend pas de corps et répond 204 :
curl -X POST -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/quotes/<quoteId>/sendLe PDF part à l'adresse du client, dans la langue du devis, puis le devis passe en sent. L'envoi n'est possible que depuis draft (409 QUOTE_EMAIL_INVALID_STATUS), avant la date de validité (422 VALID_UNTIL_IN_PAST) et avec une adresse client (422 CLIENT_EMAIL_MISSING). 502 QUOTE_PDF_GENERATION_FAILED et 502 QUOTE_EMAIL_SEND_FAILED signifient que rien n'est parti : réessayez.
Depuis sent ou expired, POST …/transition accepte MARK_ACCEPTED (avec un acceptedAt facultatif) et MARK_DECLINED. Un devis envoyé passe seul en expired à sa date de validité ; un PATCH qui repousse validUntil dans le futur le réactive.
PUT …/quotes/{quoteId} enregistre aussi l'en-tête et les lignes d'un bloc, avec les mêmes règles sur expectedVersion (409 QUOTE_VERSION_CONFLICT) et sur items (422 QUOTE_ITEM_NOT_FOUND). Deux différences : header peut ne contenir que les champs à changer, et il n'y a pas de modificationReason.
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "expectedVersion": 1772323200000,
"header": { "validUntil": "2026-06-30" },
"items": [{ "id": "<itemId>", "description": "Design", "quantity": 3, "unitPrice": 150, "vatRate": 8.1 }] }' \
$BASE/companies/$COMPANY/quotes/<quoteId>Une facture récurrente (/companies/{companyId}/recurring-invoices) n'est pas un document mais un en-tête, des lignes et une horloge : startDate amorce le prochain passage, endDate ou maxOccurrences l'arrêtent, et avec autoSend chaque facture générée est finalisée et envoyée au client. Il n'y a pas de route par ligne, PUT …/items remplace le jeu complet :
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "items": [
{ "description": "Abonnement", "quantity": 1, "unitPrice": 180, "vatRate": 8.1 },
{ "description": "Support", "quantity": 2, "unitPrice": 90, "vatRate": 8.1 }
] }' \
$BASE/companies/$COMPANY/recurring-invoices/<scheduleId>/items9. Supprimer des factures et des devis
DELETE …/invoices/{invoiceId} et DELETE …/quotes/{quoteId} suppriment un document et répondent 204. Pour plusieurs documents, envoyez de 1 à 100 identifiants :
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "ids": ["<invoiceId1>", "<invoiceId2>"] }' \
$BASE/companies/$COMPANY/invoices/bulk-delete
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "ids": ["<quoteId1>", "<quoteId2>"] }' \
$BASE/companies/$COMPANY/quotes/bulk-deleteLa réponse donne affected et skipped, qui compte les identifiants répétés. Supprimer la facture issue d'un devis rend ce devis à nouveau convertible.
Pour la paie et la comptabilité, continuez avec Paie, dépenses et comptabilité par l'API ; pour la pagination, les dates et les codes d'erreur, lisez Conventions et erreurs. Le détail de chaque champ est dans la Swagger UI de votre déploiement (…/api/docs).