thai_promptpay

pub package CI

ชุดเครื่องมือ 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 with thainum if 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.