QR พร้อมเพย์ระบุยอดทำงานยังไง: แกะโครงสร้าง EMVCo ทีละช่อง พร้อมโค้ด Python
QR พร้อมเพย์ที่สแกนจ่ายกันทุกวันไม่ได้มีอะไรลึกลับ ข้างในเป็นแค่ข้อความตัวอักษรหนึ่งบรรทัด ที่จัดรูปแบบตามมาตรฐาน EMVCo QR Code (มาตรฐานเดียวกับที่หลายประเทศใช้) แล้วแปลงเป็นรูป QR เมื่อเข้าใจโครงสร้าง เราสร้าง QR ระบุยอดเงินเองได้ทั้งหมดในโค้ดไม่กี่สิบบรรทัด ไม่ต้องส่งเบอร์และยอดเงิน ไปให้บริการภายนอก
ผมใช้วิธีนี้ในระบบร้านค้าออนไลน์ที่ออก QR ตามยอดของแต่ละออเดอร์ บทความนี้จะแกะทีละช่อง
ตัวอย่าง payload
นี่คือ payload ของ QR สำหรับโอนเข้าเบอร์ 081-234-5678 ยอด 100 บาท (เบอร์สมมติ)
00020101021229370016A0000006770101110113006681234567853037645406100.005802TH6304F142
ดูเหมือนตัวอักษรมั่ว แต่จริงๆ คือข้อมูลหลายช่องต่อกัน
รูปแบบ TLV: Tag – Length – Value
ทุกช่องเขียนแบบเดียวกันหมด
- Tag 2 หลัก — บอกว่าช่องนี้คือข้อมูลอะไร
- Length 2 หลัก — ค่าข้างหลังยาวกี่ตัวอักษร
- Value — ตัวค่าจริง ยาวตาม length
เช่น 5802TH แปลว่า tag 58 (ประเทศ) ยาว 02 ตัว ค่าคือ TH
แกะทีละช่อง
| ส่วน | Tag | ความหมาย |
|---|---|---|
000201 | 00 | Payload Format Indicator ค่าคงที่ 01 |
010212 | 01 | ประเภท QR: 11 = ใช้ซ้ำได้ (ไม่ระบุยอด) · 12 = ใช้ครั้งเดียว (ระบุยอด) |
2937… | 29 | ข้อมูลผู้รับเงินพร้อมเพย์ ยาว 37 ตัว ข้างในเป็น TLV ซ้อนอีกชั้น (ดูด้านล่าง) |
5303764 | 53 | สกุลเงิน รหัส ISO 4217 ของเงินบาทคือ 764 |
5406100.00 | 54 | ยอดเงิน 100.00 (ใส่เฉพาะ QR ระบุยอด) |
5802TH | 58 | ประเทศ |
6304F142 | 63 | CRC ตัวตรวจสอบความถูกต้องของข้อความทั้งหมด |
ข้างใน tag 29
0016A000000677010111 → tag 00 ยาว 16: Application ID ของพร้อมเพย์
01130066812345678 → tag 01 ยาว 13: เบอร์มือถือ
เบอร์มือถือต้องแปลงก่อน: ตัด 0 ตัวหน้าออก แล้วเติมรหัสประเทศ 0066
จาก 0812345678 จึงกลายเป็น 0066812345678 (13 หลัก)
ถ้าผู้รับใช้เลขบัตรประชาชน ให้ใช้ tag 02 ใส่เลข 13 หลักตรงๆ
และเลข e-Wallet 15 หลักใช้ tag 03
CRC: ช่องที่ทำให้ QR สแกนไม่ผ่านบ่อยที่สุด
4 ตัวสุดท้ายคือค่า CRC16-CCITT (polynomial 0x1021, ค่าเริ่มต้น 0xFFFF)
คำนวณจากทุกตัวอักษรข้างหน้า รวมถึง 6304 ของตัวมันเองด้วย
จุดที่คนพลาดบ่อยคือคำนวณ CRC ก่อนต่อ 6304 แอปธนาคารจะขึ้นว่า QR ไม่ถูกต้อง
โค้ด Python ฉบับเต็ม
import re
AID_PROMPTPAY = "A000000677010111"
def tlv(tag: str, value: str) -> str:
return f"{tag}{len(value):02d}{value}"
def crc16(data: str) -> str:
crc = 0xFFFF
for ch in data.encode("ascii"):
crc ^= ch << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
return f"{crc:04X}"
def promptpay_payload(target: str, amount: float | None = None) -> str:
digits = re.sub(r"\D", "", target)
if len(digits) == 10 and digits.startswith("0"):
tag, value = "01", "0066" + digits[1:] # เบอร์มือถือ
elif len(digits) == 13:
tag, value = "02", digits # เลขบัตรประชาชน
else:
raise ValueError("ต้องเป็นเบอร์มือถือ 10 หลัก หรือเลขบัตร 13 หลัก")
parts = [
tlv("00", "01"),
tlv("01", "12" if amount else "11"),
tlv("29", tlv("00", AID_PROMPTPAY) + tlv(tag, value)),
tlv("53", "764"),
]
if amount:
parts.append(tlv("54", f"{amount:.2f}"))
parts.append(tlv("58", "TH"))
body = "".join(parts) + "6304" # ต่อ 6304 ก่อนคำนวณ
return body + crc16(body)
print(promptpay_payload("081-234-5678", 100))
เอาสตริงที่ได้ไปทำรูป QR ด้วยไลบรารี qrcode (pip install qrcode[pil])
import qrcode
qrcode.make(promptpay_payload("0812345678", 100)).save("pay.png")
ทดสอบให้มั่นใจก่อนใช้จริง
- สแกนด้วยแอปธนาคารอย่างน้อย 2 ธนาคาร ต้องขึ้นชื่อผู้รับถูกและยอดถูก (สแกนดูได้โดยไม่ต้องกดยืนยันโอน)
- ลองยอดที่มีสตางค์ เช่น 99.50 และยอดหลักพันขึ้นไป
- ใช้ QR ประเภท
12(ใช้ครั้งเดียว) กับออเดอร์ที่ระบุยอด กันลูกค้าสแกน QR เดิมจ่ายซ้ำ
QR ไม่ได้บอกว่าเงินเข้าแล้ว
สิ่งที่ต้องเข้าใจคือ QR พร้อมเพย์แบบนี้ทำแค่ขั้น "สร้างคำขอให้โอน" มันไม่ได้แจ้งกลับมาว่าลูกค้าโอนแล้ว ระบบที่หน้าจอเปลี่ยนสถานะเองเมื่อเงินเข้า ต้องมีแหล่งข้อมูลยืนยันการรับเงินอีกชั้น เช่น บริการรับชำระเงินของธนาคารหรือ payment gateway ที่ส่ง webhook มาเมื่อมีเงินเข้า หรือการตรวจสลิปผ่านบริการที่ได้รับอนุญาต อย่าเชื่อแค่ว่าลูกค้ากดปุ่ม "โอนแล้ว"