บัญชีปลายทาง
implementation 0092 — รอตรวจและ deploy
Live P2P รุ่นที่พัฒนาแล้ว ใช้เฉพาะบัญชีปลายทางที่มีอยู่แล้ว Public API ลงทะเบียน/ค้นหา/ยืนยัน beneficiary และ Customer API ยังไม่อยู่ในขอบเขตนี้ ผู้ให้บริการต้องยืนยันความพร้อมของ deployment ก่อนใช้งานเงินจริง
บัญชีที่ใช้ได้ในรุ่นนี้
ให้ผู้ดูแลจัดเตรียมบัญชีที่เป็นของ Merchant และ Live mode เดียวกับ key บัญชีต้อง verified, รับรายการใหม่ได้ และผ่านเงื่อนไขตัวตน/ธนาคารปัจจุบัน บัญชีของ Merchant อื่น หรือบัญชี Test ใช้แทนกันไม่ได้
ส่ง beneficiary_id รูปแบบ ben_ + UUID hex พิมพ์เล็ก 32 ตัว:
{ "beneficiary_id": "ben_92000000000040008000000000000423" }ตัวอย่างข้างต้นแสดงรูปแบบ ID เท่านั้น ไม่ใช่บัญชีที่นำไปใช้ได้ทันที ต้องใช้ ID ของบัญชีที่ผู้ดูแลจัดเตรียมจริง ห้ามเดา ID หรือส่ง UUID ที่ยังมีขีด ระบบไม่รับเลขบัญชีหรือ inline beneficiary ในคำขอถอนรุ่นนี้ end_user_ref ยังคงเป็น reference ฝั่งคุณ ไม่ได้สร้างหรือยืนยันบัญชีปลายทางให้
ถ้าบัญชีไม่พร้อม POST /v1/pay/withdrawals ได้ 409 beneficiary_not_verified โดยไม่มีรายการถอนหรือยอดที่กันไว้เพิ่ม แก้การตั้งค่าบัญชีก่อนส่งคำขอใหม่ ดู สร้างรายการถอนที่รองรับ
Target contract — Public API ลงทะเบียนและยืนยันบัญชี ยังไม่เปิดใน implementation 0092
flow ด้านล่างเป็นเป้าหมายการพัฒนาต่อ
endpoint และ webhook การลงทะเบียน beneficiary ในส่วนนี้ไม่ได้เปิดใช้โดย Live P2P profile นี้ อย่าเรียกสร้างบัญชีจากตัวอย่างต่อด้วยรายการถอนโดยสมมติว่า API เหล่านี้พร้อมแล้ว
บัญชีที่จะรับเงินถอนต้องผ่านการยืนยันกับธนาคารก่อน จึงจะเอามาใช้สร้างรายการถอนได้
เจ้าอื่นให้กรอกเลขบัญชีตอนสั่งจ่ายได้เลย ของเราไม่ได้
gateway ส่วนใหญ่รับเลขบัญชีปลายทางมาพร้อมคำสั่งจ่ายเงิน แล้วค่อยไปพบทีหลังว่าเลขผิด
ของเราแยกเป็นสองขั้นตอน: ลงทะเบียนบัญชี → รอผลยืนยันจากธนาคาร → ค่อยสร้างรายการถอน
สิ่งที่คุณต้องทำเพิ่ม: เพิ่ม 1 หน้าจอ "จัดการบัญชีรับเงิน" ที่แยกจากหน้าถอน และมีสถานะรอระหว่างที่ผลยืนยันยังไม่กลับมา (ปกติน้อยกว่า 30 วินาที) · อย่าเขียนโค้ดที่สร้าง beneficiary แล้วยิงสร้างรายการถอนต่อทันทีในคำขอเดียว มันจะได้ 409 ทุกครั้ง
ทำไมต้องมี
ในระบบนี้เงินไม่ได้ออกจากบัญชีกลาง — ผู้ฝากคนหนึ่งโอนตรงเข้าบัญชีธนาคารที่คุณระบุ เมื่อโอนแล้วเรียกคืนไม่ได้ เพราะเป็นการโอนระหว่างบัญชีบุคคลจริง
| ถ้าไม่ยืนยันก่อน | สิ่งที่จะเกิด |
|---|---|
| เลขบัญชีพิมพ์ผิด 1 หลัก | เงินเข้าบัญชีคนอื่นจริง ไม่มีปุ่มยกเลิก ต้องไปตามเป็นรายกรณีกับธนาคาร |
| ชื่อบัญชีไม่ตรงกับเจ้าของ | เป็นช่องทางฟอกเงิน และผิดเงื่อนไขที่ธนาคารกำหนดกับผู้ให้บริการ |
| บัญชีถูกปิด/อายัด | ผู้ฝากโอนไม่ผ่าน ก้อนนั้นค้าง รายการถอนวิ่งไม่จบ |
และเพราะ เงินของคุณถูกล็อกตั้งแต่วินาทีที่สร้างรายการถอนสำเร็จ การปล่อยให้สร้างรายการด้วยบัญชีที่ยังไม่รู้ว่าใช้ได้จริงไหม แปลว่าเงินคุณโดนล็อกทิ้งไว้กับปลายทางที่อาจไม่มีอยู่ ⇒ ระบบจึงบังคับให้ยืนยันบัญชีให้ผ่าน ก่อน ที่จะมีการล็อกเงินใด ๆ
Flow ทั้งหมด 4 ขั้น
1 POST /v1/pay/beneficiaries → 201 { beneficiary_id, status: "pending_verification" }
2 ระบบยืนยันชื่อกับธนาคาร (async) → webhook beneficiary.verified หรือ beneficiary.rejected
3 GET /v1/pay/beneficiaries?end_user_ref=... → รายการที่ verified แล้ว (ไว้ทำ dropdown)
4 POST /v1/pay/withdrawals ด้วย beneficiary_id → 2011 · ลงทะเบียนบัญชี
POST /v1/pay/beneficiaries| field | บังคับ | หมายเหตุ |
|---|---|---|
end_user_ref | ✅ | id ผู้ใช้ในระบบคุณ · บัญชีผูกกับผู้ใช้คนนี้ |
bank_code | ✅ | 3 หลัก จาก GET /v1/pay/banks |
account_number | ✅ | ตัวเลขล้วน — client ต้องตัด - และช่องว่างออกก่อนส่ง |
account_name_th | ✅ | 🔴 ห้ามมีคำนำหน้า (นาย / นาง / น.ส. / คุณ) |
promptpay_id | — | ถ้ามี |
curl
curl -sS -X POST 'https://api.p2peasypay.com/v1/pay/beneficiaries' \
-H 'Authorization: Bearer sk_live_9f2c1ab6e0d74f' \
-H 'PG-Timestamp: 1755744902' \
-H 'PG-Nonce: 01K3PA2N4Q6S8U0W2Y4A6C8E0G' \
-H 'PG-Version: 2026-08-01' \
-H 'Idempotency-Key: ben-u88213-014-1234' \
-H 'PG-Signature: v1=8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b' \
-H 'Content-Type: application/json' \
-d '{
"end_user_ref": "u_88213",
"bank_code": "014",
"account_number": "4051234567",
"account_name_th": "สมชาย ใจดี",
"promptpay_id": "0812345678"
}'Response 201
{
"beneficiary_id": "ben_01K2WZ5T8Q3M7XR0VYD4NHF6BC",
"status": "pending_verification"
}201 ยังใช้ถอนไม่ได้
pending_verification แปลว่า "รับคำขอไว้แล้ว กำลังถามธนาคาร" · เอา beneficiary_id นี้ไปสร้างรายการถอนตอนนี้จะได้ 409 beneficiary_not_verified และ ไม่มีการล็อกเงิน ไม่มี order ถูกสร้าง
ให้รอ webhook beneficiary.verified (ปกติน้อยกว่า 30 วินาที) แล้วค่อยเปิดปุ่มถอนให้ผู้ใช้
Node (fetch)
import { pgFetch } from './pg-client.js'
export async function registerBeneficiary({ userId, bankCode, accountNumber, accountNameTh, promptpayId }) {
const res = await pgFetch('POST', '/v1/pay/beneficiaries', {
idempotencyKey: `ben-${userId}-${bankCode}-${accountNumber.slice(-4)}`,
body: {
end_user_ref: userId,
bank_code: bankCode,
account_number: accountNumber.replace(/\D/g, ''), // ตัด - และช่องว่างออก
account_name_th: accountNameTh.trim(),
...(promptpayId ? { promptpay_id: promptpayId } : {}),
},
})
if (res.status !== 201) throw new PgError(res.body.error)
await db.none(
`INSERT INTO pg_beneficiaries (beneficiary_id, end_user_ref, status)
VALUES ($1,$2,$3) ON CONFLICT (beneficiary_id) DO NOTHING`,
[res.body.beneficiary_id, userId, res.body.status]
)
return res.body // status = 'pending_verification' → หน้าจอต้องขึ้นสถานะรอ
}PHP
<?php
function register_beneficiary(string $userId, string $bankCode, string $accountNumber, string $nameTh, ?string $promptpayId = null): array
{
$payload = [
'end_user_ref' => $userId,
'bank_code' => $bankCode,
'account_number' => preg_replace('/\D/', '', $accountNumber),
'account_name_th' => trim($nameTh),
];
if ($promptpayId !== null) {
$payload['promptpay_id'] = $promptpayId;
}
$idemKey = 'ben-' . $userId . '-' . $bankCode . '-' . substr($accountNumber, -4);
[$status, $body] = pg_request('POST', '/v1/pay/beneficiaries', $payload, $idemKey);
if ($status !== 201) {
throw new PgError($body['error']);
}
return $body; // ['beneficiary_id' => ..., 'status' => 'pending_verification']
}2 · ผลการยืนยันมาทาง webhook
ระบบยืนยันชื่อบัญชีกับธนาคารแบบ async แล้วส่ง event หนึ่งในสองตัวนี้
| event | หมายถึง | ทำอะไรต่อ |
|---|---|---|
beneficiary.verified | ธนาคารยืนยันแล้วว่าบัญชีนี้มีจริงและชื่อตรง | เปิดปุ่มถอนให้บัญชีนี้ |
beneficiary.rejected | ใช้ไม่ได้ · ดู code | ให้ผู้ใช้แก้แล้วลงทะเบียนใหม่ |
code ของ beneficiary.rejected
code | หมายถึง | ข้อความที่ควรขึ้น |
|---|---|---|
NAME_MISMATCH | ชื่อที่กรอกไม่ตรงกับชื่อเจ้าของบัญชีที่ธนาคารมี | "ชื่อบัญชีไม่ตรงกับที่ธนาคารบันทึกไว้ ตรวจสอบการสะกดอีกครั้ง" |
ACCOUNT_NOT_FOUND | ไม่พบเลขบัญชีนี้ที่ธนาคารดังกล่าว | "ไม่พบเลขบัญชีนี้ ตรวจสอบเลขบัญชีและธนาคาร" |
BANK_UNAVAILABLE | ธนาคารตรวจสอบไม่ได้ชั่วคราว | "ธนาคารนี้ตรวจสอบไม่ได้ชั่วคราว ลองใหม่อีกครั้งภายหลัง" — 🔴 อันนี้ ไม่ใช่ความผิดผู้ใช้ ให้มีปุ่มลองใหม่ ไม่ใช่ให้กรอกใหม่ทั้งฟอร์ม |
{
"id": "evt_01K2WZ6V9R4N8QT2SM0HYDXKPC",
"object": "event",
"type": "beneficiary.verified",
"api_version": "2026-08-01",
"livemode": true,
"created_at": "2026-08-20T08:14:07.226Z",
"sequence": 90114,
"account": { "merchant_id": "mch_01K2G4H9RT", "white_label_id": "wl_01K1QZ2MSV" },
"data": {
"object": {
"beneficiary_id": "ben_01K2WZ5T8Q3M7XR0VYD4NHF6BC",
"status": "verified",
"end_user_ref": "u_88213",
"bank_code": "014"
},
"previous_attributes": { "status": "pending_verification" }
}
}parser ต้องทนกับ field ที่ไม่รู้จัก
สัญญาระบุชื่อ event และชุด code ไว้แน่นอน แต่ไม่ได้ล็อกรายชื่อ field ทั้งหมดของ object นี้ ⇒ อ่านเฉพาะ beneficiary_id · status · (กรณี rejected) code แล้วปล่อย field อื่นผ่านไป ห้าม throw เมื่อเจอ key ที่ไม่รู้จัก
// ในตัว handler กลางของ webhook หลัง verify ลายเซ็นแล้ว
case 'beneficiary.verified':
await db.query(
`UPDATE pg_beneficiaries SET status = 'verified' WHERE beneficiary_id = $1`,
[event.data.object.beneficiary_id]
)
await notifyUserWithdrawalReady(event.data.object.end_user_ref)
break
case 'beneficiary.rejected':
await db.query(
`UPDATE pg_beneficiaries SET status = 'rejected', reject_code = $2 WHERE beneficiary_id = $1`,
[event.data.object.beneficiary_id, event.data.object.code]
)
break3 · ดึงรายการที่ใช้ได้แล้ว
GET /v1/pay/beneficiaries?end_user_ref=u_88213คืนเฉพาะบัญชีที่ verified แล้ว ⇒ ใช้ทำ dropdown ในหน้าถอนได้ตรง ๆ โดยไม่ต้องกรองเอง
ให้ "เลือก" ดีกว่าให้ "พิมพ์"
ทุกครั้งที่ผู้ใช้พิมพ์เลขบัญชีใหม่คือโอกาสพิมพ์ผิดและต้องรอ verify อีกรอบ · หน้าถอนควรเป็น dropdown ของบัญชีที่ verified แล้ว + ลิงก์เล็ก ๆ "เพิ่มบัญชีใหม่" ที่พาไปหน้าลงทะเบียน
4 · เอาไปสร้างรายการถอน
{
"amount_minor": 100000,
"currency": "THB",
"merchant_reference": "WD-20260821-0007",
"end_user_ref": "u_88213",
"beneficiary_id": "ben_01K2WZ5T8Q3M7XR0VYD4NHF6BC"
}ถ้าส่ง beneficiary{} แบบกรอกสดแทน beneficiary_id ระบบยังยอมรับ แต่บัญชีนั้นต้องเคย verify ผ่านมาก่อนแล้ว ไม่งั้น:
{
"error": {
"code": "beneficiary_not_verified",
"type": "invalid_request_error",
"message": "Beneficiary account has not completed verification.",
"message_th": "บัญชีผู้รับเงินยังไม่ผ่านการยืนยัน",
"retryable": true,
"retry_after_seconds": null,
"request_id": "req_01JQA5T8V0X2Z4B6D8F0H2J4L6"
}
}🔴 409 ใบนี้ ไม่มี order ถูกสร้าง และไม่มีการล็อกเงิน ⇒ ถ้าคุณล็อกยอดผู้ใช้ไว้ในระบบตัวเองก่อนยิง ต้องปลดล็อกคืนทันที
สรุปสิ่งที่ต้องมีในระบบคุณ
- [ ] ตาราง
pg_beneficiariesเก็บbeneficiary_id·end_user_ref·status·reject_code - [ ] หน้าจอเพิ่มบัญชี แยกจากหน้าถอน + สถานะ "กำลังยืนยันกับธนาคาร"
- [ ] handler ของ
beneficiary.verifiedและbeneficiary.rejected - [ ] หน้าถอนใช้ dropdown จาก
GET /v1/pay/beneficiariesไม่ใช่ช่องพิมพ์เลขบัญชี - [ ] ปุ่มถอนถูกปิดไว้จนกว่าผู้ใช้จะมีบัญชีที่
verifiedอย่างน้อย 1 บัญชี - [ ]
BANK_UNAVAILABLEมีปุ่ม "ลองใหม่" ไม่ใช่ให้กรอกฟอร์มใหม่ทั้งใบ
ขั้นต่อไป
| ถ้าคุณกำลังจะ | ไปที่ |
|---|---|
| สร้างรายการถอนใบแรก | สร้างรายการถอน |
| แสดงยอดสุทธิก่อนกดยืนยัน | คำนวณค่าธรรมเนียมล่วงหน้า |
| เขียน handler ของ webhook รายการถอน | รับ fill หลายก้อน |
| ตรวจลายเซ็น webhook ให้ผ่าน | ตรวจลายเซ็น Ed25519 |
| ดูกฎบัญชีฝั่งผู้โอน (คนละเรื่องกับหน้านี้) | บัญชีผู้โอนของรายการฝาก |
