DEV Community

Cover image for A payment QR code is just twelve lines of text
Koray KÖYLÜ
Koray KÖYLÜ

Posted on • Originally published at ibanchecker.cash

A payment QR code is just twelve lines of text

If you have paid an invoice in Europe recently, you have probably pointed your banking app at a small square and watched it fill in the payee, the IBAN and the amount by itself. I run ibanchecker.cash and I spent the last few weeks on both sides of those squares: generating them and reading them back. The format turned out to be far simpler than most people expect, and a couple of its rules surprised me even after years around IBANs.

What is actually in there

The standard is EPC069-12, published by the European Payments Council. German banks market the result as a GiroCode. There is no binary framing and no JSON. The payload is plain text, twelve elements separated by line breaks. This is the exact content of the code on the cover image:

BCD
002
1
SCT
COBADEFF
Webajans Bilisim Ltd
DE89370400440532013000
EUR149.00


Invoice 2026-014
Enter fullscreen mode Exit fullscreen mode
Line Element In this code
1 Service tag always BCD
2 Version 002, which makes the BIC optional
3 Character set 1 means UTF-8
4 Identification SCT, a SEPA Credit Transfer
5 BIC COBADEFF
6 Beneficiary name who gets paid
7 IBAN the account
8 Amount currency glued to the value
9 Purpose empty here
10 Structured reference empty here
11 Unstructured reference free text
12 Beneficiary to originator may be absent entirely

A few rules worth knowing before you implement any of this. The whole payload must fit in 331 bytes. The name is capped at 70 characters and the reference at 140. The amount is euro only, from 0.01 to 999,999,999.99; the standard simply has no concept of another currency. Error correction must be level M. And there is no signature anywhere: nothing in the code proves who produced it, which is worth remembering the next time a QR sticker appears on a parking meter.

The field we got wrong

Line 6 is the beneficiary name. The account holder. The person or company the money goes to.

The first version of our generator wrote the detected bank name there. Scan such a code and your banking app politely suggests paying "Commerzbank" instead of the person behind the account. It ran like that for about three weeks before we caught it. If you take one thing from this article: line 6 is the payee, never the bank. It is an easy mistake to make because the bank name is usually the string you have just resolved from the IBAN and it is sitting right there in a variable.

Rebuilding the generator

The old tool had a Generate button. The new one updates the code live as you type, which sounds cosmetic but changes how people use it: you see the code grow as the reference gets longer, and you see the byte counter move toward the 331 limit.

Exports are SVG, high resolution PNG and a vector PDF. The PDF was the fun part: a QR code is nothing but rectangles, so instead of pulling in a PDF library we walk the SVG path data and emit the rectangles into a hand-written PDF. Zero dependencies. Dropping a logo into the center automatically raises error correction to H.

One design decision I want to defend explicitly: if you enter an amount in a currency other than the euro, the tool still generates the code but tells you plainly that banking apps outside SEPA may not read it. The alternative, silently producing something that looks official, felt wrong.

Reading codes back

The newer tool is the reverse: drop in a picture of a payment QR and it lists all twelve elements with their byte counts, measured against the limits the standard sets. Decoding happens entirely in the browser with a dynamically imported jsQR; the image is never uploaded anywhere. The bytes are re-decoded with whatever character set the payload declares, so an ISO 8859 umlaut survives.

The IBAN inside then runs through the same engine as the rest of the site: country, length, national structure, the ISO 13616 check digits, the national account check digit where a country defines one (25 countries so far; for Germany that means the per-bank Prüfziffer methods, for the UK the VocaLink modulus check) and a directory lookup for the bank and its BIC.

Two rules were fixed before the first line of code. The image never leaves the browser. And decoded text is never rendered as a clickable link, because QR phishing works precisely by making that tap convenient.

One honest limitation: Swiss QR-bills are recognized but not parsed. They follow a different standard from SIX, not the EPC one, and mapping their fields onto the EPC layout would be guessing dressed up as decoding.

About the cover image

The QR on the cover is real. I generated it with the first tool and verified it by feeding the finished image back into the second one. The IBAN in it is the well-known documentation example, DE89 3704 0044 0532 0130 00, not a live account.

Both tools are free and need no signup: the generator and the decoder.

Top comments (0)