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" } }' \
$BASE/companies/$COMPANY/clientsLa réponse 201 contient le client avec son id. Un client appartient à une société : utiliser sur une facture le client d'une autre société répond 403 ACCESS_DENIED. Le quota de clients du plan s'applique (403 CLIENT_LIMIT_REACHED).
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. Le logo se gère dans l'application, hasLogo est en lecture seule.
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).
La création compte dans le quota mensuel de factures (403 INVOICE_LIMIT_REACHED), et une devise autre que CHF demande un plan payant (403 CURRENCY_PRO_ONLY).
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 ne passe pas par une transition mais par POST …/send : kind: "invoice" seulement depuis ready (409 BILL_EMAIL_INVALID_STATUS sinon), kind: "reminder" seulement depuis overdue. 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.
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.
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"8. Devis et factures récurrentes
Les devis (/companies/{companyId}/quotes) ont la forme des factures, avec une règle en plus : seul un brouillon se modifie. Un devis envoyé répond 409 QUOTE_NOT_EDITABLE ; repassez-le en brouillon avec REVERT_TO_DRAFT ou dupliquez-le. Les devis sont illimités sur tous les plans, 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).
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>/itemsPour 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).