🎓 CoursesMarkets & internationalIntermediate⏱ 60 min
🔲
Accepting QR code payments. 7 chapters and a final quiz.
Roll out QR code acceptance outside Europe, from choosing a licensed participant to the accounting close. Identify the rail that settles behind the code, test an EMV QRCPS payload before go-live, choose between static and dynamic codes based on the numbers, write the display protocol and the checkout procedure, reconcile incoming credits, accept foreign QR codes, and spot a swapped display.
Identify the rail that settles a QR code, and derive customer recourse, time to credit and cost structure
Test an EMV QRCPS payload before go-live: mandatory fields, CRC, multi-app testing
Choose between static and dynamic QR store by store, based on the numbers
Write the in-store display protocol and the payment confirmation procedure at the register
Chapter 1. Identifying the rail before you sign.
A merchant never signs with a QR standard. It signs with a licensed participant (a bank, a payment institution or an e-money issuer) that enrolls it on that standard. So the first step of the project is to pin down what the participant is really selling you: not the code, but the rail that settles. That rail determines irrevocability, your customer's recourse, when funds are credited, and your largest cost item.
Eight questions to ask the participant, in this order
Which rail settles this QR code? Instant credit transfer, card, e-money or mobile money. Everything else follows from the answer.
Is the payment revocable? On an A2A rail, no. Your customer service team will have to live with that answer.
When are the funds available? With some participants, an instant credit and availability are two different things.
Which fee schedule applies, and who sets it? Regulated, banned, capped or free? The answer changes the whole negotiation.
Is the reconciliation reference carried end to end? From the QR generator to the settlement file and the bank statement.
How are refunds executed? From the original transaction, or by keying in the recipient's alias by hand?
Which limits apply? The regulatory per-transaction limit, the participant's own limits, and the limits set by the payer's app.
Are inbound foreign QR payments accepted? From which markets, through which corridor, and who sets the exchange rate?
Settlement rail
Live standards
Customer recourse
What you need to build
Instant credit transfer
QRIS, Pix QR, UPI QR, Thai QR Payment, VietQR, QR Ph, TANQR
None: the transfer is irrevocable, and there is no enforceable dispute right
A written refund policy, a stated turnaround time and a handling channel are your only recourse mechanism
Card
SGQR, which combines card schemes and local rails; TR Karekod, which covers cards and FAST transfers
The scheme's dispute cycle, with its deadlines and reason codes
Dispute ratio monitoring, evidence management, possibly a reserve
E-money
MMQR, a switch connecting 11 wallets; 聚易用 Simple Pay, which aggregates local payment instruments
The issuer's terms and conditions, not a market-wide rule
Counterparty risk monitoring and tracking of the contractual payout deadline
Mobile money
GIMACPAY in the CEMAC zone; GhQR in Ghana, for its mobile money leg
The operator's rulebook, which varies by market
A cash-out procedure and tracking of ring-fenced balances
What each rail means for your operations
⚠️
A refund is not a reversal
On an A2A rail, no message can unwind the payment: you refund by sending an outgoing credit transfer to the payer. That has three operational consequences. The refund goes through your anti-money laundering checks, like any transfer. It needs an exact payee, so it must be triggered from the original transaction and never keyed in by hand. And it carries its own risk of paying the wrong recipient, with no recourse. Build the real turnaround time into what you promise the customer.
0 %
QRIS merchant fee for a micro-enterprise (UMI) up to Rp 500,000 per transaction; 0.3% above that, 0.7% for other businesses
Bank Indonesia, published fee schedule
Banned
any fee on a UPI or RuPay debit payment, charged to the payer or the payee, since January 1, 2020
Payment and Settlement Systems Act, 2007, Section 10A; Income-tax Act, 1961, Section 269SU
0,80 %
regulatory cap on the merchant fee on the mada rail, with a ceiling of about SAR 40 per transaction
SAMA
🔑
The fee schedule is set by local law, not by the sales proposal
Four regimes coexist. A regulated, published fee schedule rules out any price negotiation, as on QRIS. A banned fee means acceptance can never be a revenue center, as on UPI, and a business model built on it is wrong from line one. A regulatory cap constrains the underlying rail, as with mada. Free pricing is still the most common case, and there the spread between two participants in the same country often exceeds the spread between two countries. Establish the regime before you start talking price.
🎯 Quick question
Your QR acceptance runs on instant credit transfers. A customer disputes a purchase. What can you invoke against the claim?
Chapter 2. Testing an EMV QRCPS payload.
The code in your window is a human-readable character string that you must be able to decode, check and archive. The reference is the EMV® QR Code Specification for Payment Systems, published by EMVCo in two volumes, both version 1.1, dated November 27, 2020. Merchant-presented mode encodes a positional TLV: a two-digit ID, a two-digit length, then the value. There is no secret and no signature, so all the checking falls to you.
Decode a merchant payload and verify its CRC, for your test plan
def tlv(payload: str):
"""Split positional TLV: 2-digit ID, 2-digit length, then the value."""
i, fields = 0, []
while i < len(payload):
tag = payload[i:i + 2]
length = int(payload[i + 2:i + 4])
fields.append((tag, payload[i + 4:i + 4 + length]))
i += 4 + length
return fields
def crc16(data: str) -> str:
"""CRC-16/CCITT-FALSE: init 0xFFFF, polynomial 0x1021, no reflection."""
crc = 0xFFFF
for byte in data.encode("ascii"):
crc ^= byte << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
return format(crc, "04X")
# The CRC covers the WHOLE payload, "6304" included, the 4 value characters excluded.
assert crc16(payload[:-4]) == payload[-4:].upper(), "Invalid CRC: no app will read this code"
fields = dict(tlv(payload))
assert fields["01"] in ("11", "12") # 11 = static, 12 = dynamic
assert len(fields["59"]) <= 25 # merchant name
assert len(fields["60"]) <= 15 # merchant city
Checks to run before printing
Enforcement
What to check
Symptom if you skip it
CRC (tag 63)
Computed over the entire payload, including “6304” but not its value
No app reads the code, and there is no usable error message
Mode (tag 01)
11 for a reusable display, 12 for a single-use code
A checkout code that gets reused, or a sticker that expires
Currency (tag 53)
Numeric ISO 4217 code, never alphabetic
Declined by the payer's app, often with no explanation
Country (tag 58)
ISO 3166-1 alpha-2, consistent with the payee account
Wrong cross-border routing, or rejection by the switch
Name and city (tags 59 and 60)
25 and 15 characters max, no unusual accented characters
Name truncated on the payer's screen, so the payer cancels to be safe
MCC (tag 52)
ISO 18245 code that matches what the store actually does
Three different issuers in the market, not just the participant's own app
➜
Point of sale
Takes a real payment for a minimal amount
Rail routing only shows up on a real transaction; sandbox tests don't exercise it
➜
Cashier
Checks the notification received
Observed delay, content, presence of the issued reference
➜
Accounting
Finds the reference in the settlement file
Then on the account statement. The last link is the only one that counts
➜
Operations
Archives the payload and its hash
The exact string displayed becomes the key evidence in a dispute
⚠️
Three apps, not one
A payload that works in the app of the participant that enrolled you proves almost nothing: each payer app has its own tolerance on lengths, encoding and account templates. Test the same code with three apps from different issuers in the target market, including at least one mass-market bank, because that is where differences show up, and nowhere else. The test takes half a day; an unreadable code in the window costs days of revenue.
One last habit is often missing from projects: version the code generator and, for every display issued, keep the exact string and its hash. That string is how you prove a disputed display. Without it, you cannot show that the code in your window was actually yours.
🎯 Quick question
What exactly does the CRC in tag 63 of a merchant QR code cover?
Chapter 3. Choosing static or dynamic, by the numbers.
The choice isn't made company-wide but store by store, and it is recalculated when the sales profile changes. The same chain can run printed counter stands in its kiosks and register-generated codes in its stores perfectly well. The useful comparison is the volume above which manual reconciliation costs more than integration.
Signal observed
Stay static
Switch to dynamic
Average ticket
Low, uniform, paid in one go
High or varied, with discounts and deposits
Transaction pace
A few payments an hour
Several payments a minute, customers waiting in line
POS system
None, or offline
Connected register that can issue a reference
Sales that carry a reference
Rare
Orders, invoices, bookings, customer accounts
Checkout staff
The merchant in person
Rotating employees with no stake in the checks
Display exposure
Counter watched at all times
Freely accessible display, out of staff's line of sight
Decision grid: what to observe at the store
The deciding calculation
Monthly cost of manual reconciliation, to compare with the integration cost
Monthly cost of manual reconciliation
= QR payments per day
x share not matched automatically
x minutes of handling per discrepancy
x loaded hourly cost / 60
x business days per month
Workshop — assumptions to REPLACE with your own measurements:
420 payments/day x 22% unmatched = 92 discrepancies/day
92 discrepancies x 3 min = 276 min/day, i.e., 4 h 36 min
4.6 h x 26 business days = 120 h/month, for ONE store
Compare with:
register integration cost (one-time, amortized over the contract term)
+ any per-transaction fee for dynamic QR
- discrepancies avoided (one-to-one matching from day one)
- losses avoided on underpayments and swapped displays
🔑
Dynamic doesn't eliminate outages, it moves them
A static code requires the customer to be online; a dynamic code requires the register to be. Dynamic therefore moves the network dependency from the sidewalk to your POS system, with no fallback comparable to the card schemes' stand-in. Write the fallback procedure before go-live, not on the day of the outage. Without a written procedure, staff improvise, and improvisation at the register produces exactly the payments you will never trace.
A sealed backup counter stand for each store, numbered, kept out of sight and brought out only on the manager's call
An amount limit in fallback mode, above which the sale waits or moves to another payment method
A time-stamped paper log: time, amount, name shown in the customer's app, employee's initials
A cross-check at the end of the shift between the log and the notifications received
A formal return to service: stand put away, seal recorded, normal mode resumed
Roll out the switch gradually, starting with high-ticket stores, where an underpayment costs the most. Keep static where it genuinely wins: low volume, uniform ticket size, owner on site. Doing it the other way around wears teams out without improving reconciliation at all.
🎯 Quick question
A store takes 40 payments a day with a uniform ticket size and is run by its owner. What do you recommend?
Chapter 4. Displaying the code in store.
The physical display, which exposes your payee address openly and within reach, is part of the payment system. Two properties are at stake at once: legibility, which drives conversion, and integrity, which decides who gets paid. A poorly designed display fails on both.
The display itself
A quiet zone of four modules around the symbol, required by ISO/IEC 18004. A decorative frame that eats into it makes the code unreadable
Error correction level: the same standard defines L, M, Q and H, which recover roughly 7%, 15%, 25% and 30% of damaged data
Center logo: it uses up the error correction budget; if marketing insists on one, go up one level and retest
Dark on light, with no inversion and no glossy laminate. A reflection off the window defeats a perfectly printed code
Sealed or behind glass, with a numbered seal that both customers and staff can see
Inventory number printed outside the symbol, to link this specific display to your store registry
🧾
At the counter
A sealed display facing the customer, lit without glare. Staff can watch it continuously, which makes it the only place where a static code belongs.
🍽️
In the dining room or a guest room
Display out of staff's sight, exposed for hours. Use a dynamic code printed on the check, or a sealed display inspected every shift.
📄
On an invoice or quote
The code travels and outlives the document. Issue one code per document, carrying the invoice number, and set a validity period.
🖥️
On the register screen
No physical display to swap, and the reference is injected by the POS system. Check the brightness: a screen in sleep mode makes the scan fail.
⚠️
The name shown to the payer is a security control
Tag 59 holds at most 25 characters. The name it carries, shown by the payer's app, is the only thing the payer can check before approving. So choose a label that is short and recognizable to the customer, not the group's full legal name. Train staff to say it out loud before the scan: a customer who has been told the expected name becomes your first swap detector, at zero cost.
Check usability from where the customer stands, never from behind the register. Height, angle, lighting and scanning distance can all be checked with an ordinary phone in 30 seconds. A perfectly printed symbol becomes unreadable as soon as a window reflects the late-afternoon sun onto it. Repeat this check every day at opening, on every display.
Scan your own code from the customer's position, with a company phone
Compare the payee name and ID shown with the store record
Check the seal and its number, then the display's physical condition
Log the time, the display number and the employee's initials. That record is what will date an incident
🎯 Quick question
Why must the label in tag 59 be short and instantly recognizable to the customer?
Chapter 5. Reconciling QR payments.
On a free or near-free A2A rail, the cost of QR acceptance lies not in the fee but in reconciliation. A credit received carries the payer's name, an amount and a time stamp, but rarely your order number. The whole job is to keep a matching key intact across four hops: the displayed code, the notification, the settlement file and the account statement.
Three-point reconciliation, from sale to journal entry
POS system
Issues the reference
A unique reference injected into tag 62-05, stored with the expected amount and the payload hash
➜
Participant
Sends the payment notification
The first checkpoint, the one that counts as confirmation, not the payer's screen
➜
Participant
Delivers the settlement file
Second checkpoint. Make sure the reference is there, not just the internal credit ID
➜
Bank
Credits the account
Third checkpoint. The statement is authoritative; a reconciliation that stops at the file does not prove the funds reached the account
➜
Accounting
Matches and closes out
One-to-one matching, entries posted, leftovers moved to a dated suspense account
When the reference is not carried through
Some national implementations make the field optional and some aggregators don't pass it on, so matching falls back on a triplet: amount, minute, and the tail of the payer's alias. That fallback fails exactly where volume is high, since two identical sales paid in the same minute can't be told apart. So measure your automatic match rate before you roll out widely, over a full week that includes a peak day.
Difference
Signature in the data
Processing
Credit with no order
Credit on the statement, no known reference to match
D+1 queue, search by amount and minute, then a dated suspense account
Order with no credit
Reference issued, no credit after the rail's observed delay
Ask the participant for the status before any goodwill gesture, and never hand over goods on the strength of a screen
Partial amount
Static code, amount entered by the payer, shortfall
Written policy: deliver, request the balance or refund, decided before go-live, not at the register
Duplicate credit
Same reference, two separate credits
Refund from the original transaction, with dual approval above a threshold
Credit outside the accounting day
Credit received after the cutoff time, on a rail that never closes
Apply one declared cutoff, identical at the register, in the file and in the general ledger
Credit in a different currency
Cross-border payment, converted amount
Isolate the rate applied line by line and reconcile it with the amount shown to the payer
The six recurring discrepancies, and how to handle them
Minimum reconciliation table, the foundation of the daily close
CREATE TABLE qr_payment (
reference TEXT PRIMARY KEY, -- tag 62-05, issued by the register
store TEXT NOT NULL,
display TEXT, -- seal number of the counter stand; NULL if dynamic
payload_sha256 TEXT NOT NULL, -- hash of the code actually displayed
expected_amount BIGINT, -- minor units; NULL if static QR
currency CHAR(3) NOT NULL, -- ISO 4217 alphabetic
issued_at TIMESTAMPTZ NOT NULL,
credit_id TEXT, -- credit ID at the participant
credited_amount BIGINT,
credited_at TIMESTAMPTZ,
accounting_day DATE, -- derived from credited_at and the declared cutoff
applied_rate NUMERIC(18, 8), -- filled in only for cross-border payments
status TEXT NOT NULL DEFAULT 'expected'
);
CREATE INDEX ON qr_payment (status, issued_at);
CREATE INDEX ON qr_payment (store, accounting_day);
🔑
The accounting cutoff is a decision, not a technical setting
An instant rail credits funds at any hour, weekends included, while your accounting works in days. So declare a cutoff time, with its time zone, and apply it identically in three places: the POS system, the settlement file import and the general ledger. Three different definitions of the day produce permanent discrepancies that can never be resolved: every close creates new ones, and none clears them.
ℹ️
Suspense accounts get aged, not wiped clean
Unmatched credits land in a suspense account. Each line must carry an age and an escalation rule: chase the participant, search manually, make a decision after a set number of days. Clearing the suspense account with a single month-end entry destroys the only record that could still reveal a hijacked display.
🎯 Quick question
How far down the chain must you test that the reconciliation reference is present before opening a store?
Chapter 6. Accepting foreign QR codes.
Cross-border acceptance changes neither your code, nor your contract, nor your equipment. A traveler scans it with their home-country app. Their country's switch recognizes a foreign payee and routes the transaction to the corridor. You receive a domestic credit in your own currency, so everything comes down to one item at your participant: FX.
From which markets are inbound QR payments accepted today, and since when for each one?
Through which corridor does the transaction run, and which switch acts as the gateway?
Who sets the exchange rate, and is that rate shown to the payer before approval?
What margin is taken, and how does it appear in the settlement file?
Is the credit domestic, in the same currency and on the same timeline as a local sale?
How do you refund a sale paid through a corridor, and how long does it take?
Bilateral link between central banks
Private wallet gateway
Who enrolls you
Your domestic participant, with no extra steps
An aggregator or your acquirer, under a separate contract
What the traveler uses
Their usual banking app or home-country wallet
A wallet that partners with the gateway
Coverage
29 intra-ASEAN and external QR and P2P links counted in December 2025
Alipay+: more than 220 markets, more than 150 million merchants
What you negotiate
Not much: the link is a market-wide agreement, but you still have to demand the FX margin
Price, wallet coverage, data reporting
Dependency created
None beyond your participant
A single integration, in exchange for dependence on a private intermediary
Two ways to open up, two different negotiations
29
QR and P2P links counted, within ASEAN or with external partners
regional sources, December 2025
134 701
transactions worth Rs 321 million in five months on the UPI corridor opened to Nepal in February 2024
Fonepay / NPCI International
> 220
markets covered by Alipay+, with more than 150 million merchants and about 50 partner wallets
alipayplus.com, accessed in 2026
⚠️
FX is the item press releases never mention
Links are always announced by showing the gesture: the traveler scans, the money arrives. But the rate applied, and how transparent it is, vary from one corridor to the next, with no common regional framework so far. Require your participant to show the FX margin line by line, not a monthly average rate. Without that detail, you can neither audit the billing nor answer a customer who compares the amount debited with the amount displayed.
ℹ️
Coverage is checked corridor by corridor, as of the project date
The cross-border network is being built one agreement at a time, and it is still incomplete. Some Southeast Asian markets still have no regional QR link; others have links with only one or two neighbors. Never promise acceptance for a country based on a published map. Ask your participant for the list of active corridors, with their go-live dates, and test a real payment before any marketing announcement.
🎯 Quick question
How does a foreign traveler paying through a cross-border corridor affect the merchant?
Chapter 7. Stopping QR code swap fraud.
When a third party sticks its own code over yours, your customers pay as usual, their apps confirm the payment and your staff see nothing wrong. The money goes elsewhere. What sets this fraud apart is its signature. It produces no error at all, only missing credits, so an alerting system built on failures will never catch it.
Fraud
What the merchant sees
Control that stops it
Display swap
Nothing: customers pay, but no credit arrives
Numbered seal, daily scan at opening, alert on missing credits
Fake confirmation on the payer's screen
A screenshot or an app that mimics the confirmation
Confirm the sale only on the merchant's own notification or a voice alert
Underpayment on a static code
A credit below the amount due, often off by one digit
Reading the amount aloud, checking the notification, switching to dynamic
Display redirected to a data-harvesting page
Customers reporting an unusual request for their details
Check scan by staff, immediate removal of the display, report to the participant
Three QR channel frauds, and the control that stops each one
⚠️
The only known public guidance puts the burden on the customer
On January 18, 2022, the FBI issued alert I-011822-PSA, Cybercriminals Tampering with QR Codes to Steal Victim Funds. Its advice on physical codes fits in one sentence: check that no sticker has been placed over the original code. That advice targets the payer; it is not a merchant control. Since no customer knows what your display normally looks like, you have to run the check yourself, every day, on every display.
The response, step by step
H+0
Freeze the display
Take the code out of public view and suspend QR payments at that store. Don't peel anything off: the swapped display is evidence.
H+1
Confirm and document
Photograph the display in place, scan the fraudulent code with a company phone, and record the payee ID shown and the time.
H+4
Notify the participant
Send the payee ID you recorded, the estimated exposure window and the list of sales with no matching credit.
D+1
Resume taking payments
New display, new seal number, payload regenerated and tested, opening log restarted.
D+7
Widen the check
Check every display across the network, not just the one that was hit. A successful swap gets repeated at similar stores.
A numbered seal on every display, recorded in the store registry
A reference photo of the display in place, dated and kept with the payload hash
A daily check scan at opening, comparing the payee name and ID
An inventory of issued codes: store, date, distribution channel, planned validity period
A revocation procedure for when a store closes or you change participants
Codes printed on documents treated as displays in their own right: they last for years
🔑
Alert on absence, not on errors
A swapped display triggers no decline, no response code and no message, so set up an alert on silence: zero QR payments at a store that has been open for two hours when its history says there should be some. Calibrate this rule by store and by time slot, using your own data. No other detection method works without relying on customers' vigilance.
🎯 Quick question
A store has been open for two hours without a single QR credit, when it usually records about 20. Which hypothesis do you test first?