Back to guides

Invoicing with the API: from client to sent invoice

The whole invoicing path in REST calls: client, template, invoice, finalisation, sending, PDF, then quotes and recurring invoices.

Developers7 min read
Table of contents+

This guide redoes, in curl, what the invoicing screen does with the mouse. Every call uses the same key and base URL as Getting started with the API.

BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...

1. Prerequisites

From GET /me, take the id of a company whose modules.invoicing is on, and keep it handy:

COMPANY=<companyId>

2. Create the 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/clients

The 201 response holds the client with its id. A client belongs to one company: using another company's client on an invoice answers 403 ACCESS_DENIED. The plan's client quota applies (403 CLIENT_LIMIT_REACHED).

3. Pick a template

curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/invoice-templates

The template list is unpaginated and takes no parameter. The one with isDefault: true is the template an invoice falls back to when you say nothing: templateId is optional on create. The logo is managed in the app, hasLogo is read-only.

4. Create the invoice

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/invoices

clientId, lang, issueDate, dueDate and currency are required; templateId, discount, shippingCosts, notes, paymentTerms, showQrBill and items are optional. The number comes from the company counter and the invoice starts as a draft (draft).

Creation counts against the monthly invoice quota (403 INVOICE_LIMIT_REACHED), and a currency other than CHF needs a paid plan (403 CURRENCY_PRO_ONLY).

5. Finalise and send

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>/send

The lifecycle is the app's: draftready (FINALIZE)sentpaid (MARK_PAID, with an optional paidDate). CANCEL cancels from draft, ready, sent or overdue; REOPEN brings a cancelled invoice back to draft. Any other pair answers 409 INVALID_TRANSITION, with the refused pair in message.

Sending is not a transition but POST …/send: kind: "invoice" only from ready (409 BILL_EMAIL_INVALID_STATUS otherwise), kind: "reminder" only from overdue. The PDF is attached and the mail goes out in the invoice language; a client without an email address answers 422 CLIENT_EMAIL_MISSING.

6. Download the 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.pdf

Both routes answer 302 to a short-lived URL (15 minutes): always follow the redirect with -L and never cache the Location. Files are generated lazily, the first call after a change renders the document. 503 PDF_UNAVAILABLE means rendering failed: retry.

7. Change an issued invoice

Once it has left draft, an invoice only changes with a reason: every header or line change must carry a non-empty modificationReason, recorded in the invoice's log. Without it the answer is 422 MODIFICATION_REASON_REQUIRED and nothing is written.

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>

Lines have their own routes (POST …/items, PATCH …/items/{itemId}, DELETE …/items/{itemId}), all returning the whole invoice with its recomputed totals. Since a DELETE has no body, the reason travels as a query parameter:

curl -X DELETE -H "Authorization: Bearer $KEY" \
  "$BASE/companies/$COMPANY/invoices/<invoiceId>/items/<itemId>?modificationReason=Line%20billed%20twice"

8. Quotes and recurring invoices

Quotes (/companies/{companyId}/quotes) have the shape of invoices, with one extra rule: only a draft can change. A sent quote answers 409 QUOTE_NOT_EDITABLE; send it back to draft with REVERT_TO_DRAFT or duplicate it. Quotes are unlimited on every plan, only the conversion consumes an invoice from the quota:

curl -X POST -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/quotes/<quoteId>/convert

The conversion creates a draft invoice and answers 201 with invoiceId and invoiceNumber. A quote converts once (409 QUOTE_ALREADY_CONVERTED), and a declined or expired quote does not convert (409 QUOTE_NOT_CONVERTIBLE).

A recurring invoice (/companies/{companyId}/recurring-invoices) is not a document but a header, lines and a clock: startDate seeds the next run, endDate or maxOccurrences stop it, and with autoSend each generated invoice is finalised and sent to the client. There is no per-line route, PUT …/items replaces the whole set:

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>/items

For payroll and accounting, continue with Payroll, expenses and accounting with the API; for pagination, dates and error codes, read Conventions and errors. Every field is detailed in your deployment's Swagger UI (…/api/docs).