Home/Blog/How the QR-Referenz check digit works (Modulo 10 recursive)

How the QR-Referenz check digit works (Modulo 10 recursive)

A clear explanation of the Modulo 10 recursive algorithm used to validate the 27-digit QR payment reference.

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:

  1. Set carry = 0
  2. For each of the 26 digits (left to right): new carry = table[carry][digit]
  3. After all 26 digits: check digit = (10 − carry) % 10

If the result is 10, use 0.

A worked example

Reference data (26 digits): 21000000000313947143000901

#DigitCarry inCarry out
1204
2143
3036
4067
5071
6019
7095
8052
9024
10048
11083
12332
13126
14365
15954
16443
17730
18109
19496
20365
21052
22024
23048
24981
25019
26193

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.