How to make QR codes for coupons and vouchers
A coupon is the one printed thing that is supposed to stop working. That makes it the case where people reach for a dynamic QR code on purpose, and it is still the wrong reach: the expiry belongs to the voucher, which your till already understands, not to the code, which should keep scanning until the paper is gone.
The five-minute version
- Decide whether the offer is one-per-voucher (single use, needs a unique ID on each) or one-per-campaign (anyone with the flyer, one code for the run).
- Generate the IDs in a spreadsheet: random, short, pasted as values. Load the same list into whatever will validate them.
- Build a
urlcolumn by formula:yourshop.com/v/plus the ID. Keep the ID in its own column too; that is the caption. - Generate static codes from the CSV, each captioned with its ID, and print the offer’s real end date next to them.
- Redeem one test voucher end to end, then try it a second time and confirm the till refuses. Keep the CSV.
The voucher expires. The code does not have to.
Every other guide in this cluster argues for static codes because the printed thing lasts for years and the code has to last with it. A coupon seems to be the exception: it is designed to die on a date. So the tempting design is a dynamic code whose redirect you switch off when the offer ends, which is exactly what the QR vendors suggest, usually on a plan that renews monthly.
Look at where the expiry actually has to live. The till, the checkout, the booking form — whatever accepts the voucher — has to know the end date anyway, because a customer who types the code in by hand bypasses the QR code entirely. So the expiry is already in your system. Putting a second copy of it in a vendor’s redirect adds nothing the voucher did not already have, and it adds a new way for the voucher to fail: the plan lapses, the card expires, the vendor changes its free tier, and every code still in circulation goes dark on the vendor’s schedule rather than yours, including the ones with a month left to run. The mechanics of that failure are in the expiry guide.
The static-vs-dynamic guide concedes that rotating promotions are a real case for dynamic codes, and this page agrees with it for the case it describes: one printed code that has to point at this week’s offer, whatever that is. A coupon is not that case. Its destination never changes; what changes is whether the voucher is still valid, and that decision is made when the code is checked, not when it is scanned. Put the logic behind a stable URL you own, and the code can stay static while the offer behind it does whatever you like.
There is a quieter benefit. A static code printed with an honest end date and a page that says “this offer ended on 30 June” is a coupon that fails gracefully. A dead redirect is a coupon that looks like a scam.
One code for the campaign, or one per voucher
- One per campaign. “10% off with this flyer”, good for anyone, any number of times until the date. The URL is the same on every copy, so one static code serves the whole print run. This is a two-minute job in the free generator, and the rest of this page is mostly not for you.
- One per voucher. Gift vouchers, single-use welcome offers, referral cards, anything with a face value or a “once only” in the terms. Each printed voucher needs its own ID so the till can mark it used, which means each voucher needs its own code. Two hundred vouchers is two hundred codes, and that is a spreadsheet job, not an afternoon of copy-pasting.
The middle case, a multi-use code you want to count scans on, is the one where people buy dynamic for the analytics. A unique parameter per channel does it without the dependency: ?src=flyer on the flyers, ?src=receipt on the receipts, and your own page counts which one brought people in.
What goes in the code
- An ID, never the discount. A code that encodes
20OFFis a code anyone can type. An opaque ID likeK7Q2-M4ZDmeans nothing until the till looks it up, so a voucher cannot be invented, only copied, and a copied single-use voucher is refused the second time. - A URL on your own domain if customers scan it themselves:
yourshop.com/v/K7Q2-M4ZD, a path that shows the offer and applies it at checkout. The packaging guide makes the case for owning the domain in the code; for vouchers it also means the page that says “expired” is yours, not a vendor’s error screen. - Just the ID as plain text if the only scanner is your own till or a staff phone, the same pattern as the asset-tag guide. Shorter payload, sparser code, and nothing a customer’s camera needs to do with it.
- The ID printed underneath in readable type, every time. Cameras fail, receipts smudge, and a voucher that can be keyed in is a voucher that still works. Choose an alphabet without
0/Oand1/Iso it can be read out over the phone.
Generating the IDs
The IDs must be random, not sequential. If voucher 0041 exists, everyone who holds it knows 0042 does too. Eight characters from a 32-letter alphabet is a billion billion possibilities, which is enough that guessing is not a strategy.
- In a spreadsheet, a column of
=UPPER(DEC2HEX(RANDBETWEEN(0;16^8-1);8))gives 8-character hex IDs. They recalculate on every edit, so copy the column and paste it back as values before doing anything else. - Check for duplicates before export:
=COUNTIF(A:A;A2)>1in a helper column should be false on every row. At a few hundred rows a collision is unlikely; at a few thousand it is worth the ten seconds. - Build the
urlcolumn from the ID:="https://yourshop.com/v/"&A2. The ID column is the label the tool prints under each code. - Load the same IDs into the system that redeems them before printing, with the face value and end date stored there, not in the code. If the import fails, nothing has been printed yet.
- Treat the CSV as the voucher book. Do not publish it, do not email it around, and keep it with the export so a lost batch can be voided by ID.
Sizes for flyers, mailers and receipts
- Handheld distance. A flyer or a card is scanned from 15 to 25 cm, so 2 to 2.5 cm across is comfortable and 1.5 cm is the floor. The print-size guide has the full rule.
- Receipts. Thermal printers are low resolution and the paper fades in weeks. Keep the payload short — a plain ID or a short own-domain URL — so the modules stay large, and print the ID in text next to it because the text outlives the code on thermal paper.
- Plain code, plenty of margin. A voucher gets folded, pocketed and handed over damp. Skip the logo and brand colours and spend the error-correction headroom on the crease. The six failure causes are in the not-scanning guide.
- Say what it is. “Scan to claim · valid until 30 June · code K7Q2-M4ZD” next to the code. A bare square on a flyer is a bare square; a square with a deadline and a fallback is a voucher.
Before you print
- Scan three codes from the proof on a cheap phone and confirm each opens the right ID, not a neighbour’s row.
- Redeem one test voucher end to end at the till or checkout. Then present it again and confirm it is refused. Then try an ID that is not in the list and confirm that is refused too.
- Set the end date in the system to yesterday for one test ID and check what the customer sees. It should be a sentence, not an error page.
- Print the real end date on the voucher itself. The code does not expire, the offer does, and the paper should say so.
- Keep the CSV and the export together, dated. A reprint of a lost batch is the same rows regenerated, and because the codes are static, the reprint is pixel-identical — void the old IDs first if the lost batch might still be out there.