Skip to content

บัญชีปลายทาง ​

implementation 0092 — รอตรวจและ deploy

Live P2P รุ่นที่พัฒนาแล้ว ใช้เฉพาะบัญชีปลายทางที่มีอยู่แล้ว Public API ลงทะเบียน/ค้นหา/ยืนยัน beneficiary และ Customer API ยังไม่อยู่ในขอบเขตนี้ ผู้ให้บริการต้องยืนยันความพร้อมของ deployment ก่อนใช้งานเงินจริง

บัญชีที่ใช้ได้ในรุ่นนี้ ​

ให้ผู้ดูแลจัดเตรียมบัญชีที่เป็นของ Merchant และ Live mode เดียวกับ key บัญชีต้อง verified, รับรายการใหม่ได้ และผ่านเงื่อนไขตัวตน/ธนาคารปัจจุบัน บัญชีของ Merchant อื่น หรือบัญชี Test ใช้แทนกันไม่ได้

ส่ง beneficiary_id รูปแบบ ben_ + UUID hex พิมพ์เล็ก 32 ตัว:

json
{ "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  →  201

1 · ลงทะเบียนบัญชี ​

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 ​

bash
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 ​

json
{
  "beneficiary_id": "ben_01K2WZ5T8Q3M7XR0VYD4NHF6BC",
  "status": "pending_verification"
}

201 ยังใช้ถอนไม่ได้

pending_verification แปลว่า "รับคำขอไว้แล้ว กำลังถามธนาคาร" · เอา beneficiary_id นี้ไปสร้างรายการถอนตอนนี้จะได้ 409 beneficiary_not_verified และ ไม่มีการล็อกเงิน ไม่มี order ถูกสร้าง

ให้รอ webhook beneficiary.verified (ปกติน้อยกว่า 30 วินาที) แล้วค่อยเปิดปุ่มถอนให้ผู้ใช้

Node (fetch) ​

js
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
<?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ธนาคารตรวจสอบไม่ได้ชั่วคราว"ธนาคารนี้ตรวจสอบไม่ได้ชั่วคราว ลองใหม่อีกครั้งภายหลัง" — 🔴 อันนี้ ไม่ใช่ความผิดผู้ใช้ ให้มีปุ่มลองใหม่ ไม่ใช่ให้กรอกใหม่ทั้งฟอร์ม
json
{
  "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 ที่ไม่รู้จัก

js
// ในตัว 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]
  )
  break

3 · ดึงรายการที่ใช้ได้แล้ว ​

GET /v1/pay/beneficiaries?end_user_ref=u_88213

คืนเฉพาะบัญชีที่ verified แล้ว ⇒ ใช้ทำ dropdown ในหน้าถอนได้ตรง ๆ โดยไม่ต้องกรองเอง

ให้ "เลือก" ดีกว่าให้ "พิมพ์"

ทุกครั้งที่ผู้ใช้พิมพ์เลขบัญชีใหม่คือโอกาสพิมพ์ผิดและต้องรอ verify อีกรอบ · หน้าถอนควรเป็น dropdown ของบัญชีที่ verified แล้ว + ลิงก์เล็ก ๆ "เพิ่มบัญชีใหม่" ที่พาไปหน้าลงทะเบียน

4 · เอาไปสร้างรายการถอน ​

json
{
  "amount_minor": 100000,
  "currency": "THB",
  "merchant_reference": "WD-20260821-0007",
  "end_user_ref": "u_88213",
  "beneficiary_id": "ben_01K2WZ5T8Q3M7XR0VYD4NHF6BC"
}

ถ้าส่ง beneficiary{} แบบกรอกสดแทน beneficiary_id ระบบยังยอมรับ แต่บัญชีนั้นต้องเคย verify ผ่านมาก่อนแล้ว ไม่งั้น:

json
{
  "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
ดูกฎบัญชีฝั่งผู้โอน (คนละเรื่องกับหน้านี้)บัญชีผู้โอนของรายการฝาก

เอกสารชุดนี้อิงสัญญา API เวอร์ชัน 2026-08-01