Questa guida rifà, in curl, ciò che la schermata di fatturazione fa con il mouse. Ogni chiamata usa la stessa chiave e lo stesso URL di base di Iniziare con l'API.
BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...1. Prerequisiti
Da GET /me, prendete l'id di una società con modules.invoicing attivo, e tenetelo a portata di mano:
COMPANY=<companyId>2. Creare il cliente
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 risposta 201 contiene il cliente con il suo id. Un cliente appartiene a una società: usare su una fattura il cliente di un'altra società risponde 403 ACCESS_DENIED. Si applica la quota clienti del piano (403 CLIENT_LIMIT_REACHED).
3. Scegliere un modello
curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/invoice-templatesLa lista dei modelli non è paginata e non prende parametri. Quello con isDefault: true è il modello su cui una fattura ripiega quando non precisate nulla: templateId è facoltativo alla creazione. Il logo si gestisce nell'app, hasLogo è in sola lettura.
4. Creare la fattura
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 e currency sono obbligatori; templateId, discount, shippingCosts, notes, paymentTerms, showQrBill e items sono facoltativi. Il numero viene dal contatore della società e la fattura parte come bozza (draft).
La creazione conta nella quota mensile di fatture (403 INVOICE_LIMIT_REACHED), e una valuta diversa da CHF richiede un piano a pagamento (403 CURRENCY_PRO_ONLY).
5. Finalizzare e inviare
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>/sendIl ciclo di vita è quello dell'app: draftready (FINALIZE)sentpaid (MARK_PAID, con un paidDate facoltativo). CANCEL annulla da draft, ready, sent o overdue; REOPEN riporta in bozza una fattura annullata. Ogni altra coppia risponde 409 INVALID_TRANSITION, con la coppia rifiutata in message.
L'invio non è una transizione ma POST …/send: kind: "invoice" solo da ready (altrimenti 409 BILL_EMAIL_INVALID_STATUS), kind: "reminder" solo da overdue. Il PDF è allegato e la mail parte nella lingua della fattura; un cliente senza indirizzo e-mail risponde 422 CLIENT_EMAIL_MISSING.
6. Scaricare il 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.pdfEntrambe le rotte rispondono 302 verso un URL di breve durata (15 minuti): seguite sempre il redirect con -L e non mettete mai il Location in cache. I file sono generati su richiesta, la prima chiamata dopo una modifica renderizza il documento. 503 PDF_UNAVAILABLE significa che il rendering è fallito: riprovate.
7. Modificare una fattura emessa
Una volta uscita da draft, una fattura cambia solo con un motivo: ogni modifica di intestazione o riga deve portare un modificationReason non vuoto, registrato nel giornale della fattura. Senza, la risposta è 422 MODIFICATION_REASON_REQUIRED e nulla viene scritto.
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>Le righe hanno le proprie rotte (POST …/items, PATCH …/items/{itemId}, DELETE …/items/{itemId}), tutte restituiscono l'intera fattura con i totali ricalcolati. Poiché un DELETE non ha corpo, il motivo viaggia come parametro di query:
curl -X DELETE -H "Authorization: Bearer $KEY" \
"$BASE/companies/$COMPANY/invoices/<invoiceId>/items/<itemId>?modificationReason=Line%20billed%20twice"8. Preventivi e fatture ricorrenti
I preventivi (/companies/{companyId}/quotes) hanno la forma delle fatture, con una regola in più: solo una bozza si modifica. Un preventivo inviato risponde 409 QUOTE_NOT_EDITABLE; riportatelo in bozza con REVERT_TO_DRAFT o duplicatelo. I preventivi sono illimitati su tutti i piani, solo la conversione consuma una fattura della quota:
curl -X POST -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/quotes/<quoteId>/convertLa conversione crea una fattura in bozza e risponde 201 con invoiceId e invoiceNumber. Un preventivo si converte una volta (409 QUOTE_ALREADY_CONVERTED), e un preventivo rifiutato o scaduto non si converte (409 QUOTE_NOT_CONVERTIBLE).
Una fattura ricorrente (/companies/{companyId}/recurring-invoices) non è un documento ma un'intestazione, delle righe e un orologio: startDate innesca la prossima esecuzione, endDate o maxOccurrences la fermano, e con autoSend ogni fattura generata è finalizzata e inviata al cliente. Non c'è una rotta per riga, PUT …/items sostituisce l'intero insieme:
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>/itemsPer paghe e contabilità, continuate con Paghe, spese e contabilità con l'API; per paginazione, date e codici di errore, leggete Convenzioni ed errori. Il dettaglio di ogni campo è nella Swagger UI del vostro deployment (…/api/docs).