Diese Anleitung macht in curl nach, was der Fakturierungsbildschirm mit der Maus tut. Jeder Aufruf nutzt denselben Schlüssel und dieselbe Basis-URL wie Erste Schritte mit der API.
BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...1. Voraussetzungen
Nehmen Sie aus GET /me die id einer Firma, bei der modules.invoicing aktiv ist, und halten Sie sie bereit:
COMPANY=<companyId>2. Den Kunden anlegen
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/clientsDie Antwort 201 enthält den Kunden mit seiner id. Ein Kunde gehört zu einer Firma: Den Kunden einer anderen Firma auf einer Rechnung zu verwenden, antwortet 403 ACCESS_DENIED. Das Kundenkontingent des Plans gilt (403 CLIENT_LIMIT_REACHED).
3. Eine Vorlage wählen
curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/invoice-templatesDie Vorlagenliste ist nicht paginiert und nimmt keinen Parameter. Die Vorlage mit isDefault: true ist die, auf die eine Rechnung zurückfällt, wenn Sie nichts angeben: templateId ist beim Erstellen optional. Das Logo wird in der App verwaltet, hasLogo ist schreibgeschützt.
4. Die Rechnung erstellen
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 und currency sind Pflicht; templateId, discount, shippingCosts, notes, paymentTerms, showQrBill und items sind optional. Die Nummer kommt vom Zähler der Firma, und die Rechnung startet als Entwurf (draft).
Das Erstellen zählt zum monatlichen Rechnungskontingent (403 INVOICE_LIMIT_REACHED), und eine andere Währung als CHF braucht einen bezahlten Plan (403 CURRENCY_PRO_ONLY).
5. Finalisieren und versenden
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>/sendDer Lebenszyklus ist der der App: draftready (FINALIZE)sentpaid (MARK_PAID, mit optionalem paidDate). CANCEL storniert aus draft, ready, sent oder overdue; REOPEN holt eine stornierte Rechnung in den Entwurf zurück. Jedes andere Paar antwortet 409 INVALID_TRANSITION, mit dem abgelehnten Paar in message.
Der Versand ist kein Übergang, sondern POST …/send: kind: "invoice" nur aus ready (sonst 409 BILL_EMAIL_INVALID_STATUS), kind: "reminder" nur aus overdue. Das PDF wird angehängt und die Mail geht in der Rechnungssprache raus; ein Kunde ohne E-Mail-Adresse antwortet 422 CLIENT_EMAIL_MISSING.
6. Das PDF herunterladen
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.pdfBeide Routen antworten 302 auf eine kurzlebige URL (15 Minuten): Folgen Sie der Weiterleitung immer mit -L und cachen Sie das Location nie. Die Dateien werden bei Bedarf erzeugt, der erste Aufruf nach einer Änderung rendert das Dokument. 503 PDF_UNAVAILABLE heisst, das Rendern ist fehlgeschlagen: erneut versuchen.
7. Eine ausgestellte Rechnung ändern
Hat eine Rechnung draft verlassen, ändert sie sich nur mit Begründung: Jede Kopf- oder Positionsänderung muss einen nicht leeren modificationReason tragen, der im Protokoll der Rechnung festgehalten wird. Ohne ihn lautet die Antwort 422 MODIFICATION_REASON_REQUIRED, und nichts wird geschrieben.
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>Positionen haben eigene Routen (POST …/items, PATCH …/items/{itemId}, DELETE …/items/{itemId}), alle geben die ganze Rechnung mit neu berechneten Summen zurück. Da ein DELETE keinen Body hat, reist die Begründung als Query-Parameter:
curl -X DELETE -H "Authorization: Bearer $KEY" \
"$BASE/companies/$COMPANY/invoices/<invoiceId>/items/<itemId>?modificationReason=Line%20billed%20twice"8. Offerten und wiederkehrende Rechnungen
Offerten (/companies/{companyId}/quotes) haben die Form von Rechnungen, mit einer zusätzlichen Regel: Nur ein Entwurf lässt sich ändern. Eine versandte Offerte antwortet 409 QUOTE_NOT_EDITABLE; setzen Sie sie mit REVERT_TO_DRAFT in den Entwurf zurück oder duplizieren Sie sie. Offerten sind auf jedem Plan unbegrenzt, nur die Umwandlung verbraucht eine Rechnung aus dem Kontingent:
curl -X POST -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/quotes/<quoteId>/convertDie Umwandlung erstellt eine Rechnung im Entwurf und antwortet 201 mit invoiceId und invoiceNumber. Eine Offerte wird einmal umgewandelt (409 QUOTE_ALREADY_CONVERTED), eine abgelehnte oder abgelaufene gar nicht (409 QUOTE_NOT_CONVERTIBLE).
Eine wiederkehrende Rechnung (/companies/{companyId}/recurring-invoices) ist kein Dokument, sondern Kopf, Positionen und Uhr: startDate setzt den nächsten Lauf, endDate oder maxOccurrences beenden ihn, und mit autoSend wird jede erzeugte Rechnung finalisiert und an den Kunden versandt. Es gibt keine Route pro Position, PUT …/items ersetzt den ganzen Satz:
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>/itemsFür Lohn und Buchhaltung geht es weiter mit Lohn, Ausgaben und Buchhaltung per API; zu Paginierung, Daten und Fehlercodes lesen Sie Konventionen und Fehler. Jedes Feld ist in der Swagger UI Ihres Deployments beschrieben (…/api/docs).