If you send invoices to customers in Germany, Austria, the Netherlands or Belgium, you have probably seen a small QR code labelled "GiroCode" or "Zahlen mit Code". The customer scans it in their banking app, and IBAN, recipient, amount and reference are filled in automatically. No typos in the IBAN, no missing reference.
The format behind it is the EPC QR code, defined by the European Payments Council in the guideline EPC069-12. It is plain text, so you can generate it with any QR library. Here is what goes into it and where it usually breaks.
The payload: 12 lines of text
The QR code contains up to 12 lines, separated by a line feed (\n):
| Line | Content | Example | Notes |
|---|---|---|---|
| 1 | Service tag | BCD |
always BCD
|
| 2 | Version | 002 |
002 makes the BIC optional within the EEA |
| 3 | Character set | 1 |
1 = UTF-8 |
| 4 | Identification | SCT |
SEPA Credit Transfer |
| 5 | BIC | (empty) | optional with version 002
|
| 6 | Beneficiary name | Muster GmbH |
max. 70 characters |
| 7 | IBAN | DE89370400440532013000 |
no spaces |
| 8 | Amount | EUR149.90 |
EUR + amount with a dot, max. 999999999.99 |
| 9 | Purpose code | (empty) | optional, 4 characters |
| 10 | Structured reference | (empty) | ISO 11649 creditor reference (RF…) |
| 11 | Unstructured reference | Rechnung 2026-041 |
max. 140 characters |
| 12 | Info to the payer | (empty) | optional, max. 70 characters |
Two rules that are easy to miss:
- Use either line 10 or line 11, never both. If you have no RF creditor reference, leave line 10 empty and put the invoice number into line 11.
- The whole payload must not exceed 331 bytes. With UTF-8, umlauts count as two bytes.
Trailing empty lines may be omitted, so a minimal code ends after line 11.
A small JavaScript function
function epcPayload({ name, iban, amount, reference, bic = "" }) {
const cleanIban = iban.replace(/\s+/g, "").toUpperCase();
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(cleanIban)) {
throw new Error("Invalid IBAN format");
}
if (name.length > 70) throw new Error("Name longer than 70 characters");
if (amount < 0.01 || amount > 999999999.99) throw new Error("Amount out of range");
const lines = [
"BCD",
"002",
"1",
"SCT",
bic,
name,
cleanIban,
"EUR" + amount.toFixed(2),
"", // purpose code
"", // structured reference (RF...)
reference.slice(0, 140),
];
const payload = lines.join("\n");
if (new TextEncoder().encode(payload).length > 331) {
throw new Error("Payload exceeds 331 bytes");
}
return payload;
}
Render it with any QR library, for example the qrcode package on npm:
import QRCode from "qrcode";
const payload = epcPayload({
name: "Muster GmbH",
iban: "DE89 3704 0044 0532 0130 00",
amount: 149.9,
reference: "Rechnung 2026-041",
});
await QRCode.toFile("girocode.png", payload, {
errorCorrectionLevel: "M",
margin: 4,
width: 400,
});
The guideline specifies error correction level M. Do not drop the quiet zone (margin), some banking apps fail to detect the code without it.
Common pitfalls
-
Amount format.
EUR149,90(comma) orEUR 149.90(space) is not valid. It has to beEUR149.90. - Spaces in the IBAN. People copy IBANs in groups of four. Strip all whitespace before building the payload.
-
Line endings. Pick one line ending for the whole payload and do not mix
\r\nand\n. LF is the safest choice. - Too small on paper. On a printed A4 invoice, keep the code at least about 2 × 2 cm, otherwise older phone cameras struggle.
- The QR code is not a payment confirmation. The customer can still change the amount in their app. Reconcile against your bank statement, not against "the code was scanned".
- Test with real apps. Banking apps show errors very differently, some just ignore an invalid code. Scan your test code with at least two different apps before you ship.
Where this is used
I built this into QROVA, a small web app from Germany for dynamic QR codes and bookkeeping for small businesses. Every invoice gets an EPC QR code automatically, and there is also a standalone EPC payment QR type for flyers or donation boxes. If you just need the code in your own invoicing tool, the function above is all you need. To check your output, you can compare it with the free GiroCode generator (German UI, runs entirely in the browser, no sign-up).
Questions about the format or edge cases? Ask in the comments.
Top comments (0)