Invoice a customer, send a QR-bill, receive the payment
This walkthrough follows a sales invoice from the customer's master data to the money in the bank: you create a Swiss customer, preview and book an invoice with the VAT data-vNext determines, hand the customer a QR-bill, record the receipt and check that nothing is open any more. It is for a client that issues invoices on behalf of a company, for example an invoicing app or a script.
Before you start
- Scopes:
readfor the lookups,draftto create and change,postso that your own invoices and receipts are booked at once (withdraftalone they wait for a second person's approval). The demo key$ERP_KEYhas all of them. - The demo organization has the entity
CH01(Demo AG, CHF, Swiss chart, VAT 8.1% standard and 2.6% reduced) with its bank account1020registered with an IBAN and a QR-IBAN, and the entityDE01(Demo GmbH, EUR). September 2026 is open for new documents. - Every
POSTthat creates something needs anIdempotency-Keyheader; repeat a request with the same key and you get the first answer again instead of a second document.
1. Create the customer
A customer is identified by its kind and code inside an entity. The structured address
(street, building_number, postal_code, town, country) is what the QR-bill prints as the
debtor, so give it in full. A customer created by a person is active at once; only new vendors
wait for a second person (see Customers and vendors).
curl -X POST "$ERP_URL/v1/entities/CH01/parties" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-customer-1" \
-H "Content-Type: application/json" \
-d '{"kind": "customer", "code": "C-MATTER", "name": "Matter Treuhand AG", "country": "CH", "currency": "CHF", "vat_number": "CHE-109.322.551", "language": "de", "street": "Bahnhofplatz", "building_number": "3", "postal_code": "3011", "town": "Bern", "email": "buchhaltung@matter-treuhand.example"}'
{"kind": "customer", "code": "C-MATTER", "status": "active", "currency": "CHF", "payment_terms_days": 30, "town": "Bern"}
The customer starts with payment terms of 30 days, which set the invoice's due date.
2. Preview the invoice
The preview answers what the invoice would look like, with the VAT of every line determined from the place of supply, and stores nothing. The body is the one you send to create the document, so a client can show live totals while the user types.
curl -X POST "$ERP_URL/v1/documents/preview" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{"entity": "CH01", "kind": "sales_invoice", "party": "C-MATTER", "document_date": "2026-09-21", "lines": [{"description": "Consulting September", "quantity": "10", "unit_price": "100"}, {"description": "Books", "quantity": "5", "unit_price": "40", "rate_type": "reduced"}]}'
{
"status": "draft",
"number": null,
"currency": "CHF",
"due_date": "2026-10-21",
"net_total": "1200.00",
"tax_total": "86.20",
"total": "1286.20",
"lines": [
{"line_no": 1, "net_amount": "1000.00", "tax_rate": "8.1", "tax_amount": "81.00", "tax": {"treatment": "domestic"}},
{"line_no": 2, "net_amount": "200.00", "tax_rate": "2.6", "tax_amount": "5.20", "tax": {"treatment": "domestic"}}
]
}
Notice that you never send a tax rate: the consulting line is charged the standard rate of 8.1%,
the line marked rate_type: reduced the reduced rate of 2.6%. Each line's tax object records
the decision and the rule behind it. number is null because only a posted invoice is numbered.
3. Create and submit the invoice
POST /v1/documents creates the invoice and, by default, submits it through the approval policy
in the same call. A person's invoice within the policy is posted and numbered at once.
curl -X POST "$ERP_URL/v1/documents" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-invoice-1" \
-H "Content-Type: application/json" \
-d '{"entity": "CH01", "kind": "sales_invoice", "party": "C-MATTER", "document_date": "2026-09-21", "lines": [{"description": "Consulting September", "quantity": "10", "unit_price": "100"}, {"description": "Books", "quantity": "5", "unit_price": "40", "rate_type": "reduced"}]}'
{
"document": {
"id": "<uuid>",
"kind": "sales_invoice",
"status": "posted",
"number": "<any>",
"party_code": "C-MATTER",
"net_total": "1200.00",
"tax_total": "86.20",
"total": "1286.20",
"open_amount": "1286.20"
},
"submit": {"status": "posted", "policy": {"decision": "allow"}}
}
Notice submit.policy.decision: allow means the invoice is posted; require_approval would leave
it pending_approval until a second person decides. The invoice number (for example
INV-2026-000006) depends on how many invoices the entity already has, so capture the id and
use it in the next requests. open_amount is what the customer still owes.
4. Read the invoice and its ETag
Reading a document returns an ETag header that names its version. You send it back as If-Match
when you change, submit or void a document, so that a change someone else made in the meantime is
not overwritten (step 10 shows it).
curl -X GET "$ERP_URL/v1/documents/$INVOICE_ID" \
-H "Authorization: Bearer $ERP_KEY"
{"status": "posted", "party_name": "Matter Treuhand AG", "total": "1286.20", "open_amount": "1286.20", "version": "<any>"}
5. Get the QR-bill
The QR-bill is the Swiss payment slip for the open amount of a posted invoice. data-vNext picks the registered bank account: with a QR-IBAN (the demo account has one) the reference is a QR reference derived from the invoice number, so the bank can report the payment by that reference; without one it is an ISO 11649 creditor reference. The default format is JSON.
curl -X GET "$ERP_URL/v1/invoices/$INVOICE_ID/qr-bill" \
-H "Authorization: Bearer $ERP_KEY"
{
"document_id": "<uuid>",
"account": "1020",
"open_amount": "1286.20",
"payload": "<any>",
"bill": {
"iban": "CH6431961000004421557",
"reference_type": "QRR",
"reference": "<any>",
"amount": "1286.20",
"currency": "CHF",
"creditor": {"name": "Demo AG", "town": "Zürich", "country": "CH"},
"debtor": {"name": "Matter Treuhand AG", "postal_code": "3011", "town": "Bern", "country": "CH"}
}
}
payload is the text that goes into the QR code (it starts with SPC, version 0200); bill
holds the same data as fields. The debtor comes from the customer's structured address. The
payment text is in English by default; add language=de, fr or it to change it.
6. Get the QR-bill as an image
With format=svg the answer is the payment part with the receipt (210 x 105 mm) ready to print;
format=pdf gives the same as a PDF. Only the start of the SVG and two labels are shown here.
curl -X GET "$ERP_URL/v1/invoices/$INVOICE_ID/qr-bill?format=svg" \
-H "Authorization: Bearer $ERP_KEY"
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
Payment part</text>
Receipt</text>
7. Record the receipt
When the customer pays, record a receipt: direction: incoming, the customer, the amount, and
the allocations that say which invoice it settles. The allocations must add up to the amount.
The bank account defaults to the entity's bank account (1020). Like an invoice, the receipt
goes through the approval policy.
curl -X POST "$ERP_URL/v1/payments" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-receipt-1" \
-H "Content-Type: application/json" \
-d '{"entity": "CH01", "direction": "incoming", "party": "C-MATTER", "payment_date": "2026-09-28", "amount": "1286.20", "reference": "QR-bill payment", "allocations": [{"document": "$INVOICE_ID", "amount": "1286.20"}]}'
{"payment_id": "<uuid>", "status": "posted", "submit": {"status": "posted", "policy": {"decision": "allow"}}}
A partial payment is allowed too: allocate less than the open amount and the rest stays open (and the next QR-bill shows only the rest).
8. Check that nothing is open
The invoice now has an open_amount of 0.00, and its allocations list the receipt.
curl -X GET "$ERP_URL/v1/documents/$INVOICE_ID" \
-H "Authorization: Bearer $ERP_KEY"
{"status": "posted", "total": "1286.20", "open_amount": "0.00"}
curl -X GET "$ERP_URL/v1/documents/$INVOICE_ID/allocations" \
-H "Authorization: Bearer $ERP_KEY"
[{"kind": "payment", "amount": "1286.20"}]
The customer card shows what the customer owes in total. Pass as_of so that the overdue amounts
are counted to a day you choose (the default is today); there is nothing open, so balances is
empty.
curl -X GET "$ERP_URL/v1/entities/CH01/parties/C-MATTER?kind=customer&as_of=2026-09-30" \
-H "Authorization: Bearer $ERP_KEY"
{"code": "C-MATTER", "status": "active", "balances": []}
A paid invoice is also found by its status in the document list:
curl -X GET "$ERP_URL/v1/documents?entity=CH01&kind=sales_invoice&status=paid&party=C-MATTER" \
-H "Authorization: Bearer $ERP_KEY"
[{"party_code": "C-MATTER", "total": "1286.20", "open_amount": "0.00"}]
9. Look at the aging report
The aging report lists every open posted document of the entity with its open amount, its days
overdue and its bucket (current, 1-30, 31-60, 61-90, 90+ days past due), with totals per
currency. Settled documents are left out, so the invoice from this page is not in it any more.
Had you asked before step 7, it would have been in the list with a shape like this (the number is
the one your entity assigned):
{
"number": "INV-2026-000006",
"party_code": "C-MATTER",
"kind": "sales_invoice",
"due_date": "2026-10-21",
"total": "1286.20",
"open": "1286.20",
"days_overdue": 0,
"bucket": "current"
}
curl -X GET "$ERP_URL/v1/entities/CH01/reports/aging?side=receivables&as_of=2026-09-30" \
-H "Authorization: Bearer $ERP_KEY"
{"entity": "CH01", "side": "receivables", "as_of": "2026-09-30"}
10. Void a draft with If-Match
A draft invoice can still be changed or withdrawn. Create one with submit=false, read it to get
its ETag, and let another person change it before you void it. Your void names the old version,
so it is refused instead of discarding the other person's work.
curl -X POST "$ERP_URL/v1/documents?submit=false" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-draft-1" \
-H "Content-Type: application/json" \
-d '{"entity": "CH01", "kind": "sales_invoice", "party": "C-MATTER", "document_date": "2026-09-21", "lines": [{"description": "Consulting October", "quantity": "10", "unit_price": "100"}]}'
{"document": {"status": "draft", "number": null, "total": "1081.00"}, "submit": null}
curl -X GET "$ERP_URL/v1/documents/$DRAFT_ID" \
-H "Authorization: Bearer $ERP_KEY"
{"status": "draft", "open_amount": null}
The other person changes the note on the draft (PATCH replaces only what you send). The answer
carries the new ETag.
curl -X PATCH "$ERP_URL/v1/documents/$DRAFT_ID" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{"notes": "Please quote order PO-8812"}'
{"status": "draft", "notes": "Please quote order PO-8812"}
Voiding with the ETag you read earlier is refused with 412; nothing changes.
curl -X POST "$ERP_URL/v1/documents/$DRAFT_ID/void" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-void-stale" \
-H 'If-Match: $OLD_ETAG' \
-H "Content-Type: application/json" \
-d '{"reason": "Customer withdrew the order"}'
{"error": {"code": "precondition_failed"}}
Read the document again (or use the ETag the PATCH returned) and void it. A voided draft keeps
no number and posts nothing.
curl -X POST "$ERP_URL/v1/documents/$DRAFT_ID/void" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-void-current" \
-H 'If-Match: $NEW_ETAG' \
-H "Content-Type: application/json" \
-d '{"reason": "Customer withdrew the order"}'
{"status": "void", "number": null}
11. Issue a German invoice as XRechnung
The entity DE01 (Demo GmbH) issues invoices to German customers, and German public buyers expect
them as XRechnung, a structured XML invoice. The demo has an August invoice to the customer
C-HH; list it to find its id.
curl -X GET "$ERP_URL/v1/documents?entity=DE01&kind=sales_invoice&party=C-HH" \
-H "Authorization: Bearer $ERP_KEY"
[{"party_code": "C-HH", "status": "posted", "currency": "EUR", "net_total": "5000.00", "tax_total": "950.00", "total": "5950.00", "buyer_reference": "4500012345"}]
The XRechnung is generated from the posted invoice and checked against the EN 16931 and XRechnung
rules before it is returned (an invoice that breaks a fatal rule answers 422; format=json shows
the check). The file here is the invoice's XML; only two of its lines are shown.
curl -X GET "$ERP_URL/v1/invoices/$HH_INVOICE_ID/xrechnung" \
-H "Authorization: Bearer $ERP_KEY"
urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0
<ram:BuyerReference>4500012345</ram:BuyerReference>
The buyer reference 4500012345 is the customer's order number on the invoice. A customer that
is a public buyer carries a Leitweg-ID instead, which becomes the buyer reference.
What can go wrong
A credit note cannot credit more than was invoiced, less earlier credit notes. The invoice from step 3 is far smaller than this credit, so it is refused with 422 and nothing is stored. (A credit note within that limit is accepted even for a paid invoice: what it credits beyond the open amount stays open on the credit note, to refund or apply to another invoice.)
curl -X POST "$ERP_URL/v1/documents" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-credit-1" \
-H "Content-Type: application/json" \
-d '{"entity": "CH01", "kind": "sales_credit_note", "party": "C-MATTER", "document_date": "2026-09-30", "credits": "$INVOICE_ID", "lines": [{"description": "Goodwill", "quantity": "1", "unit_price": "1000000"}]}'
{"error": {"code": "credit_more_than_invoiced"}}
A settled invoice has no QR-bill; there is nothing left to pay.
curl -X GET "$ERP_URL/v1/invoices/$INVOICE_ID/qr-bill" \
-H "Authorization: Bearer $ERP_KEY"
{"error": {"code": "qr_bill_settled"}}
A posted invoice cannot be voided; it is corrected with a credit note.
curl -X POST "$ERP_URL/v1/documents/$INVOICE_ID/void" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-void-posted" \
-H "Content-Type: application/json" \
-d '{"reason": "Entered by mistake"}'
{"error": {"code": "document_not_draft"}}
A receipt cannot allocate more than the open amount (here nothing is open any more).
curl -X POST "$ERP_URL/v1/payments" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: ci-receipt-2" \
-H "Content-Type: application/json" \
-d '{"entity": "CH01", "direction": "incoming", "party": "C-MATTER", "payment_date": "2026-09-29", "amount": "100.00", "allocations": [{"document": "$INVOICE_ID", "amount": "100.00"}]}'
{"error": {"code": "more_than_open"}}
Reference
POST /v1/entities/{entity}/partiesGET /v1/entities/{entity}/parties/{party}POST /v1/documents/previewPOST /v1/documentsGET /v1/documentsGET /v1/documents/{document_id}PATCH /v1/documents/{document_id}POST /v1/documents/{document_id}/voidGET /v1/documents/{document_id}/allocationsGET /v1/invoices/{invoice}/qr-billPOST /v1/paymentsGET /v1/entities/{entity}/reports/agingGET /v1/invoices/{invoice}/xrechnung