<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ERPLight Engineering]]></title><description><![CDATA[ERPLight Engineering]]></description><link>https://erplight.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/69c466fc10e664c5da037587/b55eff9a-ec1d-4bc3-b751-2c8329c2cca8.png</url><title>ERPLight Engineering</title><link>https://erplight.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 16 Sep 2026 07:48:36 GMT</lastBuildDate><atom:link href="https://erplight.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Built a Swiss QR Bill Generator in React (The Complete Technical Guide)]]></title><description><![CDATA[In 2022, Switzerland killed its 100-year-old orange paper payment slip system overnight. Every business in the country had to switch to a new format: the Swiss QR Bill — a standardized invoice with a ]]></description><link>https://erplight.hashnode.dev/how-i-built-a-swiss-qr-bill-generator-in-react-the-complete-technical-guide</link><guid isPermaLink="true">https://erplight.hashnode.dev/how-i-built-a-swiss-qr-bill-generator-in-react-the-complete-technical-guide</guid><category><![CDATA[switzerland]]></category><category><![CDATA[swiss]]></category><category><![CDATA[invoice]]></category><category><![CDATA[qr code]]></category><category><![CDATA[QR code generator]]></category><dc:creator><![CDATA[ERPLight]]></dc:creator><pubDate>Sat, 28 Mar 2026 13:35:36 GMT</pubDate><content:encoded><![CDATA[<p>In 2022, Switzerland killed its 100-year-old orange paper payment slip system overnight. Every business in the country had to switch to a new format: the <strong>Swiss QR Bill</strong> — a standardized invoice with a machine-readable QR code.</p>
<p>I'm the founder of <a href="https://erplight.ch">ERPLight</a>, an ERP system for Swiss small businesses. When we needed to generate compliant QR bills, I couldn't find a good guide explaining how the standard actually works under the hood.</p>
<p>So I built it from scratch. Here's everything I learned.</p>
<h2>What is a Swiss QR Bill?</h2>
<p>A Swiss QR Bill is a payment document defined by <a href="https://www.six-group.com/en/products-services/banking-services/payment-standardization/standards/qr-bill.html">SIX Group</a> (Switzerland's financial infrastructure provider). It replaces the old ESR/BESR payment slips and follows the ISO 20022 standard.</p>
<p>Every QR Bill has two parts:</p>
<ul>
<li><p><strong>Receipt</strong> (left, 62mm wide) — for the payer to keep</p>
</li>
<li><p><strong>Payment part</strong> (right) — contains the QR code with all payment data</p>
</li>
</ul>
<p>The QR code contains a structured text string with exactly <strong>31 fields</strong>. Let's break it down.</p>
<h2>The 31-Field QR Data String</h2>
<p>The QR code encodes a newline-separated string. Here's the complete structure:</p>
<pre><code class="language-plaintext">  SPC                          ← Header (always "SPC")
  0200                         ← Version (always "0200")
  1                            ← Encoding (1 = UTF-8)
  CH4431999123000889012        ← IBAN (21 chars, CH or LI only)
  S                            ← Creditor address type (S = Structured)
  ERPLight GmbH                ← Creditor name (max 70 chars)
  Bahnhofstrasse               ← Street
  15                           ← Building number
  8001                         ← Postal code
  Zürich                       ← City
  CH                           ← Country
                               ← 7 empty lines (Ultimate Creditor)
                               ← Reserved by the spec
                               ← NEVER used since 2020
                               ← They exist "for future use"
                               ← Yes, every QR code wastes 7 lines
                               ← on fields that do nothing
                               ← (SIX Group loves planning ahead)
  1500.00                      ← Amount (2 decimal places)
  CHF                          ← Currency (CHF or EUR only)
  S                            ← Debtor address type
  Max Müller                   ← Debtor name
  Dorfstrasse                  ← Street
  7                            ← Building number
  3000                         ← Postal code
  Bern                         ← City
  CH                           ← Country
  QRR                          ← Reference type (QRR, SCOR, or NON)
  210000000003139471430009017  ← Reference (27 digits for QRR)
  Invoice 2026-042             ← Additional info (max 140 chars)
  EPD                          ← Trailer (always "EPD")
</code></pre>
<p>In code, generating this string looks like:</p>
<pre><code class="language-javascript">const qrDataString = [
  'SPC',           // Header
  '0200',          // Version
  '1',             // UTF-8
  iban,            // Creditor IBAN
  'S',             // Structured address
  creditorName,
  street,
  buildingNumber,
  postalCode,
  city,
  'CH',
  '', '', '', '', '', '', '',  // 7 empty Ultimate Creditor fields
  amount.toFixed(2),
  'CHF',
  'S',             // Debtor structured address
  debtorName,
  debtorStreet,
  debtorBuildingNumber,
  debtorPostalCode,
  debtorCity,
  'CH',
  referenceType,   // 'QRR' or 'NON'
  reference,       // 27-digit QR reference or empty
  additionalInfo,
  'EPD'            // End Payment Data
].join('\n');

QR-IBAN vs Regular IBAN

Here's something that tripped me up: Switzerland has two types of IBANs for QR bills.

A QR-IBAN has an Institution Identifier (IID) between 30000 and 31999 (digits 5-9 of the IBAN). It requires a QR Reference (27-digit number with a check digit).

A regular IBAN has an IID outside that range. It uses reference type NON — no structured reference, just free text in the additional info field.

function isQrIban(iban) {
  const cleanIban = iban.replace(/\s/g, '');
  const iid = parseInt(cleanIban.substring(4, 9));
  return iid &gt;= 30000 &amp;&amp; iid &lt;= 31999;
}

Why does this matter? If you put a QR reference on a regular IBAN (or vice versa), the payment will be rejected by the bank. This is the #1 integration bug I see.

The Modulo 10 Recursive Check Digit

The QR Reference uses a check digit algorithm called Modulo 10 recursive — and it's NOT the same as the Modulo 97 used for IBAN validation.

It uses a fixed lookup table defined by SIX Group:

function calculateMod10CheckDigit(reference) {
  const table = [0, 9, 4, 6, 8, 2, 7, 1, 3, 5];
  let carry = 0;

  for (const char of reference) {
    const digit = parseInt(char);
    carry = table[(carry + digit) % 10];
  }

  return (10 - carry) % 10;
}

The reference is built from a numeric user ID + invoice number, padded to 26 digits, then the check digit is appended as the 27th digit.

IBAN Validation: Big Numbers in JavaScript

IBAN validation uses Modulo 97 (ISO 7064). The algorithm:
1. Move the first 4 characters to the end
2. Convert letters to numbers (A=10, B=11... Z=35)
3. Calculate modulo 97 of the resulting number
4. Result must be exactly 1

The problem? After conversion, you get a 24+ digit number. JavaScript's Number type loses precision above 2^53. My solution: process digit by digit.

function validateIBAN(iban) {
  const rearranged = iban.slice(4) + iban.slice(0, 4);
  const numericString = rearranged
    .split('')
    .map(c =&gt; (c &gt;= 'A' ? (c.charCodeAt(0) - 55).toString() : c))
    .join('');

  let remainder = 0;
  for (const digit of numericString) {
    remainder = (remainder * 10 + parseInt(digit)) % 97;
  }

  return remainder === 1;
}

Rendering the Payment Part in PDF

This was the hardest part. The Swiss QR Bill spec defines exact measurements in millimeters:

- A4 page: 210mm × 297mm
- Payment part height: 105mm (starts at Y=192mm)
- Receipt width: 62mm
- QR code size: 46mm × 46mm
- Perforation lines: Dashed, with scissor symbols

I used https://github.com/parallax/jsPDF for PDF generation. The trickiest challenges:

Challenge 1: The Swiss Cross in the QR Code

The QR code must contain a Swiss cross overlay in the center. But adding an image on top of a QR code destroys data — unless you use the right error correction level.

The spec mandates Error Correction Level M (15% recovery). This means up to 15% of the QR code can be obscured and still scan correctly. The Swiss cross fits within this tolerance.

// Generate QR code as SVG
const qrSvg = await QRCode.toString(qrDataString, {
  type: 'svg',
  errorCorrectionLevel: 'M',
  width: 460,
  margin: 0
});

// Inject Swiss cross SVG into the center
const crossSize = 80;  // pixels
const center = 230;    // 460/2
const crossSvg = `
  &lt;rect x="\({center-40}" y="\){center-40}"
        width="\({crossSize}" height="\){crossSize}"
        fill="white"/&gt;
  &lt;rect x="\({center-35}" y="\){center-35}"
        width="70" height="70"
        fill="#e30613"/&gt;
  &lt;rect x="\({center-7}" y="\){center-25}"
        width="14" height="50"
        fill="white"/&gt;
  &lt;rect x="\({center-25}" y="\){center-7}"
        width="50" height="14"
        fill="white"/&gt;
`;

Challenge 2: SVG to PNG for jsPDF

jsPDF can't embed SVG directly. I convert the SVG to a canvas, then to a PNG data URL:

const img = new Image();
img.src = 'data:image/svg+xml;base64,' + btoa(svgString);
await new Promise(resolve =&gt; img.onload = resolve);

const canvas = document.createElement('canvas');
canvas.width = 460;
canvas.height = 460;
canvas.getContext('2d').drawImage(img, 0, 0);

const pngDataUrl = canvas.toDataURL('image/png');
doc.addImage(pngDataUrl, 'PNG', x, y, 46, 46);  // 46mm

Challenge 3: Scissor Symbols on Perforation Lines

The spec requires perforation lines with scissor symbols. Instead of embedding an image, I drew them mathematically with jsPDF:

// Scissor = two ellipses (handles) + two lines (blades)
function drawScissor(doc, x, y, rotation) {
  doc.saveGraphicsState();
  // Draw handle ellipses
  doc.ellipse(x - 2, y, 1.5, 0.8, 'F');
  doc.ellipse(x + 2, y, 1.5, 0.8, 'F');
  // Draw blade lines crossing
  doc.line(x - 2, y, x + 4, y - 3);
  doc.line(x + 2, y, x - 4, y - 3);
  doc.restoreGraphicsState();
}

Challenge 4: Street/Number Splitting

The spec requires separate fields for street name and building number. Swiss addresses don't always follow a clean pattern. My regex-based splitter:

function splitStreetAndNumber(address) {
  const match = address.match(/^(.+?)\s+(\d+\s*[a-zA-Z]?)$/);
  if (match) {
    return { street: match[1], number: match[2] };
  }
  return { street: address, number: '' };
}

The November 2025 Spec Update

SIX Group released Implementation Guidelines v2.3, effective November 21, 2025. Key changes:
- Combined address type "K" was removed — only structured addresses ("S") are allowed now
- Extended character set — Latin Extended-A characters are now permitted
- Line endings — only LF or CR+LF, not CR alone

If you're building a QR bill generator, make sure you're not still using address type "K".

What I'd Do Differently

1. Start with the spec, not examples. I initially tried to reverse-engineer QR codes from existing invoices. Bad idea — go straight to the
https://www.six-group.com/en/products-services/banking-services/payment-standardization/standards/qr-bill.html.
2. Test with real banking apps. Not all banking apps parse QR bills the same way. Test with UBS, PostFinance, Raiffeisen, and ZKB at minimum.
3. Handle edge cases early. Addresses without building numbers, IBANs with spaces, amounts with trailing zeros — these all caused bugs in production.

Try It

If you want to see this in action, https://erplight.ch lets you create Swiss QR bills for free. No signup required for the basic version.

The full QR bill generator runs entirely in the browser — no server-side PDF rendering. Every invoice generates a spec-compliant QR code with the Swiss cross overlay, proper reference numbers, and
pixel-perfect payment part layout
</code></pre>
<p>I'm Raffaele, founder of ERPLight. I build the simplest ERP system for Swiss small businesses. If you habe questions about Swiss QR bills or payment standards, drop a comment below.</p>
]]></content:encoded></item></channel></rss>