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, prendi l'id di una società con modules.invoicing attivo, e tienilo 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" },
"details": { "kind": "company", "contactPersons": [] } }' \
$BASE/companies/$COMPANY/clientsLa risposta 201 contiene il cliente con il suo id. name, address e details sono obbligatori: details.kind vale company (con contactPersons, una lista che può essere vuota, e facoltativamente ideNumber e vatNumber) oppure individual. Un cliente appartiene a una società: usare in una fattura il cliente di un'altra società risponde 403 ACCESS_DENIED. Nel piano Business i clienti sono illimitati; 403 CLIENT_LIMIT_REACHED compare solo se il tuo abbonamento non copre il modulo Fatturazione.
Importare clienti in lotto
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/batchIl lotto accetta da 1 a 200 clienti ed è atomico: tutto viene scritto oppure niente, e un lotto che supererebbe la quota di clienti viene rifiutato per intero (403 CLIENT_LIMIT_REACHED). Una riga la cui e-mail esiste già nella società, o compare più in alto nel lotto, viene ignorata senza errore; una riga senza e-mail non è mai considerata un duplicato. La risposta 201 riporta i due contatori, inserted e skippedDuplicates. La lettura del file (CSV, foglio di calcolo) resta a tuo carico.
3. Scegliere un modello
curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/invoice-templatesLa lista dei modelli non è paginata e non accetta alcun parametro. Quello con isDefault: true è il modello a cui una fattura ripiega quando non precisi nulla: templateId è facoltativo alla creazione. Nel JSON hasLogo è di sola lettura: il logo passa da una rotta dedicata, in byte grezzi (PNG o JPEG, 2 MB al massimo).
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 fa lo stesso con il logo della società (ruolo di proprietario o admin richiesto); il logo di un modello lo sostituisce sui documenti di quel modello. DELETE su entrambe le rotte rimuove il logo.
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).
Nel piano Business le fatture sono illimitate e le valute diverse dal CHF sono consentite. 403 INVOICE_LIMIT_REACHED e 403 CURRENCY_PRO_ONLY compaiono solo se il tuo abbonamento non copre il modulo Fatturazione: si applicano allora i limiti del piano Starter.
Riutilizzare il catalogo prodotti
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "name": "Consulenza 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": "Pacchetto lancio", "items": [{ "productId": "<productId>", "quantity": 2 }] }' \
$BASE/companies/$COMPANY/product-listsUn prodotto richiede name, unitPrice, currency e vatRate; description, unit e reference sono facoltativi. Un elenco conserva i suoi prodotti nell'ordine dell'array (500 al massimo, ciascuno una sola volta, altrimenti 422 PRODUCT_LIST_DUPLICATE_PRODUCT). Entrambe le risorse si elencano con paginazione, si leggono, si modificano con PATCH (items sostituisce l'intero elenco) e si eliminano; un prodotto eliminato scompare dagli elenchi che lo contenevano.
L'API non espande un elenco in righe di fattura: invia sempre description, quantity, unitPrice e vatRate su ogni riga e aggiungi productId per mantenere il legame con il prodotto. Se il catalogo è disattivato sulla società, queste rotte rispondono 403 PRODUCT_CATALOG_DISABLED: per attivarlo vedi la guida al catalogo prodotti.
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 per e-mail passa da POST …/send: kind: "invoice" solo da ready (altrimenti 409 BILL_EMAIL_INVALID_STATUS), kind: "reminder" solo da overdue (altrimenti 409 BILL_REMINDER_INVALID_STATUS). Il PDF è allegato e l'e-mail parte nella lingua della fattura; un cliente senza indirizzo e-mail risponde 422 CLIENT_EMAIL_MISSING. Esiste anche l'evento SEND di …/transition, ma si limita a contrassegnare come sent una fattura ready, senza e-mail.
Annullare un pagamento
curl -X POST -H "Authorization: Bearer $KEY" \
$BASE/companies/$COMPANY/invoices/<invoiceId>/cancel-paymentSenza corpo, risposta 204: la data di pagamento viene cancellata e la fattura torna allo stato precedente (sent oppure overdue). Una fattura non pagata risponde 409 BILL_NOT_PAID. Un pagamento registrato dalla riconciliazione bancaria risponde 409 BILL_PAYMENT_RECONCILED: si annulla rimuovendo la riconciliazione nell'app.
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): segui sempre il redirect con -L e non mettere 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 non è riuscito: riprova. Una fattura importata da un altro software non viene mai renderizzata: …/pdf serve il suo file originale, che può essere un'immagine, quindi leggi il Content-Type.
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"Una fattura pagata rifiuta qualsiasi modifica degli importi (409 BILL_PAID_LOCKED) e una fattura importata qualsiasi modifica di ciò che viene stampato (409 BILL_IMPORTED_LOCKED).
Salvare tutto in una sola volta
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": "Assistenza", "quantity": 1, "unitPrice": 90, "vatRate": 8.1 }
],
"modificationReason": "Scope extended" }' \
$BASE/companies/$COMPANY/invoices/<invoiceId>PUT salva l'intestazione e tutte le righe in un'unica transazione e restituisce l'intera fattura. expectedVersion è l'updatedAt della tua ultima lettura: se nel frattempo la fattura è cambiata, la risposta è 409 BILL_VERSION_CONFLICT e non viene scritto nulla. header riprende i campi obbligatori della creazione; senza discount o shippingCosts i valori salvati restano, invia 0 per azzerarli.
items è l'elenco completo, in ordine: una riga assente viene eliminata. Un id che non appartiene alla fattura, o ripetuto, risponde 422 BILL_ITEM_NOT_FOUND. Gli identificativi delle righe cambiano quando cambia il contenuto della fattura: rileggili nella risposta, come il nuovo updatedAt. Su una fattura uscita da draft, modificationReason è obbligatorio non appena cambia ciò che viene stampato.
8. Preventivi e fatture ricorrenti
I preventivi (/companies/{companyId}/quotes) hanno la forma delle fatture, con validUntil al posto di dueDate e una regola in più: un preventivo si modifica solo in draft o expired. Un preventivo inviato risponde 409 QUOTE_NOT_EDITABLE; riportalo in bozza con REVERT_TO_DRAFT oppure duplicalo. Ai preventivi non si applica alcuna quota, 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).
Per inviare un preventivo al cliente, POST …/send non prende alcun corpo e risponde 204:
curl -X POST -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/quotes/<quoteId>/sendIl PDF parte all'indirizzo del cliente, nella lingua del preventivo, poi il preventivo passa a sent. L'invio è possibile solo da draft (409 QUOTE_EMAIL_INVALID_STATUS), prima della data di validità (422 VALID_UNTIL_IN_PAST) e con un indirizzo cliente (422 CLIENT_EMAIL_MISSING). 502 QUOTE_PDF_GENERATION_FAILED e 502 QUOTE_EMAIL_SEND_FAILED indicano che non è partito nulla: riprova.
Da sent o expired, POST …/transition accetta MARK_ACCEPTED (con un acceptedAt facoltativo) e MARK_DECLINED. Un preventivo inviato passa da solo a expired alla data di validità; un PATCH che sposta validUntil nel futuro lo riattiva.
PUT …/quotes/{quoteId} salva anche l'intestazione e le righe in una sola volta, con le stesse regole su expectedVersion (409 QUOTE_VERSION_CONFLICT) e su items (422 QUOTE_ITEM_NOT_FOUND). Due differenze: header può contenere solo i campi da modificare e non esiste 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>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>/items9. Eliminare fatture e preventivi
DELETE …/invoices/{invoiceId} e DELETE …/quotes/{quoteId} eliminano un documento e rispondono 204. Per più documenti, invia da 1 a 100 identificativi:
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 risposta riporta affected e skipped, che conta gli identificativi ripetuti. Eliminare la fattura nata da un preventivo rende di nuovo convertibile quel preventivo.
Per paghe e contabilità, continua con Paghe, spese e contabilità con l'API; per paginazione, date e codici di errore, leggi Convenzioni ed errori. Il dettaglio di ogni campo è nella Swagger UI del tuo deployment (…/api/docs).