thai_promptpay 0.3.0
thai_promptpay: ^0.3.0 copied to clipboard
Pure-Dart PromptPay (Thai EMVCo QR) toolkit: build and parse the QR payload for mobile, National ID / Tax ID, e-Wallet and Bill Payment, with amount and CRC validation.
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)