# PromptPay QR Code Generator UI in SwiftUI

> By Lawrence Arya, Founder & CEO of VP0. Published 2026-06-07. 6 min read.
> Source: https://vp0.com/blogs/promptpay-qr-code-generator-ui-swiftui

Generate the format wrong and no bank app will scan it. The engineering is the payload; the UI is making it scannable.

**TL;DR.** A PromptPay QR generator produces Thailand's national instant-payment receive code (PromptPay launched 2016), and the build is fundamentally about generating a correct, standards-compliant QR: an EMVCo TLV (tag-length-value) payload with the PromptPay ID, THB currency, optional amount, and a CRC checksum, not free text, since a wrong format will not scan in any banking app. The key product decision is static (reusable, no amount) versus dynamic (per-transaction amount baked in), the QR must render large and high-contrast, and the payee and amount must show as readable text beside it as a fraud defense. The app generates the receive code; the payer's bank moves the money. A free VP0 design supplies the receive and amount screens.

## What is PromptPay, and what does "generator" mean here?

Thailand's national instant-payment system, where you receive money by showing a QR code tied to your phone number, citizen ID, or e-wallet instead of sharing bank details. [PromptPay](https://en.wikipedia.org/wiki/PromptPay) launched in 2016 and became how Thailand pays: a merchant or person displays a QR, the payer scans it in their banking app, and money moves instantly. A "PromptPay QR generator" produces that receive-code, optionally with an amount baked in, so the build is fundamentally about **generating a correct, standards-compliant QR**, not a generic QR with text in it.

The honest framing first: the QR is not free text, it is a structured [EMVCo merchant-presented QR](https://en.wikipedia.org/wiki/EMV) payload following the Thai standard, with specific data fields and a checksum. Generate it wrong and no banking app will scan it, so the engineering is encoding the payload to spec, and the UI is making that correct code easy to show and share. A clone that draws a pretty QR encoding the wrong format is a decoration, not a payment code.

## How is the payload actually built?

As an EMVCo TLV (tag-length-value) string with a CRC checksum, which is the part that must be exactly right:

| Field | What it carries | Why it matters |
| --- | --- | --- |
| Payload format / type | Static (reusable) vs dynamic (one amount) | A fixed amount changes the whole code |
| Merchant account info | The PromptPay ID (phone, citizen ID, e-wallet) | Who gets paid |
| Currency / country | THB, Thailand | Required by the standard |
| Transaction amount | Optional; baked in for dynamic QR | Payer cannot mistype the amount |
| CRC checksum | Validates the whole payload | A wrong checksum makes the QR unscannable |

The static-versus-dynamic distinction is the key product decision: a **static** QR (no amount) is the reusable code a shop tapes to the counter, the payer types the amount; a **dynamic** QR encodes a specific amount so the payer just confirms, which is what an invoice or a point-of-sale screen generates per transaction. Getting the TLV structure and the CRC right is non-negotiable, and rendering the QR from that correct string uses Core Image's [QR generator filter](https://developer.apple.com/documentation/coreimage/cifilter-swift.class) at high enough resolution to scan reliably (its error-correction level M can recover roughly 15% of a damaged code, but a clean high-contrast render still matters most).

## What does the receive screen owe the user?

Scannability and honest context, because this screen is shown to someone who is about to send money. The QR must render large and high-contrast (a small or low-res QR fails to scan in real lighting, the most common complaint), with the recipient name and, for a dynamic code, the amount shown in plain text beside it so the payer confirms they are paying the right person the right amount before scanning. Showing the amount and payee as readable text next to the QR is itself a fraud-defense: a QR alone is unverifiable to a human, so the human-readable context is how a payer catches a swapped code.

Two more honesties. Save and share: the user wants to send the QR (to a customer over chat) or save it, so a clean export of the code-plus-context is core, the same render-to-share discipline as any [QR or share artifact](/blogs/instagram-story-share-export-template-react-native/). And the receive side is the app's job, the pay side is the payer's banking app, so a generator app is honest that it produces a code to be paid, it does not move the money itself, the same render-the-UI, settle-through-the-licensed-rail framing as every fintech build like [the PromptPay-adjacent regional wallets](/blogs/maya-digital-bank-ui-clone-react-native/).

## What completes a PromptPay generator?

The surrounding flow. Multiple PromptPay IDs (a user may register a phone and a citizen ID), a quick amount entry for dynamic codes, a saved-payees or recent-amounts convenience, and clear handling of the static-vs-dynamic toggle so the user understands which kind of code they are showing. And validation: the app should confirm the PromptPay ID is well-formed before generating, because a code for a mistyped ID looks fine and silently sends money nowhere recoverable.

The screens, the receive screen with the big QR, the amount entry, the ID management, the share, come as a free [VP0](https://vp0.com) design, so an agent builds the EMVCo payload encoder and Core Image rendering onto a UI already shaped for scannable display and human-readable context rather than a generic QR maker.

## Key takeaways: a PromptPay QR generator

- **The payload is a standard, not free text**: an EMVCo TLV string with a CRC checksum; wrong format means no app will scan it.
- **Static vs dynamic is the key decision**: a reusable no-amount code versus a per-transaction code with the amount baked in.
- **Render large and high-contrast**: a small or low-res QR failing to scan is the most common real-world complaint.
- **Show payee and amount as text beside the QR**: human-readable context is a fraud defense, since a QR alone is unverifiable.
- **Generate the receive code; the payer's bank moves the money**: validate the PromptPay ID, and never imply the app settles funds.

## Frequently asked questions

**How do I build a PromptPay QR code generator in SwiftUI?** Encode an EMVCo TLV payload (payload type, the PromptPay ID, THB currency, optional amount, and a CRC checksum) to the Thai standard, then render it with Core Image's QR filter at a scannable resolution, showing the payee and amount as text beside the code. A free VP0 design supplies the receive, amount-entry, and ID-management screens.

**Why can't I use a generic QR generator for PromptPay?** Because a PromptPay QR is a structured EMVCo merchant-presented payload with specific data fields and a checksum, not arbitrary text. A generic QR encoding the wrong format will not scan in any banking app, so the engineering is producing a standards-compliant payload, and the pretty rendering is the easy part.

**What is the difference between a static and dynamic PromptPay QR?** A static QR carries no amount and is reusable, the code a shop tapes to the counter where the payer types the amount; a dynamic QR bakes in a specific amount so the payer just confirms, which an invoice or point-of-sale screen generates per transaction. The choice changes the payload, so the app should expose it clearly.

**Why must the QR render large and high-contrast?** Because a small or low-resolution QR is the most common real-world failure: it will not scan reliably in ordinary lighting on the payer's phone. Render the code large and high-contrast, and generate it at sufficient resolution, since an unscannable receive code defeats the entire purpose of the screen.

**Does a PromptPay generator app move the money?** No: it produces the receive code, and the payer's own banking app scans it and moves the money through the licensed PromptPay rails. An honest generator validates the PromptPay ID and shows readable payee-and-amount context, but it never settles funds itself or implies it does.

## Frequently asked questions

### How do I build a PromptPay QR code generator in SwiftUI?

Encode an EMVCo TLV payload (payload type, the PromptPay ID, THB currency, optional amount, and a CRC checksum) to the Thai standard, then render it with Core Image's QR filter at a scannable resolution, showing the payee and amount as text beside the code. A free VP0 design supplies the receive, amount-entry, and ID-management screens.

### Why can't I use a generic QR generator for PromptPay?

Because a PromptPay QR is a structured EMVCo merchant-presented payload with specific data fields and a checksum, not arbitrary text. A generic QR encoding the wrong format will not scan in any banking app, so the engineering is producing a standards-compliant payload, and the pretty rendering is the easy part.

### What is the difference between a static and dynamic PromptPay QR?

A static QR carries no amount and is reusable, the code a shop tapes to the counter where the payer types the amount; a dynamic QR bakes in a specific amount so the payer just confirms, which an invoice or point-of-sale screen generates per transaction. The choice changes the payload, so the app should expose it clearly.

### Why must a PromptPay QR render large and high-contrast?

Because a small or low-resolution QR is the most common real-world failure: it will not scan reliably in ordinary lighting on the payer's phone. Render the code large and high-contrast, and generate it at sufficient resolution, since an unscannable receive code defeats the entire purpose of the screen.

### Does a PromptPay generator app move the money?

No: it produces the receive code, and the payer's own banking app scans it and moves the money through the licensed PromptPay rails. An honest generator validates the PromptPay ID and shows readable payee-and-amount context, but it never settles funds itself or implies it does.

---
*Published on the [VP0 Journal](https://vp0.com/blogs). Free to read, index and cite with attribution.*
