thai_promptpay
ชุดเครื่องมือ PromptPay (Thai EMVCo QR) แบบ pure Dart — สร้างและถอดรหัส payload ของ QR สำหรับเบอร์มือถือ / เลขบัตรประชาชน-ภาษี / e-Wallet พร้อมจำนวนเงินและตรวจ CRC
A pure-Dart PromptPay (Thai EMVCo QR) toolkit — build and parse the QR payload string for a mobile number, National ID / Tax ID or e-Wallet, with amount and CRC validation.
- Pure Dart, no Flutter, one dependency (
thainum, for phone normalization + National-ID MOD-11 validation). Works in back-end / CLI (generate a QR for an invoice server-side) and Flutter. - Encode and decode. Most PromptPay libraries only generate — this one also parses a payload back into structured data and verifies the CRC.
- It returns the payload string, not an image. Render it with any QR package
you like (
qr_flutter,qr,barcode, …). - Exact money. Amounts are integer satang (1 baht = 100 satang) — no
double, no rounding surprises.
The output is verified byte-for-byte against the canonical PromptPay references
(dtinth/promptpay-qr and the
promptpay Python library).
dart pub add thai_promptpay
Encode
import 'package:thai_promptpay/thai_promptpay.dart';
// To a mobile number (accepts 0812345678, 081-234-5678, +66812345678, …):
promptPayMobile('0812345678');
// 00020101021129370016A0000006770101110113006681234567853037645802TH6304823E
// With an amount (100.00 baht = 10000 satang) → a one-time "dynamic" QR:
promptPayMobile('0812345678', amountSatang: 10000);
// To a 13-digit National ID / personal Tax ID (validated with a MOD-11 checksum):
promptPayNationalId('1101700230708', amountSatang: 25075); // 250.75 baht
// To a 15-digit e-Wallet ID:
promptPayEWallet('004999000000001');
Then render the returned string as a QR with any package, e.g. qr_flutter:
QrImageView(data: promptPayMobile('0812345678', amountSatang: 10000));
Decode
final p = decodePromptPay(
'00020101021229370016A0000006770101110113006681234567853037645406100.005802TH6304F142',
);
p.target.type; // PromptPayType.mobile
p.target.value; // '0812345678'
p.amountSatang; // 10000 (100.00 baht)
p.isDynamic; // true (one-time QR)
// Non-throwing variant — returns null on a bad payload / CRC mismatch:
tryDecodePromptPay('not a promptpay qr'); // null
decodePromptPay verifies the CRC and throws a PromptPayException (which
implements FormatException) on any malformed payload, CRC mismatch, or unknown
proxy. tryDecodePromptPay returns null instead.
Bill Payment (EMVCo tag 30)
Besides the personal transfer above (tag 29), this package also builds and parses Bill Payment QR codes — the ones printed on Thai invoices/utilities/tax forms. They pay a registered Biller ID with Ref1 / Ref2 reference numbers:
// Encode a bill-payment QR (biller + Ref1, optional Ref2 + amount):
encodeBillPayment(
billerId: '010553609264101', // 13- or 15-digit Biller ID (Tax ID [+ suffix])
ref1: '000002201649894',
ref2: 'INV0001', // optional
amountSatang: 25075, // optional → 250.75 baht, one-time "dynamic" QR
);
// Decode it back:
final b = decodeBillPayment(payload);
b.billerId; b.ref1; b.ref2; b.amountSatang; b.isDynamic;
tryDecodeBillPayment('bad'); // null instead of throwing
Don't know which kind a payload is? decodeAny returns a sealed union so the
switch is exhaustive:
switch (decodeAny(payload)) {
case PromptPayPayload p: print('personal → ${p.target}');
case BillPaymentPayload b: print('bill → ${b.billerId} / ${b.ref1}');
}
decodePromptPay stays personal-only (it throws on a tag-30 payload). The
bill-payment output is verified byte-for-byte against the community references
maythiwat/promptparse and
mrwan2546/promptparse-go.
Slip Verify (Mini-QR)
The small QR printed on a Thai transfer slip is not a payable QR — it is a
receipt-verification code (Slip Verify Mini-QR). This package decodes it
offline (no bank API call) into its own sealed SlipData type:
import 'package:thai_promptpay/thai_promptpay.dart';
// A bank slip:
final slip = decodeSlip(
'004100060000010103014022000111222233344ABCD125102TH910417DF',
);
switch (slip) {
case BankSlip s:
s.sendingBankCode; // '014'
s.bank?.nameEn; // 'Siam Commercial Bank'
s.bank?.nameTh; // 'ธนาคารไทยพาณิชย์'
s.transRef; // '00111222233344ABCD12'
s.countryCode; // 'TH'
case TrueMoneySlip s:
s.eventType; // e.g. 'P2P'
s.transactionId;
s.date; // 'DDMMYYYY'
}
// Non-throwing variant — returns null on a bad payload / CRC mismatch:
tryDecodeSlip('not a slip qr'); // null
decodeSlip verifies the CRC and throws a PromptPayException on a malformed
payload or CRC mismatch; an unknown bank code never throws (bank is simply
null). Look up a bank code directly with
thaiBankByCode('014') → ThaiBank?.
The slip TLV structure and test vectors are ported from maythiwat/promptparse (MIT) and cross-checked with SCB's mini-QR documentation — see NOTICE.md.
Notes
- Money is handled in integer satang end to end (
amountSatang). Pair it withthainumif you want the baht text. - Validation: mobile numbers are normalized/validated, National IDs are checked with the 13-digit MOD-11 checksum, e-Wallet IDs are length-checked.
- Scope: personal PromptPay (EMVCo tag 29) — mobile / National ID / e-Wallet — and Bill Payment (tag 30). The tag-62 additional-data block (Ref3) is tolerated on decode but not generated.
crc16ccitt(String)(CRC-16/CCITT-FALSE) is exported for convenience.
License
MIT © 2026 MaIII (ultramcu)
Libraries
- thai_promptpay
- Pure-Dart PromptPay (Thai EMVCo QR) toolkit: build and parse the QR payload string for mobile, National ID / Tax ID and e-Wallet, with amount and CRC validation. Render the resulting string with any QR package.