CorebanqCorebanq Developer Docs
Invoices

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 of YYYYMMDD, so a number reads INV-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: with reset_period monthly, Monday 2006 renders Thursday 2006 for both February and March.
  • {YEAR} is a Go reference-date layout. Use date tokens only — 2006 year, 01 month, 02 day. 15 is 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 the YYYYMMDD default 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 is 1 — 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 to start_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 id as the identifier and treat invoice_number as 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 draft carries 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 status DELETE /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_number as 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 customers CRUD 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_number on 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, so INV-1 and INV-1 are the same number. This applies to every path that writes the number: POST /v2/invoices, POST /v3/invoices, PUT /v2/invoices/{id} and PUT /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 returns invoices.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 -> issued
  • issued/pending -> sent
  • issued/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_id and destination.counterparty_id are 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_id must belong to selected counterparty and be active.
  • Optional cp_address_id must be an active address on selected counterparty. If omitted, API picks active preferred billing/fallback address.
  • party_type sets the direction: receiver (the default when omitted) is an outgoing invoice the customer issues to bill a counterparty; sender is 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 than draft. 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_id is used when supplied; if it names a bank account (rather than an own account), the request is rejected. Otherwise own accounts that can settle amount.currency are 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 draft may be created and edited before an account is chosen, since it is not yet payable. For an outgoing invoice, leaving draft requires a source IBAN, so it is enforced when the invoice is issued, sent, or marked paid.
  • An explicit source.cp_account_id is 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 7 is returned as CH9300762011623852957 on 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 its QRR reference type instead of falling back to NON.
  • destination.cp_account_id carries no IBAN requirement, since the destination IBAN is not rendered on the invoice.
  • Every invoice has a PDF, including a draft that 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}/pdf re-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}/pdf serves 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}/pdf re-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 next GET /v1/invoices/{id}/pdf re-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 a Shipping 0.00 line.
  • While an invoice is waiting for that repair its metadata carries a transient qr_bill_needs_rerender key. It is removed once the document has been replaced. The key is reserved: it is stripped from any metadata a client sends, on every create and edit, so only a migration sets it. A v1/v2 edit additionally leaves the invoice's own metadata exactly 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. Treat metadata as 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 CHF or EUR amount, 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 as QRR; a standard IBAN is encoded as SCOR when reference is a valid ISO 11649 creditor reference (RF...) and as NON otherwise. 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, including amount.currency, and use minor units without repeating precision.
  • Request-side rate inputs are grouped under rates.
  • Item price is sent and returned in minor units.
  • Item total_price is returned in minor units.
  • amount.shipping is sent and returned in minor units.
  • rates.discount and rates.vat are rates in percent, not monetary amounts.
  • Invoice-level response amount is compact: shared currency and precision are declared once, and each monetary field is returned as a minor-unit string.
  • Item price and total_price are minor-unit strings. Use amount.currency + amount.precision for 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 global rates.vat percentage is retained only for compatibility and is not applied on write — a request that sets a non-zero rates.vat while no line carries an mwst_rate is rejected, because the VAT would otherwise be discarded silently.
  • the creditor's VAT/UID number (vat_number, Swico S1 tag 30) — accepts the formatted UID CHE-###.###.### (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 more discount:days pairs, e.g. 2:10;0:30.
  • a per-line VAT (MWST) rate (item mwst_rate) — one of the four Swiss rates 8.1, 2.6, 3.8 or 0. An omitted rate is treated as 0.

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 than draft, 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_number is already used by another invoice of the same customer. A client-supplied duplicate returns invoices.duplicate_invoice_number; a generated collision returns invoices.invoice_number_collision and 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_number is already used by another invoice of the same customer. A client-supplied duplicate returns invoices.duplicate_invoice_number; a generated collision returns invoices.invoice_number_collision and 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_id identifies 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 up
  • transfer_id identifies the draft transfer prefilled from OCR data (only when status is 200)
  • counterparty_match is found when an existing destination counterparty matched, or created when a new one was created (only when status is 200)
  • counterparty_id and cp_account_id identify the resolved destination selectors used by the prefilled draft transfer
  • transaction_id is a legacy customer-transaction draft identifier; new jobs use transfer_id instead
  • recipient and recipient_match are 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:

  • creditor is the biller (the party to be paid); its iban/bic/bank are the payment coordinates. debtor is the bill-to party.
  • reference_type is QRR (Swiss QR reference), SCOR (ISO 11649 creditor reference), or NON (no structured reference).
  • mwst_rate on each line is the Swiss VAT percentage (8.1, 2.6, 3.8, or 0).
  • vat_number and payment_conditions are 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=true

Free-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 to
  • ne: Not equal to
  • gt: Greater than
  • gte: Greater than or equal to
  • lt: Less than
  • lte: Less than or equal to
  • in: 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 fields

Stack Parameters

Group results by field(s):

stack=status           # group by status
stack=currency,status  # group by currency then status

Pagination

Control the number of results:

limit=25    # items per page (default: 10, max: 100)
offset=50   # skip first 50 items

Example

/v1/invoices?search.status.in=draft,issued&search.amount.gt=1000&sort=-created_at&stack=status&limit=25

This will:

  1. Find invoices with status 'draft' or 'issued'
  2. Filter for amounts greater than 1000
  3. Sort by creation date (newest first)
  4. Group results by status
  5. 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 ascending

Stacking 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 order

Regular 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 supplied invoice_number is 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

ElementBusiness meaning
Creditor accountThe IBAN to be credited. Must be a Swiss or Liechtenstein account for a Swiss QR-bill to be produced at all.
CreditorThe institution or customer being paid, with its structured address.
Amount and currencyThe sum due. Only Swiss francs and euro are admitted by the standard.
Ultimate debtorThe payer, where known, with their structured address.
Payment referenceThe reference that ties a received payment back to this invoice.
Unstructured messageFree-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 RF reference 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:

StatusMeaning
draftBeing prepared. Its content can be edited.
issuedFinalised and valid, but not yet sent — its content can still be corrected. pending is a legacy alias accepted for compatibility.
sentShared with the customer.
overduePast its due date and unpaid. Derived, never set directly.
paidSettled. 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

  1. An invoice raised against a QR-IBAN produces a payment part with a QR reference whose check digit validates.
  2. An invoice raised against an ordinary IBAN produces a payment part with a Creditor Reference.
  3. An invoice in a currency the standard does not admit is issued without a payment part rather than with an invalid one.
  4. A generated payment part is scanned successfully by a Swiss banking application.
  5. The invoice document renders in each configured language, with the standard's own headings on the payment part.
  6. A valid QR-bill file is read back into its fields; a corrupted one is reported as invalid and offers no payment instruction.
  7. A non-draft invoice refuses content edits and accepts only lifecycle transitions.
  8. Invoice numbers are allocated without gaps.

On this page