Every QR-Referenz — the 27-digit payment reference on a Swiss QR-bill — ends with a check digit. That last digit is calculated from the preceding 26 digits using an algorithm called Modulo 10 recursive. Banks, payment systems, and any software that processes QR references use this algorithm to detect whether a reference has been corrupted or mistyped.
If you are generating QR references in your billing system, validating them before you put them on invoices, or debugging a rejection from a bank, you need to understand how this works.
What a QR-Referenz looks like
A QR-Referenz is exactly 27 digits — no letters, no separators when stored, though it is displayed in groups for readability:
21 00000 00003 13947 14300 09017
The last digit (7) is the check digit. The first 26 digits are your reference data — typically a number you assign that ties back to a specific invoice in your system.
How the algorithm works
The Modulo 10 recursive algorithm processes the 26 reference digits one at a time, left to right, carrying a single value (0–9) between steps. A fixed lookup table determines how the carry changes with each digit.
The lookup table:
Digit processed
0 1 2 3 4 5 6 7 8 9
Carry 0: 0 9 4 6 8 2 7 1 3 5
1: 9 4 6 8 2 7 1 3 5 0
2: 4 6 8 2 7 1 3 5 0 9
3: 6 8 2 7 1 3 5 0 9 4
4: 8 2 7 1 3 5 0 9 4 6
5: 2 7 1 3 5 0 9 4 6 8
6: 7 1 3 5 0 9 4 6 8 2
7: 1 3 5 0 9 4 6 8 2 7
8: 3 5 0 9 4 6 8 2 7 1
9: 5 0 9 4 6 8 2 7 1 3
Steps:
- Set carry =
0 - For each of the 26 digits (left to right): new carry =
table[carry][digit] - After all 26 digits: check digit =
(10 − carry) % 10
If the result is 10, use 0.
A worked example
Reference data (26 digits): 21000000000313947143000901
| # | Digit | Carry in | Carry out |
|---|---|---|---|
| 1 | 2 | 0 | 4 |
| 2 | 1 | 4 | 3 |
| 3 | 0 | 3 | 6 |
| 4 | 0 | 6 | 7 |
| 5 | 0 | 7 | 1 |
| 6 | 0 | 1 | 9 |
| 7 | 0 | 9 | 5 |
| 8 | 0 | 5 | 2 |
| 9 | 0 | 2 | 4 |
| 10 | 0 | 4 | 8 |
| 11 | 0 | 8 | 3 |
| 12 | 3 | 3 | 2 |
| 13 | 1 | 2 | 6 |
| 14 | 3 | 6 | 5 |
| 15 | 9 | 5 | 4 |
| 16 | 4 | 4 | 3 |
| 17 | 7 | 3 | 0 |
| 18 | 1 | 0 | 9 |
| 19 | 4 | 9 | 6 |
| 20 | 3 | 6 | 5 |
| 21 | 0 | 5 | 2 |
| 22 | 0 | 2 | 4 |
| 23 | 0 | 4 | 8 |
| 24 | 9 | 8 | 1 |
| 25 | 0 | 1 | 9 |
| 26 | 1 | 9 | 3 |
Final carry: 3
Check digit: (10 − 3) % 10 = 7
Full reference: 210000000003139471430009017 ✓
Implementing it in code
The algorithm is simple to implement in any language. Here is Python:
TABLE = [
[0, 9, 4, 6, 8, 2, 7, 1, 3, 5],
[9, 4, 6, 8, 2, 7, 1, 3, 5, 0],
[4, 6, 8, 2, 7, 1, 3, 5, 0, 9],
[6, 8, 2, 7, 1, 3, 5, 0, 9, 4],
[8, 2, 7, 1, 3, 5, 0, 9, 4, 6],
[2, 7, 1, 3, 5, 0, 9, 4, 6, 8],
[7, 1, 3, 5, 0, 9, 4, 6, 8, 2],
[1, 3, 5, 0, 9, 4, 6, 8, 2, 7],
[3, 5, 0, 9, 4, 6, 8, 2, 7, 1],
[5, 0, 9, 4, 6, 8, 2, 7, 1, 3],
]
def check_digit(digits_26: str) -> int:
carry = 0
for ch in digits_26:
carry = TABLE[carry][int(ch)]
return (10 - carry) % 10
def build_qr_reference(reference_data: str) -> str:
"""Pad to 26 digits and append check digit."""
padded = reference_data.zfill(26)
if len(padded) != 26 or not padded.isdigit():
raise ValueError("Reference data must be at most 26 digits")
return padded + str(check_digit(padded))
def validate_qr_reference(ref_27: str) -> bool:
digits = ref_27.replace(" ", "")
if len(digits) != 27 or not digits.isdigit():
return False
return int(digits[26]) == check_digit(digits[:26])
And the same logic in JavaScript:
const TABLE = [
[0,9,4,6,8,2,7,1,3,5],
[9,4,6,8,2,7,1,3,5,0],
[4,6,8,2,7,1,3,5,0,9],
[6,8,2,7,1,3,5,0,9,4],
[8,2,7,1,3,5,0,9,4,6],
[2,7,1,3,5,0,9,4,6,8],
[7,1,3,5,0,9,4,6,8,2],
[1,3,5,0,9,4,6,8,2,7],
[3,5,0,9,4,6,8,2,7,1],
[5,0,9,4,6,8,2,7,1,3],
];
function checkDigit(digits26) {
let carry = 0;
for (const ch of digits26) carry = TABLE[carry][parseInt(ch)];
return (10 - carry) % 10;
}
function validateQrReference(ref27) {
const digits = ref27.replace(/\s/g, '');
if (digits.length !== 27 || !/^\d+$/.test(digits)) return false;
return parseInt(digits[26]) === checkDigit(digits.slice(0, 26));
}
Where this validation matters
At generation time: Your billing system should calculate and append the check digit when creating a QR reference. If it hard-codes references or uses a sequence without the check digit, the references will be invalid and payers' banks will reject them.
At payment time: When a customer scans a QR-bill, their bank validates the reference before accepting the payment instruction. An invalid check digit causes an immediate rejection — the payer sees an error and the payment cannot proceed.
At reconciliation time: When you receive a payment and try to match the incoming reference to an open invoice, validating the check digit first is a fast way to discard garbled data before doing a database lookup.
The check digit does not tell you whether the reference exists in your system — it only confirms the 27-digit number is internally consistent. Matching it to an invoice is your responsibility.