Description
Purpose and use
Invoices manage billing documents from draft through issued, sent, overdue, and paid states. They support line items, VAT, Swiss QR-bill data, PDF generation, OCR extraction, payer details, and prefilled transfer drafts.
Who uses this. Finance operations, billing teams, customer service, payment operations, and customers use invoices when issuing payment documents or preparing payments from received invoices.
How it works. Users create invoice records with line items and payer data, or upload a document for OCR. OCR serves two flows: the pay-a-bill flow identifies invoice details, resolves the counterparty, creates supporting upload records, and prefills a transfer draft for review; the authoring flow (v3) instead extracts a Swiss-shaped invoice — biller and payer parties, per-line MWST rates, QR reference type, Swico tags — to prefill the create-invoice form for the operator to review against the document and save.
What users do. Users create or edit drafts, issue and send invoices, upload scanned invoices, check OCR status, review extracted payment data, and mark invoices paid when settlement is confirmed.
Outcomes and side effects. Invoice workflows can create PDFs, uploads, QR payment references, payer records, and draft transfers. Only the transfer or payment workflow moves money; invoice status records the billing lifecycle.
Related manuals: Payers, Transfers, Uploads, PDFGen.
Overview
The Invoices module provides comprehensive functionality for managing invoices, including:
- Creation and management of invoices with line items
- Automatic invoice numbering with configurable formats
- OCR processing of invoice documents
- QR code generation and reading (Swiss QR-bill compatible)
- PDF generation with embedded QR codes
- Management of common receivers and senders
- Support for multiple currencies and VAT calculations
Invoice numbers
invoice_number is generated per customer from a template of three segments joined by a separator: {PREFIX}{SEP}{YEAR}{SEP}{SEQ}.
- Shipped defaults are prefix
INV, separator-, a 6-digit zero-padded sequence, and a{YEAR}segment ofYYYYMMDD, so a number readsINV-20260806-000001. - A
{YEAR}layout that does not give every reset period its own text is replaced with the default too, since the counter restarts each period: withreset_periodmonthly,Monday 2006rendersThursday 2006for both February and March. {YEAR}is a Go reference-date layout. Use date tokens only —2006year,01month,02day.15is the hour token and would stamp the wall-clock hour where the day belongs; a layout that renders any time-of-day part, or nothing at all, is replaced with theYYYYMMDDdefault both when a customer's numbering config is created and each time a number is generated from it, so a config stored before the layout was corrected still mints day-stamped numbers.- The sequence starts at the configured
start_number— its default is1— from the customer's very first invoice, not from the second numbering period. - The sequence resets yearly by default, independent of the
{YEAR}segment's granularity, so numbers stay sequential across days within a year. Each reset returns the counter tostart_number. - Numbers are unique within a customer, enforced by a unique index, and allocated atomically so concurrent creates cannot take the same one. They are not unique across customers: two customers can hold the same number. Use the invoice
idas the identifier and treatinvoice_numberas an opaque display string — clients must not parse it for meaning. Operators may reconfigure every segment, so its shape is not part of the API contract. - A number is drawn when the invoice row is created, whatever status it is created in: a
draftcarries its final number from the start and keeps it when it is issued, so resuming or editing a draft never redraws. Deleting a draft — the only statusDELETE /v1/invoices/{id}accepts — does not step the counter back, and an abandoned draft holds its number indefinitely. - The sequence is therefore allowed to have gaps, and this is deliberate. A retired draft number is one cause; the other is a create that failed after drawing, which gives the number back only when no concurrent create has moved past it. Do not read
invoice_numberas a count of invoices issued, and do not expect consecutive invoices to carry consecutive numbers. - Previewing the next number does not reserve it. The preview is advisory and carries an expiry: a concurrent create can take that number first, and the preview does not move the sequence.
- The preview, the v3 create and update, and both OCR scans are authorized against the customer they name, not merely against the caller's right to issue invoices, which RBAC grants on the record type. The permission asked for is read: issuing an invoice does not modify the customer, and the shipped company roles hold
customersCRUD only for admin and executive, CR for accountant and R for employee. A caller with no relationship to the customer holds neither and gets 403, and no numbering sequence is opened for them. - A client must not submit the previewed value back as
invoice_number. A client-supplied number never touches the counter, so a form that fills the field from the preview pins the sequence: every create sends the same number and the second is rejected with 409. Render the preview as a placeholder and send the field empty to let the server draw. - Supplying
invoice_numberon a create or an edit is allowed, but a value the customer already holds is rejected with 409 rather than creating a duplicate. Leading and trailing whitespace is stripped before the value is stored or compared, soINV-1andINV-1are the same number. This applies to every path that writes the number:POST /v2/invoices,POST /v3/invoices,PUT /v2/invoices/{id}andPUT /v3/invoices/{id}. - The two 409 causes are distinguished. A number the client sent returns
invoices.duplicate_invoice_number. A generated number that collided with an earlier client-supplied one returnsinvoices.invoice_number_collision— the caller sent nothing wrong and can retry, because the sequence has already advanced past the collision.
Invoice direction
Every invoice records a direction, seen from the customer's own books, so the two sides of the billing relationship stay separate in the register.
- Sent Invoices (
party_type=receiver) — the customer issued the invoice to bill a counterparty for goods or services. This is money owed to the customer: accounts receivable. It is the default direction for an invoice the customer creates. - Received Invoices (
party_type=sender) — a counterparty issued the invoice to bill the customer. This is money the customer owes: accounts payable, typically the bill that a later payment settles.
Direction is chosen when the invoice is created and is not changed by ordinary edits: an operator who only corrects a line, a date, or the status leaves the direction as it stands, so a bill filed as Received never silently moves into the Sent register. The invoices list can be filtered by direction, which is what backs the separate Sent Invoices and Received Invoices views — each view simply lists one direction.
Invoice statuses
Client 2.0 invoices use these MVP statuses:
draft: editable invoice that is not finalised.issued: finalised and valid invoice, not necessarily sent to the customer.sent: invoice was shared or sent to the customer.overdue: invoice due date has passed and the invoice is not paid. The API may return this effective status for issued or sent invoices whose due date is in the past.paid: final successful state.
pending is a legacy alias for issued. New clients should send and display issued; existing pending records and requests are accepted for compatibility and are treated as finalised/valid invoices that are not necessarily sent.
Allowed MVP transitions are:
draft->issuedissued/pending->sentissued/pending/sent/overdue->paid
A draft or an issued invoice that has not yet been sent can still have its content edited — line items, parties, amounts, dates and payment details — because neither has left the biller's hands. Once an invoice is sent (or paid, or overdue), it is locked: only lifecycle status transitions are accepted, no content change. Editing an issued invoice keeps it issued and re-renders its document and QR payment part; it is never demoted back to draft, since that is not a legal transition. overdue is read-derived only: clients cannot set it directly, and the API returns it when an issued or sent invoice is past due and unpaid. paid is final and cannot transition further.
Endpoints
Create Invoice V3
POST /v3/invoices
Create or update selector-based invoices using source and destination counterparties instead of inline address payloads.
V3 request shape:
source.counterparty_idanddestination.counterparty_idare required.- The source (creditor) party must be the invoice customer's own company — the QR bill credits the source account, so an invoice can only ever be issued to be paid into the customer's own counterparty. A source that names any other counterparty is rejected, even if the caller can otherwise read it. The destination (debtor) is the external client and is not restricted this way.
- Optional
cp_account_idmust belong to selected counterparty and be active. - Optional
cp_address_idmust be an active address on selected counterparty. If omitted, API picks active preferred billing/fallback address. party_typesets the direction:receiver(the default when omitted) is an outgoing invoice the customer issues to bill a counterparty;senderis a received invoice a counterparty issued to bill the customer (an accounts-payable bill recorded to pay later). An explicit value is honored; on update an omitted value preserves the stored direction.- For an outgoing (
receiver) invoice the source party is the customer's own company, and the QR bill credits its account — so the source counterparty must resolve to an IBAN for any status other thandraft. The source resolves against the customer's own (internal) accounts only — a bank account on the source counterparty is an external receiving detail and is never used as the invoice's creditor.source.cp_account_idis used when supplied; if it names a bank account (rather than an own account), the request is rejected. Otherwise own accounts that can settleamount.currencyare preferred, and only if none matches does an own account in another currency apply, so an outgoing invoice is never left without a creditor. The destination (debtor) party keeps the full bank-then-own resolution its account legitimately needs. - For a received (
sender) invoice the source party is the supplier, and no QR bill is issued for it — so the supplier IBAN is not required to save the bill, in any status. It is a payment convenience captured later when the bill is paid. - A
draftmay be created and edited before an account is chosen, since it is not yet payable. For an outgoing invoice, leavingdraftrequires a source IBAN, so it is enforced when the invoice is issued, sent, or marked paid. - An explicit
source.cp_account_idis never substituted. If it names an account that carries no IBAN, such as a crypto wallet, the request is rejected instead of falling back to another account. - The resolved IBAN is normalized before it is stored: every separator is removed and the value is upper-cased. An account saved as
CH93 0076 2011 6238 5295 7is returned asCH9300762011623852957on the invoice, which is the only form the QR bill payload admits. This covers any separator the account was stored with, not only plain spaces — a value pasted with tabs, non-breaking or narrow spaces, or hyphens normalizes the same way, which is what lets a QR-IBAN keep itsQRRreference type instead of falling back toNON. destination.cp_account_idcarries no IBAN requirement, since the destination IBAN is not rendered on the invoice.- Every invoice has a PDF, including a
draftthat names no account yet. The document is the invoice — parties, items, totals — and the Swiss QR payment part is a section that is included only when the invoice credits a real account and the standard admits the combination. An invoice with no source IBAN therefore gets its document with the payment part omitted, never with a fabricated account number. Updating a v3 invoice re-renders it. - If the stored PDF is missing for an invoice that does credit an account — for example because document storage was unavailable right after the invoice was committed —
GET /v1/invoices/{id}/pdfre-renders it from the invoice on demand, stores it again, and serves it. A post-commit storage failure therefore never fails the create or update request and never permanently loses the document. - A read never deletes a stored PDF.
GET /v1/invoices/{id}/pdfserves the stored document, or re-renders it from the committed invoice when none is stored — for any invoice, whether or not it credits an account. - V3 invoices created before this release were stored with an empty IBAN, and their PDF printed a placeholder account number. Those are repaired in place: a migration marks them, and a
GET /v1/invoices/{id}/pdfre-renders the document from the invoice, replaces the stored copy and clears the mark. Until that succeeds the stored copy is still what is served, so a read that cannot render or store retries on the next one. The re-rendered document omits the payment part rather than guessing a creditor; issuing an account on those invoices is an ordinary edit. - Invoices carrying a shipping charge that were issued before this release hold a PDF whose totals block omits it, and they are repaired the same way: a migration marks every invoice with a non-zero
shipping, and the nextGET /v1/invoices/{id}/pdfre-renders the document, replaces the stored copy and clears the mark. Invoices with no shipping charge are left alone — their stored rows already reconcile with their total, so re-rendering would only add aShipping 0.00line. - While an invoice is waiting for that repair its
metadatacarries a transientqr_bill_needs_rerenderkey. It is removed once the document has been replaced. The key is reserved: it is stripped from anymetadataa client sends, on every create and edit, so only a migration sets it. A v1/v2 edit additionally leaves the invoice's ownmetadataexactly as stored — that endpoint does not write the client's map to the invoice row at all, only to its line items — so the marker survives an edit there even though that path never republishes the document. Treatmetadataas an open map, as the schema already declares. - A content edit is rejected as a conflict when a concurrent request advanced the invoice past its editable window first — for example another request sent the invoice while it was being edited. The status observed when the edit was read is re-checked as part of the write, so a full edit can never revive a sent (or paid) invoice at its earlier amounts, nor apply an issued invoice's edit to a copy a concurrent request already sent. Nothing is changed; re-read the invoice and retry as a status change if that is still the intent.
- Publishing the PDF is serialized per invoice with a Postgres advisory lock spanning the row commit and the document store, so concurrent edits (and on-demand regeneration) cannot leave a stored document that describes an older edit than the row. Waiting for that lock is bounded at ten seconds; past that the request fails rather than holding its database connection indefinitely, and a read falls back to the document already stored. A re-render reads the invoice and its items in a single repeatable-read snapshot, so an edit committing mid-read cannot produce a document whose lines and payment amount come from different states.
- PDF amounts — item prices and the totals block — are formatted at the invoice currency's precision: a JPY invoice shows whole amounts, a three-decimal currency keeps all three decimals.
- The totals block names every amount the total is built from: discount, the subtotal before tax, VAT, and shipping, then the total itself. Shipping prints immediately above the total, as it does in the web app, and prints as zero rather than being omitted when there is no shipping charge. Every amount prints unsigned, and the total adds the subtotal, VAT and shipping and subtracts the discount — so a reader can account for the total from the rows shown, which is what a document that charged for shipping without naming it could not offer.
- An invoice with more than ten line items moves its whole items table to a second page, and that page's totals block is bounded by the page footer: from 44 line items the block stops following the table and sits above the footer instead, so the total stays on the printable area however long the item list runs. The second page does not paginate, so a long enough item list runs into that block: from 45 line items the block's separator rule crosses the last item row, and from 46 the remaining item rows are drawn across the block and the two amount columns overprint. Such an invoice needs its item list shortened until page-2 pagination exists.
- The QR bill's amount and the printed Currency/Amount boxes are formatted from the stored decimal total, so an amount that has no exact binary representation (22.36, 100.10) is offered exactly as invoiced. The currency is emitted upper-cased, the only form the standard admits.
- The Swiss QR payment part is included only when the standard admits the combination: a Swiss or Liechtenstein creditor account, a
CHForEURamount, and a matching reference. Any other invoice is accepted and its PDF is generated, just without the payment part, rather than with a code banking apps would reject. - The payload's reference type follows the account and the invoice
reference: a QR-IBAN (institution ID 30000–31999) requires a valid 27-digit QR reference and is encoded asQRR; a standard IBAN is encoded asSCORwhenreferenceis a valid ISO 11649 creditor reference (RF...) and asNONotherwise. Free-text references are never emitted as structured payment references, and a QR-IBAN without a valid QR reference renders without the payment part. - Request-side invoice amounts are grouped under
amount, includingamount.currency, and use minor units without repeating precision. - Request-side rate inputs are grouped under
rates. - Item
priceis sent and returned in minor units. - Item
total_priceis returned in minor units. amount.shippingis sent and returned in minor units.rates.discountandrates.vatare rates in percent, not monetary amounts.- Invoice-level response
amountis compact: sharedcurrencyandprecisionare declared once, and each monetary field is returned as a minor-unit string. - Item
priceandtotal_priceare minor-unit strings. Useamount.currency+amount.precisionfor conversion:major = minor / 10^amount.precision.
Swiss e-bill structured fields. In addition to the free reference string and the global
rates.vat percentage, the V3 request accepts the structured Swiss e-bill fields a fully compliant
QR-bill and Swico S1 accounting block require. All are optional; an invoice that omits them behaves
exactly as before.
- reference type (
reference_type) and structured reference number (reference_number): when a number is supplied it drives the QR-bill payment reference. The type stays bound to the account — a QR-IBAN always emits a QRR reference, a standard IBAN a SCOR or none — so a mismatched type is corrected to what the account admits rather than rejected. A supplied number that the resolved account cannot carry (for example a non-QRR number on a QR-IBAN source) is a different case: it is rejected with a validation error, so the reference is never silently dropped from an invoice the payer would then be unable to pay by QR. - per-line VAT vs the global rate: VAT is charged per line via each item's
mwst_rate. The globalrates.vatpercentage is retained only for compatibility and is not applied on write — a request that sets a non-zerorates.vatwhile no line carries anmwst_rateis rejected, because the VAT would otherwise be discarded silently. - the creditor's VAT/UID number (
vat_number, Swico S1 tag 30) — accepts the formatted UIDCHE-###.###.###(optionally with the VAT marker MWST/TVA/IVA) or the bare nine digits. - payment conditions / discount terms (
payment_conditions, Swico S1 tag 40) — one or morediscount:dayspairs, e.g.2:10;0:30. - a per-line VAT (MWST) rate (item
mwst_rate) — one of the four Swiss rates8.1,2.6,3.8or0. An omitted rate is treated as0.
When per-line rates are present the response carries an amount.vat_by_rate breakdown — one entry
per rate, each with its net and VAT in minor units, highest net first — as Swiss VAT law (MWSTG
Art. 26) requires for a mixed-rate invoice. On such an invoice the VAT is computed per rate and
summed, and that sum is what amount.vat_amount, the stored total (amount.total) and the QR
payment amount all use — so the amount on the payment slip, the totals block and the per-rate Swico
S1 breakdown always reconcile. A counterparty-native (v3) invoice is always charged from its per-line
rates: an invoice whose every line is 0 (a VAT-free / export bill) owes no VAT, whatever the
rates.vat percentage in the request — the blended percentage is never applied to it. The blended
rates.vat fallback is reserved for genuine legacy records that predate per-line rates, keeping
their stored totals unchanged. The per-rate breakdown (amount.vat_by_rate) is a v3-only field:
a legacy record omits it and reports only the blended amount.vat_amount, so the two never
disagree.
Structured fields on a v1/v2 read. The four structured e-bill fields (vat_number,
payment_conditions, reference_type, reference_number) live on the shared invoice record, so an
invoice that was given any of them through the v3 API returns them on a v1/v2 read too. They are
documented as optional additive fields on the v1/v2 invoice response; an invoice never touched by v3
omits them and its v2 shape is unchanged. The QR payload's
billing-information field carries the assembled Swico S1 string; its free-text values (invoice
number, customer reference, payment conditions) are escaped so a slash a user types cannot forge an
extra S1 tag. An invalid structured field is rejected with a field-named validation error naming the
offending field.
Request Body:
{
"invoice_number": "INV-V3-000001",
"description": "Monthly services",
"reference": "REF-2024-04",
"reference_type": "QRR",
"reference_number": "210000000003139471430009017",
"vat_number": "CHE-106.017.086 MWST",
"payment_conditions": "2:10;0:30",
"status": "draft",
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"source": {
"counterparty_id": "11111111-1111-1111-1111-111111111111",
"cp_account_id": "22222222-2222-2222-2222-222222222222",
"cp_address_id": "33333333-3333-3333-3333-333333333333"
},
"destination": {
"counterparty_id": "44444444-4444-4444-4444-444444444444"
},
"amount": {
"currency": "CHF",
"shipping": "0"
},
"rates": {
"discount": 0,
"vat": 7.70
},
"items": [
{
"title": "Consulting",
"description": "Standard-rate services",
"quantity": 1,
"price": "720000",
"mwst_rate": 8.1
},
{
"title": "Publications",
"description": "Reduced-rate goods",
"quantity": 1,
"price": "90000",
"mwst_rate": 2.6
}
],
"metadata": {
"department": "IT",
"project_code": "P123"
}
}Success Response (201):
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"invoice_number": "INV-V3-000001",
"status": "draft",
"reference": "REF-2024-04",
"reference_type": "QRR",
"reference_number": "210000000003139471430009017",
"vat_number": "CHE-106.017.086 MWST",
"payment_conditions": "2:10;0:30",
"source": {
"counterparty_id": "11111111-1111-1111-1111-111111111111",
"cp_account_id": "22222222-2222-2222-2222-222222222222",
"cp_address_id": "33333333-3333-3333-3333-333333333333"
},
"destination": {
"counterparty_id": "44444444-4444-4444-4444-444444444444",
"cp_account_id": null,
"cp_address_id": "66666666-6666-6666-6666-666666666666"
},
"amount": {
"currency": "CHF",
"precision": 2,
"subtotal": "810000",
"shipping": "0",
"discount_amount": "0",
"vat_amount": "60660",
"vat_by_rate": [
{ "rate": 8.1, "net": "720000", "vat": "58320" },
{ "rate": 2.6, "net": "90000", "vat": "2340" }
],
"total": "870660"
},
"rates": {
"discount": 0,
"vat": 7.7
},
"items": [
{
"id": "987fcdeb-51a2-43d7-9012-345678901234",
"title": "Consulting",
"quantity": 1,
"price": "720000",
"total_price": "720000",
"mwst_rate": 8.1
},
{
"id": "aaaabbbb-cccc-dddd-eeee-ffff00001111",
"title": "Publications",
"quantity": 1,
"price": "90000",
"total_price": "90000",
"mwst_rate": 2.6
}
]
}Interpretation example:
amount.currency = "CHF"amount.precision = 2- item
price = "1050"means CHF 10.50 - item
total_price = "2100"means CHF 21.00
Error Responses:
- 400: Invalid input data. For an outgoing (
receiver) invoice in any status other thandraft, this includes a source counterparty that resolves to no IBAN (source.counterparty_id) or an explicitly selected source account without one (cp_account_id). A received (sender) invoice does not require the supplier IBAN, so it is not rejected on that basis - 401: Unauthorized
- 403: Insufficient permissions
- 409: The
invoice_numberis already used by another invoice of the same customer. A client-supplied duplicate returnsinvoices.duplicate_invoice_number; a generated collision returnsinvoices.invoice_number_collisionand can be retried because the sequence has advanced - 500: Internal server error
Create Invoice
POST /v2/invoices
Create a new invoice with automatic numbering and PDF generation.
Request Body:
{
"description": "Monthly services",
"status": "draft",
"party_type": "receiver",
"due_date": "2024-04-01T00:00:00Z",
"currency": "CHF",
"iban": "CH93 0076 2011 6238 5295 7",
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"name_from": "Sender Company AG",
"address_from": "Bahnhofstrasse 1",
"zip_from": "8001",
"city_from": "Zürich",
"country_from": "Switzerland",
"name_to": "Receiver GmbH",
"address_to": "Hauptstrasse 10",
"zip_to": "3000",
"city_to": "Bern",
"country_to": "Switzerland",
"shipping": 10.00,
"discount": 5.00,
"vat": 7.70,
"items": [
{
"title": "Service A",
"description": "Monthly subscription",
"quantity": 1,
"unit_price": 100.00
}
],
"active": true,
"metadata": {
"department": "IT",
"project_code": "P123"
}
}Success Response (201):
{
"invoice": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"invoice_number": "INV-2024-000001",
"status": "draft",
"total": 112.31,
"created_at": "2024-03-21T10:00:00Z"
},
"items": [
{
"id": "987fcdeb-51a2-43d7-9012-345678901234",
"title": "Service A",
"quantity": 1,
"unit_price": 100.00,
"total": 100.00
}
]
}Error Responses:
- 400: Invalid input data
- 401: Unauthorized
- 403: Insufficient permissions
- 409: The
invoice_numberis already used by another invoice of the same customer. A client-supplied duplicate returnsinvoices.duplicate_invoice_number; a generated collision returnsinvoices.invoice_number_collisionand can be retried because the sequence has advanced - 500: Internal server error
Process Invoice OCR
POST /v1/invoices/ocr
Extract invoice details from documents using OCR technology.
Request Body:
{
"base64_file": "JVBERi0xLjcKCjEgMCBvYmogICUgZW50...",
"options": {
"language": "en",
"extract_items": true,
"detect_qr": true
}
}Success Response (200):
{
"extracted_data": {
"invoice_number": "INV-2024-000123",
"date": "2024-03-21",
"total": 112.31,
"currency": "CHF",
"iban": "CH93 0076 2011 6238 5295 7",
"recipient": {
"name": "Receiver GmbH",
"address": "Hauptstrasse 10",
"zip": "3000",
"city": "Bern"
},
"items": [
{
"description": "Service A",
"quantity": 1,
"price": 100.00
}
]
},
"confidence_score": 0.95
}Error Responses:
- 400: Invalid file format
- 401: Unauthorized
- 413: File too large
- 422: Processing failed
- 500: Internal server error
Process Invoice OCR V2
POST /v2/invoices/ocr
Extract invoice details from documents using OCR technology with automatic recipient search, draft upload creation, and draft transfer prefilling.
Request Body:
{
"base64_file": "JVBERi0xLjcKCjEgMCBvYmogICUgZW50...",
"customer_id": "550e8400-e29b-41d4-a716-446655440000"
}Success Response (202):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 202,
"upload_id": "123e4567-e89b-12d3-a456-426614174002",
"message": "Job is processing"
}Poll GET /v2/invoices/ocr/{id} for completion. When status is 200, the response includes transfer_id, counterparty_match, counterparty_id, cp_account_id, and result.
Error Responses:
- 400: Invalid input or missing required fields
- 401: Unauthorized
- 403: Insufficient permissions
- 413: File too large
- 500: Internal server error
Check Invoice OCR Status
GET /v1/invoices/ocr/{id}
Check the status of an OCR processing job.
Path Parameters:
- id: Job UUID
Success Response (200):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 200,
"result": {
"total": "112.31",
"customer_name": "Receiver GmbH",
"customer_email": "info@receiver.ch",
"currency_code": "CHF",
"currency_symbol": "CHF",
"due_date": "2024-04-01",
"subtotal": "100.00",
"sales_tax_percentage": "7.7",
"sales_tax_amount": "7.70",
"invoice_number": "INV-2024-000123",
"reference": "REF123",
"bic": "BCGEVBGG",
"iban": "CH93 0076 2011 6238 5295 7"
}
}Error Responses:
- 400: Invalid job ID
- 401: Unauthorized
- 403: Forbidden - job doesn't belong to user
- 404: Job not found
- 500: Internal server error
Check Invoice OCR Status V2
GET /v2/invoices/ocr/{id}
Check the status of an OCR processing job with counterparty destination resolution.
Path Parameters:
- id: Job UUID
Success Response (200):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 200,
"upload_id": "123e4567-e89b-12d3-a456-426614174002",
"transfer_id": "123e4567-e89b-12d3-a456-426614174003",
"counterparty_match": "found",
"counterparty_id": "123e4567-e89b-12d3-a456-426614174001",
"cp_account_id": "123e4567-e89b-12d3-a456-426614174004",
"message": "Job completed",
"result": {
"total": "112.31",
"customer_name": "Receiver GmbH",
"customer_email": "info@receiver.ch",
"currency_code": "CHF",
"currency_symbol": "CHF",
"due_date": "2024-04-01",
"subtotal": "100.00",
"sales_tax_percentage": "7.7",
"sales_tax_amount": "7.70",
"invoice_number": "INV-2024-000123",
"reference": "REF123",
"bic": "BCGEVBGG",
"iban": "CH93 0076 2011 6238 5295 7"
}
}Processing Response (202):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 202,
"upload_id": "123e4567-e89b-12d3-a456-426614174002",
"message": "Job is processing"
}Failed Response (500):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 500,
"message": "Job failed"
}Notes:
upload_ididentifies the draft upload created for the scanned document; present while the job is processing and when complete; omitted when the job fails and draft resources are cleaned uptransfer_ididentifies the draft transfer prefilled from OCR data (only whenstatusis200)counterparty_matchisfoundwhen an existing destination counterparty matched, orcreatedwhen a new one was created (only whenstatusis200)counterparty_idandcp_account_ididentify the resolved destination selectors used by the prefilled draft transfertransaction_idis a legacy customer-transaction draft identifier; new jobs usetransfer_idinsteadrecipientandrecipient_matchare legacy fields for older jobs; new OCR jobs expose counterparty fields instead
Error Responses:
- 400: Invalid job ID
- 401: Unauthorized
- 403: Forbidden - job doesn't belong to user
- 404: Job not found
- 500: Internal server error
Process Invoice OCR V3 (authoring)
POST /v3/invoices/ocr
Extract a Swiss-shaped invoice from an uploaded document to prefill the create-invoice form. This is the authoring counterpart to the pay-a-bill OCR: it resolves no counterparty and creates no draft transfer. Instead it returns the biller and payer parties, per-line items with their MWST (Swiss VAT) rate, the QR-bill reference type, and the Swico billing-information tags, so the operator can review each field against the rendered document and correct it before saving the invoice. Requires the Bedrock language-model driver.
Request Body:
{
"base64_file": "JVBERi0xLjcKCjEgMCBvYmogICUgZW50..."
}Success Response (201):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 202,
"message": "Job is processing"
}Poll GET /v3/invoices/ocr/{id} for completion.
The original uploaded document is stored best-effort, scoped to the request's customer, so the invoice authored from this scan can show its source again on edit. A request that carries no customer, or a storage failure, simply leaves the invoice without a linked original — the scan still runs and the fields are still extracted. When the invoice is later created, its link to that stored document is only made if the create is issued under the same customer while the job has completed; otherwise the invoice is saved without the link. A failed scan drops the stored original rather than leaving it orphaned.
Error Responses:
- 400: Invalid input or missing required fields
- 401: Unauthorized
- 403: Insufficient permissions
- 413: File too large
- 500: Internal server error
Check Invoice OCR Status V3 (authoring)
GET /v3/invoices/ocr/{id}
Check the status of an authoring OCR job. When complete, the Swiss-shaped result is returned for prefilling the create-invoice form.
Path Parameters:
- id: Job UUID
Success Response (200):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 200,
"message": "Job completed",
"result": {
"invoice_number": "INV-2024-000123",
"reference": "210000000003139471430009017",
"reference_type": "QRR",
"reference_number": "210000000003139471430009017",
"vat_number": "CHE-116.281.710",
"payment_conditions": "30 days net",
"currency": "CHF",
"issue_date": "2024-03-01",
"due_date": "2024-04-01",
"creditor": {
"name": "Muster AG",
"address": "Musterstrasse 1, 8000 Zurich",
"iban": "CH9300762011623852957",
"bic": "BCGEVBGG",
"bank": "Banque Cantonale de Geneve"
},
"debtor": {
"name": "Receiver GmbH",
"email": "info@receiver.ch"
},
"items": [
{ "title": "Consulting", "quantity": "2", "price": "500.00", "mwst_rate": "8.1" },
{ "title": "Books", "quantity": "1", "price": "40.00", "mwst_rate": "2.6" }
],
"subtotal": "1040.00",
"discount": "",
"shipping": "",
"vat_amount": "82.04",
"total": "1122.04"
}
}Processing Response (202):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 202,
"message": "Job is processing"
}Failed Response (500):
{
"job_id": "123e4567-e89b-12d3-a456-426614174000",
"status": 500,
"message": "Job failed"
}Notes:
creditoris the biller (the party to be paid); itsiban/bic/bankare the payment coordinates.debtoris the bill-to party.reference_typeisQRR(Swiss QR reference),SCOR(ISO 11649 creditor reference), orNON(no structured reference).mwst_rateon each line is the Swiss VAT percentage (8.1,2.6,3.8, or0).vat_numberandpayment_conditionsare the Swico S1 billing-information tags.- Every field is best-effort; unread fields come back as empty strings for the operator to complete.
Error Responses:
- 400: Invalid job ID
- 401: Unauthorized
- 403: Forbidden - job doesn't belong to user
- 404: Job not found or not an authoring OCR job
- 500: Internal server error
Read QR Code
POST /v1/invoices/qrcode
Extract payment information from Swiss QR-bills.
Request Body:
{
"base64_file": "JVBERi0xLjcKCjEgMCBvYmogICUgZW50...",
"options": {
"page": 1,
"validate_format": true
}
}Success Response (200):
{
"qr_info": {
"iban": "CH93 0076 2011 6238 5295 7",
"creditor": {
"name": "Sender Company AG",
"address": "Bahnhofstrasse 1",
"zip": "8001",
"city": "Zürich",
"country": "CH"
},
"amount": 112.31,
"currency": "CHF",
"reference": "RF18539007547034",
"message": "Invoice INV-2024-000123"
}
}Error Responses:
- 400: Invalid file format or no QR code found
- 401: Unauthorized
- 413: File too large
- 422: Invalid QR code format
- 500: Internal server error
Get Invoice By ID
GET /v2/invoices/{id}
Retrieve complete invoice details including line items.
Path Parameters:
- id: Invoice UUID
Query Parameters:
- include_items: Include line items (default: true)
- include_metadata: Include metadata (default: false)
Success Response (200):
{
"invoice": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"invoice_number": "INV-2024-000001",
"description": "Monthly services",
"status": "draft",
"party_type": "receiver",
"due_date": "2024-04-01T00:00:00Z",
"currency": "CHF",
"iban": "CH93 0076 2011 6238 5295 7",
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"name_from": "Sender Company AG",
"address_from": "Bahnhofstrasse 1",
"zip_from": "8001",
"city_from": "Zürich",
"country_from": "Switzerland",
"name_to": "Receiver GmbH",
"address_to": "Hauptstrasse 10",
"zip_to": "3000",
"city_to": "Bern",
"country_to": "Switzerland",
"subtotal": 100.00,
"shipping": 10.00,
"discount": 5.00,
"vat": 7.70,
"total": 112.31,
"created_at": "2024-03-21T10:00:00Z",
"metadata": {
"department": "IT",
"project_code": "P123"
}
},
"items": [
{
"id": "987fcdeb-51a2-43d7-9012-345678901234",
"invoice_id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Service A",
"description": "Monthly subscription",
"quantity": 1,
"unit_price": 100.00,
"total": 100.00
}
]
}Error Responses:
- 400: Invalid invoice ID format
- 401: Unauthorized
- 403: Insufficient permissions
- 404: Invoice not found
- 500: Internal server error
Get All Invoices
GET /v2/invoices
List all invoices with support for filtering, sorting, and stacking.
Query Parameters:
- status: Filter by invoice status (draft/issued/sent/overdue/paid; legacy pending is accepted as issued)
- date_from: Start date (format: 2024-03-21T00:00:00Z)
- date_to: End date (format: 2024-03-21T23:59:59Z)
- currency: 3-letter currency code (e.g., CHF)
- customer_id: Customer UUID
- limit: Records per page (default: 10, max: 50)
- offset: Pagination offset (default: 0)
- sort: Sort fields with optional - prefix (e.g., -created_at,status)
- stack: Stack by field with optional format (e.g., created_at[DD-MM-YYYY])
Query Parameters Documentation
Search Parameters
Use search.field.operator=value format for filtering:
search.status.eq=draft
search.amount.gt=1000
search.currency.in=CHF,EUR
search.active=trueFree-text search (_text)
A single free-text term (search._text.like=acme) matches an invoice on any of its own text
fields — invoice number, description, free reference, status, currency — and on the party
names it is billed between. The debtor (destination) and creditor (source) counterparty names are
matched even though a name is not stored on the invoice row itself, so searching a client's name
returns every invoice raised to or from that party. Each party contributes its legal name, its
display name and its alias — all three, not one as a fallback for another — and both sides are
searched with one term. The alias matters because it is what the list itself prints in the "Bill
to" column whenever a counterparty has one, so the string on screen is the string that searches. Every field the
free-text term matches — the invoice's own text fields and the denormalized party names — is backed
by a trigram search index, so the whole free-text match stays fast as the customer's invoice and
counterparty lists grow. This applies to the selector-based V3 invoice list; it needs no change to
the request shape.
Available operators:
eq: Equal tone: Not equal togt: Greater thangte: Greater than or equal tolt: Less thanlte: Less than or equal toin: In array (comma-separated values)nin: Not in array (comma-separated values)like: Contains string (case-insensitive)ilike: Contains string (case-sensitive)
Sort Parameters
Use sort=field for ascending or sort=-field for descending order:
sort=created_at # ascending
sort=-amount # descending
sort=status,-date # multiple fieldsStack Parameters
Group results by field(s):
stack=status # group by status
stack=currency,status # group by currency then statusPagination
Control the number of results:
limit=25 # items per page (default: 10, max: 100)
offset=50 # skip first 50 itemsExample
/v1/invoices?search.status.in=draft,issued&search.amount.gt=1000&sort=-created_at&stack=status&limit=25This will:
- Find invoices with status 'draft' or 'issued'
- Filter for amounts greater than 1000
- Sort by creation date (newest first)
- Group results by status
- Return up to 25 results per page
Sorting Examples:
sort=created_at # Sort by created_at ascending
sort=-created_at # Sort by created_at descending
sort=-due_date,status # Sort by due_date descending, then status ascendingStacking Examples:
stack=created_at[DD-MM-YYYY] # Group by created_at, format as DD-MM-YYYY
stack=total[#,##0.00] # Group by total, format with thousand separators
stack=status # Group by status as-is
stack=-currency # Group by currency in reverse orderRegular Response (200):
{
"data": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"invoice_number": "INV-2024-000001",
"status": "draft",
"total": 112.31,
"created_at": "2024-03-21T10:00:00Z"
}
],
"total": 100,
"has_more": true
}Stacked Response (200):
{
"data": {
"24-03-2024": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"invoice_number": "INV-2024-000001",
"status": "draft",
"total": 112.31,
"created_at": "2024-03-24T10:00:00Z"
},
{
"id": "123e4567-e89b-12d3-a456-426614174001",
"invoice_number": "INV-2024-000002",
"status": "paid",
"total": 245.00,
"created_at": "2024-03-24T14:30:00Z"
}
],
"23-03-2024": [
{
"id": "123e4567-e89b-12d3-a456-426614174002",
"invoice_number": "INV-2024-000003",
"status": "issued",
"total": 89.99,
"created_at": "2024-03-23T09:15:00Z"
}
]
},
"total": 100,
"has_more": true
}Error Responses:
- 400: Invalid query parameters
- 401: Unauthorized
- 403: Insufficient permissions
- 500: Internal server error
Update Invoice
PUT /v2/invoices/{id}
Update existing invoice details and items. Only draft invoices can be edited; non-draft invoices only accept valid lifecycle status transitions.
Path Parameters:
- id: Invoice UUID
Request Body:
{
"status": "issued",
"due_date": "2024-04-15T00:00:00Z",
"items": [
{
"id": "987fcdeb-51a2-43d7-9012-345678901234",
"quantity": 2,
"unit_price": 95.00
}
]
}Success Response (200):
{
"invoice": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"status": "issued",
"due_date": "2024-04-15T00:00:00Z",
"total": 209.62,
"updated_at": "2024-03-21T11:00:00Z"
}
}Error Responses:
- 400: Invalid invoice ID or request body
- 401: Unauthorized
- 403: Insufficient permissions
- 404: Invoice not found
- 409: The invoice status changed concurrently (
invoices.status_conflict), or a suppliedinvoice_numberis already used by another invoice of the same customer (invoices.duplicate_invoice_number) - 422: Invalid status transition
- 500: Internal server error
Concurrent edits: the update is applied only if the invoice status has not changed while the request is processed. If another user or process moved it, the whole update — details, items and party links alike — is rejected with 409 and nothing is written. Re-read the invoice before retrying and confirm the edit still applies; no partial change was left behind.
Delete Invoice
DELETE /v1/invoices/{id}
Remove invoice and associated items. Only draft invoices can be deleted.
Path Parameters:
- id: Invoice UUID
Success Response (200):
{
"message": "Invoice successfully deleted",
"id": "123e4567-e89b-12d3-a456-426614174000"
}Error Responses:
- 400: Invalid invoice ID
- 401: Unauthorized
- 403: Insufficient permissions
- 404: Invoice not found
- 422: Cannot delete non-draft invoice
- 500: Internal server error
Get Customer Invoices
GET /v1/invoices/customers/{id}
List all invoices for a specific customer with filtering options.
Path Parameters:
- id: Customer UUID
Query Parameters:
- status: Filter by invoice status (draft/issued/sent/overdue/paid; legacy pending is accepted as issued)
- date_from: Start date (format: 2024-03-21T00:00:00Z)
- date_to: End date (format: 2024-03-21T23:59:59Z)
- currency: 3-letter currency code (e.g., CHF)
Success Response (200):
{
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"total_invoices": 50,
"total_amount": 5615.50,
"currency_breakdown": {
"CHF": 4500.00,
"EUR": 1115.50
},
"invoices": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"invoice_number": "INV-2024-000001",
"status": "paid",
"currency": "CHF",
"total": 112.31,
"created_at": "2024-03-21T10:00:00Z"
}
]
}Error Responses:
- 400: Invalid customer ID or query parameters
- 401: Unauthorized
- 403: Insufficient permissions
- 404: Customer not found
- 500: Internal server error
Get Invoice PDF
GET /v1/invoices/{id}/pdf
Generate or retrieve PDF version of an invoice with embedded QR code.
Path Parameters:
- id: Invoice UUID
Query Parameters:
- template: PDF template name (optional, default: "default")
- language: Language code for PDF content (optional, default: "en")
- include_qr: Include QR code in PDF (optional, default: true)
Success Response (200):
{
"content": "JVBERi0xLjcKCjEgMCBvYmogICUgZW50...",
"filename": "INV-2024-000001.pdf",
"content_type": "application/pdf",
"size": 123456
}Error Responses:
- 400: Invalid invoice ID
- 401: Unauthorized
- 403: Insufficient permissions
- 404: Invoice not found
- 422: PDF generation failed
- 500: Internal server error
Original uploaded document (received invoices)
GET /v3/invoices/{id}/original-document
When a received (incoming / accounts-payable) invoice is authored by scanning a supplier's bill, the operator uploads the original document — a PDF or an image — and the system reads its details. That original is kept with the invoice so it can be shown again later: when someone opens the received invoice to correct it, the edit screen displays the scanned source alongside the fields, exactly as it appeared during the first review. This lets an operator check every value against the real document rather than trusting the extracted text alone.
Who uses it: operations and accounts-payable staff reviewing or correcting a received supplier invoice.
How access works: anyone allowed to read the invoice may retrieve its original document — the permission follows the invoice, not the person who happened to run the scan, so a colleague who picks up the invoice sees the same source. The document is returned as the file content, its media type, and a display name.
When there is none: an invoice created by hand (no scan), or one whose scan was abandoned before it was saved, simply has no original on file. The edit screen then shows a "document unavailable" note and all fields remain fully editable. The stored original is a copy of a customer document and is held with the customer's other files; a failed scan's upload is discarded rather than retained.
Path Parameters:
- id: Invoice UUID
Error Responses:
- 400: Invalid invoice ID
- 401: Unauthorized
- 403: Insufficient permissions
- 404: Invoice not found, or it has no linked original document
Get Next Invoice Number
GET /v1/invoices/customers/{id}/next-number
Preview the number the next create would most likely take for a customer. Nothing is reserved: the call writes nothing, and a concurrent create can take that number first. expires_at bounds how long the preview is worth showing, not how long the number is held.
Path Parameters:
- id: Customer UUID
Success Response (200):
{
"invoice_number": "INV-20240321-000001",
"expires_at": "2024-03-21T10:05:00Z"
}Error Responses:
- 400: Invalid customer ID
- 401: Unauthorized
- 403: Insufficient permissions
- 500: Internal server error
Integration
This section describes how a Corebanq deployment issues invoices carrying a Swiss QR payment part, how that payment part is generated and validated, and how an inbound QR-bill received as a file is read back into a payment instruction. It is the operator- and integrator-facing companion to the Invoices manual.
Who uses this. Deployment operators when configuring invoice templates and numbering; billing and client-service staff when issuing or reconciling an invoice; integrators connecting a customer system to invoice issuance.
How it works. An invoice is raised against a customer with its lines, currency, due date and the account to be credited. On finalisation the platform composes the Swiss Payments Code payload from the creditor account, the amount and the payment reference, renders it as a QR code with the Swiss cross, and embeds it in the invoice document in the recipient's language. A payment made against that QR code carries the reference back, which is what allows the receipt to be matched to the invoice automatically.
What operators do. Configure the invoice document templates and the languages offered, confirm the creditor accounts are correct for the environment, and verify that a generated payment part is accepted by a Swiss banking application.
Outcomes and side effects. A finalised invoice produces a document that a Swiss payer can settle by scanning, and a receivable the platform can reconcile. The invoice's status tracks its commercial lifecycle from draft through to paid.
Related manuals: Accounts, Ledgers, Transfers.
Standard conformity
The payment part conforms to the Swiss QR-Bill standard version 2.3 as published by SIX. The payload is the Swiss Payments Code — a fixed sequence of fields in a defined order, terminated by the trailer the standard prescribes. Field order, occupancy and length are not negotiable: a payment part that deviates is rejected by the payer's banking application, so conformity is checked at generation time rather than discovered by the customer.
What is checked before a code is printed. Every payment part is judged against the standard on its way to the page, and an invoice that fails any of these is issued without a payment part rather than with one a bank refuses:
- the creditor account's own check digit, and that it is a Swiss or Liechtenstein account;
- the creditor's name, postal code, town and country, which the standard requires on every payment part;
- the amount, which must be payable — between 0.01 and 999999999.99, with no more than two decimals;
- the currency, which the standard limits to Swiss francs and euro;
- the payment reference against the rules for its type, and that a bill claiming no reference carries none;
- every field's length, and the two remittance fields against their shared limit as well as their own;
- the country codes, and the structured address form the standard now requires;
- the recipient's address, which must be either complete or absent altogether — the standard does not admit a half-filled one;
- the total size, so the code still fits the printed square.
Some of these change what existing invoices render. A bill saved without a creditor name, one saved without the creditor's town or country, and one whose total is zero or negative previously carried a payment part and now do not: none can be collected by scanning, so the bill is issued without one rather than with a code the payer's bank turns away. A credit note is the usual case of the last, and it is refunded rather than collected.
The recipient's address changes in the opposite direction. A bill that names no recipient, or names one without a postal code or town, now leaves the recipient block off the payment part entirely instead of printing an empty one. The payer completes it by hand, exactly as the standard intends — and the bill stays scannable, which the half-filled block would have prevented. The printed Payable by field follows the same rule as the scannable code, so the two always say the same thing: where the recipient is not fully known, the field is left blank for the payer to fill in rather than showing a name the code does not carry.
Text is adapted rather than rejected where it safely can be. A name or address carrying characters the standard does not admit is converted to the closest permitted form — a euro sign becomes EUR, a typographic dash becomes a hyphen, a ligature is split back into its letters, an accented letter with no equivalent keeps its base letter — and anything with no sensible equivalent is dropped. A field longer than the standard allows is shortened to fit. Both are preferable to the alternative, which is a scannable code no bank accepts, or no payment part at all.
The same adaptation now covers the structured billing information, which carries the invoice number, the customer's own reference and the payment conditions — the one part of the payment part a user types freely. A dash pasted out of a word processor used to be enough to cost the whole bill its payment part; it is converted like any other text. Where that information is simply too long for its field, it is left off and the payment part is still printed: the payer does not need it in order to pay, and a bill with no code cannot be paid at all.
What the payment part carries
| Element | Business meaning |
|---|---|
| Creditor account | The IBAN to be credited. Must be a Swiss or Liechtenstein account for a Swiss QR-bill to be produced at all. |
| Creditor | The institution or customer being paid, with its structured address. |
| Amount and currency | The sum due. Only Swiss francs and euro are admitted by the standard. |
| Ultimate debtor | The payer, where known, with their structured address. |
| Payment reference | The reference that ties a received payment back to this invoice. |
| Unstructured message | Free-text information for the payer. |
Where the creditor account or the currency does not admit a Swiss QR-bill, no payment part is produced. The invoice is still issued — it simply carries no scannable payment part, and the payer settles it by other means. This is a business outcome, not a failure.
Payment references and their check digits
The standard admits three reference types, and which one applies is decided by the creditor account rather than by preference:
- QR reference — a 27-digit reference used with a QR-IBAN. It ends in a check digit computed by the recursive mod-10 method the standard prescribes. A reference whose check digit does not agree with its body is refused at generation, because a payer's bank would refuse it too.
- Creditor Reference — the international
RFreference used with an ordinary IBAN, validated by the mod-97 method. Case and spacing are normalised before the check is applied. - No reference — permitted where the creditor account admits it. The payer is then identified from the unstructured message and the credit is matched manually.
Pairing the wrong reference type with an account is the most common cause of a QR-bill being rejected in the field, so the pairing is enforced rather than left to the caller. An all-zero reference passes the arithmetic check but carries no information, and is treated as absent.
Documents and languages
The invoice document is rendered from a template. Several templates can be configured for a deployment, and the language of the document — including the payment-part headings the standard prescribes — follows the recipient's language preference, with the deployment's default language as fallback.
The payment part itself is not translated field by field: its headings are the standard's own, in the standard's wording for the chosen language. Only the commercial part of the invoice — line descriptions, terms, the covering text — comes from the deployment's template.
Reading a QR-bill back
A QR-bill received as a PDF or an image can be read back, so that a payer does not have to retype a
creditor's details. The document is scanned for a Swiss Payments Code payload, up to the page limit
set by invoices.limit_pages, and the payload is decoded into its fields.
Decoding is not the same as accepting. A decoded payload is validated before anything is offered as a payment instruction: the field sequence and the trailer must match the standard, the creditor account must be well formed, the amount and currency must be admissible, and the payment reference must satisfy the check digit for its type. A payload that fails any of these is reported as an invalid QR-bill rather than being partly accepted, because a partly accepted payment instruction is how money reaches the wrong account.
The page limit exists so that a very large document cannot occupy the platform indefinitely while it is searched for a code that may not be there.
Invoice lifecycle
An invoice's status describes where it stands commercially:
| Status | Meaning |
|---|---|
draft | Being prepared. Its content can be edited. |
issued | Finalised and valid, but not yet sent — its content can still be corrected. pending is a legacy alias accepted for compatibility. |
sent | Shared with the customer. |
overdue | Past its due date and unpaid. Derived, never set directly. |
paid | Settled. Final. |
Once an invoice leaves draft, its content is fixed and only lifecycle transitions are accepted —
this is what allows a document already in a customer's hands to be trusted. overdue is derived
from the due date at read time rather than stored, so an invoice does not need a nightly job to
become overdue. paid is terminal.
Invoice numbers are allocated by the platform on finalisation so that the sequence carries no gaps.
Storage and transmission
Invoice documents are held in the deployment's object storage and served through the platform, so access follows the same permission rules as the rest of the customer file rather than depending on a guessable link. Documents are encrypted at rest, and every retrieval is over HTTPS with TLS. The payment reference and the creditor account are part of the customer file and carry the same retention and audit treatment as any other payment data.
Acceptance checks
- An invoice raised against a QR-IBAN produces a payment part with a QR reference whose check digit validates.
- An invoice raised against an ordinary IBAN produces a payment part with a Creditor Reference.
- An invoice in a currency the standard does not admit is issued without a payment part rather than with an invalid one.
- A generated payment part is scanned successfully by a Swiss banking application.
- The invoice document renders in each configured language, with the standard's own headings on the payment part.
- A valid QR-bill file is read back into its fields; a corrupted one is reported as invalid and offers no payment instruction.
- A non-draft invoice refuses content edits and accepts only lifecycle transitions.
- Invoice numbers are allocated without gaps.
PreStop lifecycle hook for container orchestration (AWS ECS, Kubernetes, Docker) graceful shutdown. Should only be called by container orchestrator or internal infrastructure.
Returns the generated PDF for the invoice as a base64-encoded string. The document's totals block names every amount the total is built from — discount, subtotal before tax, VAT and shipping — each printed unsigned, with the total adding the subtotal, VAT and shipping and subtracting the discount. A document stored before this release omits the shipping row: invoices carrying a shipping charge are marked by a migration and re-rendered on their first read after this release, which replaces the stored copy.