swiss-qr-bill-mcp
by dmikushin
README.md
# swiss-qr-payment
Generator for the **Swiss QR-bill** — the payment part and receipt that every
Swiss invoice carries since the end of the orange and red payment slips.
The package covers the full standard:
* the **Swiss QR Code payload** ("Swiss Payments Code") with all 34 elements,
* **validation** of every rule of the guidelines, each check citing the
chapter it enforces,
* the **printed slip** in PDF, SVG or PNG, in German, French, Italian,
English or Romansh.
Normative sources, both published by SIX Interbank Clearing:
| Document | Used for |
|---|---|
| *Swiss Implementation Guidelines QR-bill*, v2.3 / v2.4 | data structure, validation rules, QR code parameters |
| *Style Guide QR-bill*, v1.1 | print layout, typography, headings |

*The 210 x 105 mm slip as this package renders it, here in German. It is the
specimen printed on page 19 of the Style Guide, and the test suite compares
the two run by run.*
Every millimetre in `swiss_qr_payment/layout.py` was measured from the 1:1
vector artwork of the Style Guide with `tools/measure_styleguide.py`, and the
test suite compares the rendered slip against that artwork element by element.
---
## Installation
```sh
python3 -m venv .venv
.venv/bin/pip install -e .
```
Runtime dependencies are `segno` (QR encoding) and `reportlab` (PDF and font
metrics). PNG output additionally needs `pymupdf`.
The four typefaces the guidelines permit are Arial, Frutiger, Helvetica and
**Liberation Sans**. Liberation Sans is the default: it is metrically
compatible with Arial, freely redistributable, and gets **embedded** into the
PDF, so the printout does not depend on the fonts of whoever opens the file.
---
## Library
```python
from swiss_qr_payment import Address, QRBill, PrintOptions, generate
bill = QRBill(
account="CH4431999123000889012", # IBAN or QR-IBAN
creditor=Address(
name="Max Muster & Söhne",
street="Musterstrasse",
building_number="123",
postal_code="8000",
town="Seldwyla",
country="CH",
),
amount="1949.75", # None prints a blank field
currency="CHF", # CHF or EUR
debtor=Address( # omit to print a blank field
name="Simon Muster",
street="Musterstrasse",
building_number="1",
postal_code="8000",
town="Seldwyla",
country="CH",
),
reference="210000000003139471430009017", # QRR, SCOR or nothing
unstructured_message="Auftrag vom 15.06.2020",
billing_information="//S1/10/10201409/11/170309/20/14000000/30/106017086",
alternative_procedures=["eBill/B/simon.muster@example.com"],
)
generate(bill, "bill.pdf", PrintOptions(language="de"))
```
`generate` validates the bill first and raises `ValidationError` if it
violates the standard. To inspect the findings instead:
```python
from swiss_qr_payment import validate
for message in validate(bill):
print(message) # ERROR reference: ... [IG 2.12.1]
```
### Every settable element
| Attribute of `QRBill` | Payload line | Notes |
|---|---|---|
| `account` | 4 | IBAN or QR-IBAN, `CH` or `LI` |
| `creditor` | 5–11 | structured address, mandatory |
| `amount` | 19 | `Decimal`, `str`, `int` or `None` |
| `currency` | 20 | `CHF` or `EUR` |
| `debtor` | 21–27 | structured address, may be empty |
| `reference_type` | 28 | `QRR`, `SCOR`, `NON`; inferred when omitted |
| `reference` | 29 | check digits are verified |
| `unstructured_message` | 30 | |
| `billing_information` | 32 | Swico syntax, `//S1/...` |
| `alternative_procedures` | 33–34 | at most two |
Lines 12–18 (ultimate creditor) are reserved by the standard for future use
and are always emitted empty, which is why they are not exposed.
### Print options
| Attribute of `PrintOptions` | Values | Default |
|---|---|---|
| `language` | `de`, `fr`, `it`, `en`, `rm` | `en` |
| `font_family` | `Liberation Sans`, `Arial`, `Helvetica`, `Frutiger` | `Liberation Sans` |
| `page` | `a4` (slip at the bottom of the page), `slip` (210 × 105 mm alone) | `a4` |
| `separator` | `none`, `solid`, `dashed`, `dotted` | `solid` |
| `scissors` | draw the scissors symbol on the separation lines | `True` |
| `separator_text` | print "Separate before paying in" instead of the scissors | `False` |
| `force_country_prefix` | print `CH-` in front of Swiss postal codes too | `False` |
---
## Command line
```sh
swiss-qr-bill \
--account CH4431999123000889012 \
--creditor-name "Max Muster & Söhne" \
--creditor-street Musterstrasse --creditor-building-number 123 \
--creditor-postal-code 8000 --creditor-town Seldwyla --creditor-country CH \
--amount 1949.75 --reference 210000000003139471430009017 \
--language de --output bill.pdf
```
A bill can also be described by a JSON document, which command line options
then override:
```sh
swiss-qr-bill --from-json bill.json --amount 99.90 --output bill.svg
```
Other useful modes:
```sh
swiss-qr-bill --from-json bill.json --check-only # validate, exit 1 on error
swiss-qr-bill --from-json bill.json --print-payload # dump the QR code content
swiss-qr-bill --make-qr-reference 313947143000901 # append the check digit
swiss-qr-bill --make-creditor-reference 539007547034 # build an RF reference
```
`swiss-qr-bill --help` lists every option.
---
## MCP server: letting an agent issue bills
`swiss-qr-bill-mcp` speaks the Model Context Protocol over stdio, so an agent
can produce a payment slip itself:
```json
{
"mcpServers": {
"swiss-qr-bill": {
"command": "swiss-qr-bill-mcp",
"env": { "SWISS_QR_BILL_OUTPUT_DIR": "/srv/invoices" }
}
}
}
```
Install it with `pip install swiss-qr-payment[mcp]`.
`SWISS_QR_BILL_OUTPUT_DIR` confines every file the agent writes to one
directory and refuses paths that try to leave it; without the variable, paths
are resolved against the working directory.
| Tool | What it does |
|---|---|
| `create_bill` | Writes a print-ready PDF, SVG or PNG and returns the path, what the slip shows, the QR payload and the symbol size |
| `check_bill` | Same checks without writing anything, for validating a draft |
| `make_reference` | Turns an invoice number into a QR reference or an ISO 11649 Creditor Reference with correct check digits |
| `decode_bill` | Reads the content of a Swiss QR Code that was scanned elsewhere |
| `read_bill` | Reads a file back: decodes the printed symbol with the same reader banking apps use and reports the page geometry and embedded fonts |
`create_bill` refuses to write a bill that violates the guidelines and returns
the offending rules instead, each quoting its chapter, so the agent can
correct the data rather than guess. `read_bill` closes the loop: after
issuing a bill the agent can confirm that a scanner really sees the intended
payment data before the file is sent to anyone.
---
## Examples
```sh
python3 examples/generate.py
```
writes the six reference bills of the Style Guide plus a euro bill with a
foreign debtor and a "DO NOT USE FOR PAYMENT" notification into
`examples/out/`, in German and English, together with `scan-test.pdf`,
`scan-test.svg` and `scan-test.png` for a scanning test.
---
## How the output is verified
Formal verification, not eyeballing. `pytest tests/` runs 255 checks:
**Against the official artwork.** Each of the six example bills printed in the
Style Guide is rendered, the resulting PDF is read back
(`swiss_qr_payment.probe`), and every text run is compared with the
corresponding run in SIX's own 1:1 artwork — position, baseline, font size and
right edge. The line breaks of the structured billing information come out
identical to the artwork, character for character.
**Against the standard's own data.** `tools/extract_annex_examples.py` parses
the six example tables of Annex A of the Implementation Guidelines straight
out of the PDF; the payload encoder is checked against those 34 element
values. This also reproduces a documented erratum: the Creditor Reference of
Annex A example 4 fails its own mod-97-10 check in versions 2.2, 2.3 and 2.4
of the guidelines, and the test asserts that this is detected.
**Against a real reader.** The slip is rasterised at 200, 300 and 600 dpi and
decoded with ZXing, the library behind most mobile banking scanners. The
decoded bytes must equal the generated payload exactly, including the largest
possible bill and non-ASCII content.
**Through a real MCP client.** The server is driven over stdio by an actual
client session rather than by calling its functions: the schema an agent sees,
the transport and the structured results are what is asserted, including that
a bill written through `create_bill` reads back through `read_bill` with the
identical payload, and that the output directory cannot be escaped.
**Across the backends.** The SVG is rasterised by an independent renderer
(`rsvg-convert`), its symbol is decoded, and the symbol area is compared with
the PDF rendering pixel by pixel — it comes out identical, so the two
backends cannot drift apart unnoticed.
**Known limits of the standard, recorded rather than hidden.** Two of them
are checked by tests so that they cannot be forgotten:
* The 997 of chapter 6.2 are *bytes*, not characters. A bill with every
element filled to its character limit is 869 bytes in ASCII, 939 bytes with
half the message accented, and 1009 bytes when all of it is — the last one
does not fit and is rejected.
* At 939 bytes the symbol reaches version 25 with 117 modules, which at the
46 mm mandated by chapter 6.4 gives 0.393 mm per module — below the 0.4 mm
minimum of chapter 6.3. The guidelines contradict themselves here, so the
validator issues a warning instead of pretending otherwise.
> Note on zbar: it cannot read a Swiss QR Code at all. The recognition symbol
> sits on top of the central alignment pattern; ZXing reconstructs the
> geometry and repairs the covered modules through the error correction, zbar
> gives up. `tests/test_qr.py::test_zbar_cannot_read_the_recognition_symbol`
> records this and proves the cause by decoding the same symbol without the
> cross.
### Running the tests
The layout tests need the specification PDFs, which are not redistributed
here:
```sh
python3 tools/fetch_spec.py # downloads them into spec/
.venv/bin/pip install -e ".[test]"
.venv/bin/python3 -m pytest tests/ -q
```
Tests that need a document or a library that is missing are skipped, not
failed.
Only the PDF embeds the typeface. The SVG names the font family and relies
on the viewer having it, so line breaks and ellipses — computed from the real
metrics — can look different where the font is missing. Use the PDF whenever
the printout matters.
`tools/measure_styleguide.py` works on any QR-bill PDF, including the output
of this package, and prints its geometry in slip-local millimetres — useful
for checking a printout against the standard by hand:
```sh
python3 tools/measure_styleguide.py bill.pdf 0 --kind text
```
---
## How this compares to the other Python packages
Two packages already generate Swiss QR-bills in Python. Both are honest
work, and both reproduce the same deviations, because both descend from the
same Java reference implementation, [SwissQRBill by Manuel
Bleichenbacher](https://github.com/manuelbl/SwissQRBill), whose layout
constants differ from the Style Guide artwork in places.
Measured with `tools/measure_styleguide.py` against page 19 of the Style
Guide, rendering the specimen printed there:
| | [`qrbill`](https://pypi.org/project/qrbill/) 1.2.0 | [`swiss-qr-bill`](https://pypi.org/project/swiss-qr-bill/) 0.0.31 | this package |
|---|---|---|---|
| Output formats | SVG | SVG (`PDF` and `PNG` raise `NotImplementedError`) | PDF, SVG, PNG |
| Typeface embedded | no | no | yes, in the PDF |
| "Amount" heading, payment part | — | 7.00 mm too far left | on the artwork |
| "Amount" heading, receipt | — | 10.00 mm too far left | on the artwork |
| Receipt information baselines | — | 1.31 mm too high | on the artwork |
| Blank amount field | 27 x 11 mm | — | 30 x 10 mm, as specified |
| Blank debtor field, receipt | 52 x 25 mm | — | 52 x 20 mm, as specified |
| Swiss QR Code | 45.8 mm | 46 mm | 46 mm |
| Country code on a Swiss address | — | printed as `CH 8000 Seldwyla` | omitted, as the guidelines ask |
| Checked against the artwork | no | no | every specimen, every run |
The point of the table is not that the others are careless: the deviations
are inherited, and nothing in those projects would have caught them, because
neither compares its output with the source of truth. That comparison is
what this package is built around.
---
## Layout reference
Coordinates in millimetres, origin at the top left of the 210 × 105 mm slip.
```
receipt payment part
0 5 57 62 67 113 118 205 210
0 +--------------------------+-------------------------------------------+
5 | Receipt | Payment part | Account / Payable to |
12 | Account / Payable to | | ... |
| Reference | 17 +-----------+ | Reference |
| Payable by | | Swiss QR | | Additional info |
| | | Code | | Payable by |
| | 63 +-----------+ | |
68 | Currency Amount | Currency Amount |
82 | Acceptance point| 90 ----------------------------------------
| | Name AV1: ... |
105 +--------------------------+-------------------------------------------+
```
| Element | Value |
|---|---|
| Slip | 210 × 105 mm, receipt 62 mm, payment part 148 mm |
| Margins | 5 mm on all four sides |
| Swiss QR Code | 46 × 46 mm at (67, 17), quiet zone 5 mm |
| Recognition symbol | 7 × 7 mm white square, 6 × 6 mm black square, bars 3.896 × 1.171 mm |
| Receipt | title 11 pt bold, headings 6 pt bold, values 8 pt, leading 9 pt |
| Payment part | title 11 pt bold, headings 8 pt bold, values 10 pt, leading 11 pt |
| Blank amount field | receipt 30 × 10 mm at (27, 70), payment part 40 × 15 mm at (77.7, 73.8) |
| Blank debtor field | receipt 52 × 20 mm, payment part 65 × 25 mm |
| Corner marks | 3 mm legs, 0.75 pt |
---
## Licence
MIT. The scissors outline in `swiss_qr_payment/drawing.py` is taken from the
MIT licensed reference implementation *SwissQRBill* by Manuel Bleichenbacher.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues