Webhook ของ P2P

flow ทั้งสองฝั่ง payload จริงทุก event การตรวจลายเซ็น และกับดักที่พลาดแล้วเสียเงิน

Use Case

Matched exactly, one leg

A single depositor for the exact amount. Done in 3 minutes.

You receive 3 webhooks, in this order

  1. 1
    PAYMENT_PAIDthe deposit side is paid here
  2. 2
    WITHDRAWAL_PARTIALLY_FUNDEDfunding_status: FULLY_SETTLED — do not post from this one
  3. 3
    WITHDRAWAL_COMPLETEDchannel: P2P_PURE — post here

Post to your ledger from the third webhook only. The second is a progress report even though it reads FULLY_SETTLED.

  1. 1
    MerchantCreate a withdrawal for 500

    The record waits open with no legs yet

    no webhook
  2. 2
    PlatformA depositor arrives; leg 1/1 opens for 500

    The withdrawer account and a QR are revealed; the 10-minute clock starts

    no webhook
  3. 3
    DepositorTransfers 500 straight to the customer, then attaches the slip

    The money never touches the central account

    no webhook
  4. 4
    PlatformSlip clears; the deposit order is credited
    PAYMENT_PAIDthe deposit side is paid here
  5. 5
    PlatformAnnounces that this leg settled
    WITHDRAWAL_PARTIALLY_FUNDEDfunding_status: FULLY_SETTLED — do not post from this one
  6. 6
    PlatformCloses the withdrawal
    WITHDRAWAL_COMPLETEDchannel: P2P_PURE — post here

P2P ทำงานยังไง

ผู้ฝากโอนตรงเข้าบัญชีผู้ถอน — เงินไม่ผ่านบัญชีกลาง โอนครั้งเดียวจบทั้งรายการฝากและรายการถอน

ฝาก

  1. 1

    ลูกค้าขอฝาก → ระบบจับคู่กับรายการถอนที่ยังขาดยอด

  2. 2

    เปิดเลขบัญชีผู้ถอนและ QR ให้ลูกค้า มีเวลาโอน 10 นาที

  3. 3

    ลูกค้าโอนเข้าบัญชีนั้นแล้วแนบสลิป

  4. 4

    ตรวจสลิปผ่าน → PAYMENT_PAID

มี webhook ตัวเดียวคือ PAYMENT_PAID — ระหว่างทาง (จับคู่ / รอโอน / ตรวจสลิป / หมดอายุ / ข้อพิพาท) เงียบทั้งหมด ต้อง poll เอา ยกเว้นสลิปซ้ำที่ได้ PAYMENT_FAILED

ถอน

  1. 1

    ลูกค้าขอถอน 1,000

    ยังไม่ถูกตัดขาตอนนี้ — รายการเปิดรอไว้เฉย ๆ

  2. 2

    ผู้ฝากทยอยเข้ามา ระบบเปิดขาใหม่ให้ทีละราย

    ขาถูกเพิ่มตามที่มีคนมา ไม่ได้ตัดล่วงหน้า และไม่จำเป็นต้องเป็นรายการฝากที่ร้านสร้างคู่กัน

  3. 3

    ผู้ฝากโอนเข้าบัญชีลูกค้าโดยตรง

  4. 4

    ทุกขาที่สำเร็จได้ WITHDRAWAL_PARTIALLY_FUNDED

    ห้ามลงบัญชี — ไม่มีบล็อก summary โดยเจตนา ดูว่าครบยอดหรือยังจาก progress.funding_status

  5. 5

    หมดเวลาแล้วยังขาด บัญชีกลางโอนส่วนที่เหลือ แล้วจบด้วย event ปลายทาง 1 ใบ

    ลงบัญชีตรงนี้ — COMPLETED / COMPLETED_PARTIALLY / REJECT อย่างใดอย่างหนึ่ง

ยอดที่บัญชีกลางออกให้ถูกหักจากเครดิต THB_P2P ของร้าน แต่ payload ไม่ได้บอกว่าจ่ายไปเท่าไร ต้องดูจาก balance history

รายการถอนจบได้ 3 แบบ — ดูที่ outcome.result

eventoutcome.resultเกิดจากอะไรผลกับลูกค้า
WITHDRAWAL_COMPLETEDFULLY_SETTLEDผู้ฝากเติมครบทุกขา หรือ บัญชีกลางโอนส่วนที่ขาดให้ — สองทางนี้จบเหมือนกัน แยกจาก payload ไม่ได้ ต่างกันแค่ summary.channel เป็น P2P_PURE หรือ HYBRIDได้ครบตามที่ขอ
WITHDRAWAL_COMPLETED_PARTIALLYPARTIALLY_SETTLEDบางขาสำเร็จ ส่วนที่เหลือหาคนเติมไม่ได้และบัญชีกลางไม่ได้โปะ (SPLIT_PARTIAL_UNCOVERED) หรือแพ้ข้อพิพาทบางขา (DISPUTE_LOST)ได้บางส่วน ที่เหลือคืน — เงินถึงลูกค้าไปแล้ว อย่าอ่านเป็นถอนไม่สำเร็จ
WITHDRAWAL_REJECTNOT_SETTLEDไม่มีขาไหนสำเร็จเลย — หมดเวลาจับคู่และไม่มีช่องทางสำรอง (MATCH_EXPIRED_NO_FALLBACK) ยกเลิกเอง (WITHDRAWER_CANCELLED) หรือบัญชีปลายทางใช้ไม่ได้ (INVALID_BANK_ACCOUNT)ไม่ได้เลย คืนเต็มจำนวน
ไม่มี eventธนาคารตีกลับ รายการค้างเป็น FREEZED — ระบบถือว่ายังไม่จบ จึงไม่ประกาศอะไรเลย ต้อง poll GET /v1/withdrawal/detail/:idเงินไม่ถึงลูกค้าและยังไม่คืนร้าน

ยอดคืนลูกค้าคือ summary.withdrawer_pending_refund ส่วน agent_refunded กับ outcome.refunded_amount คือยอดที่คืนเข้าร้าน ซึ่งรวมค่าธรรมเนียมที่ได้คืนตามสัดส่วนไว้ด้วย จึงมากกว่าเสมอ — หยิบผิดตัวไปคืนลูกค้าคือคืนเกิน

อย่าแยกด้วยชื่อ event — PARTIALLY_SETTLED ที่อ่านเป็น "ถอนไม่สำเร็จ" แล้วคืนเงินเต็ม จะกลายเป็นจ่ายซ้ำ

ในใบ PARTIALLY_FUNDED อย่าอ่าน withdrawal.withdrawal_status — บางใบเป็น COMPLETED แล้วเพราะประกอบจากแถวหลัง settle ให้ดู progress.funding_status เท่านั้น

split_total เพิ่มได้ระหว่างทาง — ขาที่เคยเป็น 1/1 กลายเป็น 1/2 เมื่อมีขาใหม่เข้ามา อย่าเก็บค่าเก่าไปคำนวณ

ขาที่จบด้วยการตัดสินข้อพิพาท ไม่ส่ง PARTIALLY_FUNDED ต่างจากขาที่ settle ปกติ — ฝั่งถอนจะเงียบจนกว่ารายการจะจบ

ฝั่งฝาก — ได้กี่ใบ

จับคู่ P2P สำเร็จ1PAYMENT_PAID (deposit_type: P2P)
ตกไปช่องธนาคาร1PAYMENT_PAID (deposit_type: CLASSIC)
สลิปซ้ำ1PAYMENT_FAILED (DUPLICATE_SIGNATURE)
หมดอายุ / ยกเลิก / ข้อพิพาท0— ต้อง poll

payload ฝั่ง P2P ไม่มีบล็อก transaction — parser ต้องถือว่าเป็น optional

ฝั่งถอน — ไทม์ไลน์จริงจาก testnet

12:12สร้างรายการถอน 1,000ไม่มี
12:13ขา 1 จับคู่ผู้ฝาก 400ไม่มี
12:13ขา 1 โอนครบ — ยังขาด 600WITHDRAWAL_PARTIALLY_FUNDED
12:16ขา 2 จับคู่ผู้ฝาก 300ไม่มี
12:16ผู้ฝากขา 2 เปิดข้อพิพาทไม่มี
14:38แอดมินตัดสินให้ผู้ฝากPAYMENT_PAID
14:38ปิดหน้าต่าง split เหลือ 300 ให้บัญชีกลางไม่มี
14:39บัญชีกลางโอน 300 สำเร็จ → ครบยอดWITHDRAWAL_COMPLETED

Event Types

Payment/PAYMENT_PAID
{
  "event": "PAYMENT_PAID",
  "data": {
    "deposit_type": "P2P",
    "payment": {
      "id": "8683af40-9186-4871-a8b7-0d04fff4c4ae",
      "seq_num": 4478,
      "source_id": "wpayz",
      "slip_ref": null,
      "order_id": "744352fd-8271-49b3-bf5a-500e8e678ef0",
      "order_user_reference": "USER134",
      "order_auto_cancel_previous": true,
      "order_display_mode": "FIAT",
      "request_ip": null,
      "request_platform": "API",
      "payment_method_type": "P2P",
      "invoice_type": "FIAT",
      "from_currency": "THB",
      "to_currency": "THB",
      "address": "0641236101",
      "from_address": "0000000000",
      "amount": "500",
      "payment_amount": 500,
      "payer_paid_currency": "THB",
      "payer_paid_amount": 500,
      "payer_bank_provider": "TEST",
      "payer_bank_account_number": "0000000000",
      "payer_bank_account_name": "TEST SENDER",
      "merchant_promptpay_id": null,
      "merchant_amount": 500,
      "merchant_number": "0641236101",
      "merchant_name": "SOMCHAI JAIDEE",
      "merchant_provider": "PromptPay",
      "ebank_account_id": null,
      "is_multiple_order": true,
      "lifetime": 600,
      "expired_at": "2026-08-09T05:12:06.210Z",
      "fee_subtract": 0,
      "discount_percent": 0,
      "discount_amount": 0,
      "accuracy_percent": 0,
      "additional_data": null,
      "audit_trail": [],
      "order_tracking_id": "AQMW3TH0",
      "payment_url": null,
      "payment_qr": null,
      "payment_domain": null,
      "url_return": "",
      "url_success": "",
      "url_failed": "",
      "exchange_rate": 0.02992803,
      "exchange_rate_source": "COINGECKO",
      "tx_value": 0,
      "fx_rate": null,
      "payment_status": "PAYMENT_PAID",
      "payment_match_type": "EXACTLY",
      "is_completed": false,
      "status": "COMPLETED",
      "failed_reason": "UNKNOWN",
      "audit_by_user_id": null,
      "audit_time": "2026-08-09T05:05:27.781Z",
      "sequence_time": "2026-08-09T05:02:06.226Z",
      "created_at": "2026-08-09T05:02:06.226Z",
      "updated_at": "2026-08-09T05:05:27.833Z",
      "request_withdrawal_id": "22266595-2de3-4532-baa4-4069adfc682b",
      "request_withdrawal_sequence_time": "2026-08-09T04:59:46.488Z",
      "organization_id": "5f5a3ae8-6fa4-47fd-bb4f-231935a32eb8",
      "agent_id": "b1fe1b91-5054-49cc-90f4-f280ab88c9f4",
      "partner_agent_id": null
    },
    "idempotency_key": "8683af40-9186-4871-a8b7-0d04fff4c4ae:PAYMENT_PAID"
  },
  "type": "FIAT",
  "request_id": "req_1786251927914_reyfnjurpoc"
}

Header ที่ระบบส่งมาจริง

HTTP header ไม่สนตัวพิมพ์เล็กใหญ่ ให้อ่านแบบ case-insensitive

headerตัวอย่างค่าใช้ทำอะไร
trust-x-eventWITHDRAWAL_COMPLETEDroute ได้ก่อน parse JSON — ตรงกับ body.event
trust-x-request-idreq_1786251928335_l7t7uvlvuvrตรงกับ request_id ใน body ใช้แจ้งซัพพอร์ตเวลาตามรอยใบใดใบหนึ่ง
x-idempotency-key22266595-…:WITHDRAWAL_COMPLETEDคีย์เดียวที่ใช้ dedup ได้ ตรงกับ data.idempotency_key
x-signaturesha256=19c38d2a…HMAC-SHA256 ของ {timestamp}.{JSON ของ data} มี prefix sha256=
x-timestamp1786251928หน่วยวินาที ใช้ทั้งเช็คอายุและเป็นส่วนหนึ่งของลายเซ็น
user-agentaxios/1.13.2ไม่ต้องใช้ตรวจอะไร บันทึกไว้เฉย ๆ

x-timestamp ของ webhook เป็น วินาที (10 หลัก) ส่วน x-timestamp ตอนที่เราเรียก API ของระบบเป็นมิลลิวินาที (13 หลัก) — คำนวณผิดหน่วยแล้ว signature ไม่ผ่านทุกใบ

worker ทำอะไรกับแต่ละ event

eventสิ่งที่ควรทำคีย์ที่ใช้
PAYMENT_PAIDเครดิตออเดอร์ตาม data.payment.order_id — ทำครั้งเดียวต่อ payment เดียว ห้ามดูจาก deposit_type ว่ามาทางไหนpayment.id
PAYMENT_FAILEDปิดออเดอร์ฝากเป็นไม่สำเร็จ — เกิดกรณีเดียวคือสลิปซ้ำpayment.id
WITHDRAWAL_PARTIALLY_FUNDEDอัปเดต progress ที่แสดงผลเท่านั้น ห้ามลงบัญชี — ไม่มี summary ใน payload นี้match id
WITHDRAWAL_COMPLETEDปิดรายการถอนและลงบัญชีตาม summary.withdrawer_received — ไม่ใช่ withdrawal.amountwithdrawal.id
WITHDRAWAL_COMPLETED_PARTIALLYลูกค้าได้เงินไปแล้วบางส่วน — ลงบัญชีตาม summary.withdrawer_received และคืนลูกค้าเฉพาะ summary.withdrawer_pending_refund ห้ามใช้ agent_refunded เพราะรวมค่าธรรมเนียมที่คืนเข้าร้าน ไม่ใช่ยอดของลูกค้าwithdrawal.id
WITHDRAWAL_REJECTไม่มีเงินถึงลูกค้าเลย คืนเต็มจำนวน — payload มี summary และ outcome เหมือน event ปลายทางตัวอื่น ยอดคืนลูกค้าอ่านจาก summary.withdrawer_pending_refund (สะกดไม่มี -ED)withdrawal.id
PAYMENT_CANCELEDยกเลิกออเดอร์ตาม data.payment.idอาจเป็นรายการเก่าที่ถูกยกเลิกอัตโนมัติเมื่อผู้จ่ายคนเดิมสร้างรายการใหม่ อย่าเดาว่าเป็นใบล่าสุดpayment.id
PAYMENT_CONVERTEDมาต่อจาก PAYMENT_PAID เมื่อเปิด auto-convert — อัปเดตสกุลที่ settle จริง คนละ system_id ต้อง join กลับด้วย data.payment.idconvert:<id>
WITHDRAWAL_APPROVED / _EXPIREDเกิดกับ CRYPTO เท่านั้น — ฝั่ง FIAT/P2P ไม่ต้องรอสองตัวนี้withdrawal.id
WITHDRAWAL_VERIFYsynchronous — ยิงระหว่างที่ร้านเรียก API สร้างรายการถอน ต้องตอบ 2xx ใน 10 วินาที ห้ามเข้าคิวเดียวกับ event อื่น— (ยังไม่มี id)
defaultevent อื่น (classic, crypto, TRANSACTION_NEW) — บันทึกไว้แล้วตอบ 200 อย่าโยน error เพราะระบบจะถือว่าส่งไม่สำเร็จและไม่มีการส่งซ้ำ

เขียน handler อย่างไร

endpoint เดียวรับทั้งฝากและถอน — ระบบมี URL เดียวต่อ 1 agent จึงแยก endpoint ตามประเภทไม่ได้ ให้ route ด้วย body.event · ขั้นรับเหมือนกันทุก event ต่างกันแค่ขั้นสุดท้ายใน worker

ต้องจบภายใน 10 วินาทีPOST /webhooks (endpoint เดียว)ลายเซ็นตรงหรือไม่HMAC(ts + "." + JSON(data))ไม่ตรง401 + alertตรงevent = WITHDRAWAL_VERIFY ?ใช่ตอบผลทันที — ห้ามเข้าคิวไม่ใช่x-idempotency-key ซ้ำหรือไม่INSERT ... ON CONFLICTซ้ำตอบ 200 แล้วจบใหม่ตอบ 200 + enqueueประมวลผลจริงแบบ async — ไม่มีเพดานเวลาswitch (body.event)PAYMENT_* → ล็อกออเดอร์ฝากWITHDRAWAL_* → ล็อกรายการถอน
endpoint ที่ช้าแยกไม่ออกจาก endpoint ที่ล่ม และไม่มีการส่งซ้ำอัตโนมัติ — งานจริงต้องอยู่หลังเส้น 200
รับ request — endpoint เดียวทุก event (Node / Express)
app.post('/webhooks/p2p',
  express.raw({ type: 'application/json' }),   // ต้องเก็บ raw body ไว้
  async (req, res) => {
    const raw = req.body.toString('utf8')
    const body = JSON.parse(raw)

    // 1) timestamp ก่อน ถูกกว่าคำนวณ HMAC
    //    x-timestamp ของ webhook เป็น "วินาที" (10 หลัก) ไม่ใช่มิลลิวินาที
    //    คนละหน่วยกับ x-timestamp ตอนที่เราเรียก API ของระบบ
    const ts = req.header('x-timestamp') ?? ''
    if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
      return res.sendStatus(401)
    }

    // 2) ลายเซ็นครอบเฉพาะ object data
    const expected = 'sha256=' + crypto
      .createHmac('sha256', process.env.WEBHOOK_SECRET)
      .update(`${ts}.${JSON.stringify(body.data)}`)
      .digest('hex')

    const got = req.header('x-signature') ?? ''
    if (got.length !== expected.length ||
        !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
      logger.warn({ event: body.event }, 'bad signature')
      return res.sendStatus(401)
    }

    // 3) dedup ที่ x-idempotency-key เท่านั้น (unique index บนคอลัมน์นี้)
    const key = req.header('x-idempotency-key')
    const { rowCount } = await db.query(
      `INSERT INTO webhook_inbox (idempotency_key, event, payload)
        VALUES ($1, $2, $3) ON CONFLICT (idempotency_key) DO NOTHING`,
      [key, body.event, raw],
    )
    if (rowCount === 0) return res.sendStatus(200)   // เคยรับแล้ว ไม่ทำซ้ำ

    // 4) ตอบก่อน แล้วค่อยประมวลผล
    await queue.add('p2p-webhook', { key })
    return res.sendStatus(200)
  })
worker — route ตาม event
// event ปลายทางชนะเสมอ — webhook ไม่รับประกันลำดับ
const RANK = {
  WITHDRAWAL_PARTIALLY_FUNDED: 1,
  PAYMENT_PAID: 2,
  PAYMENT_FAILED: 2,
  WITHDRAWAL_COMPLETED: 3,
  WITHDRAWAL_COMPLETED_PARTIALLY: 3,
  WITHDRAWAL_REJECT: 3,
}

async function handle({ key }) {
  const { event, payload } = await inbox.get(key)
  const data = JSON.parse(payload).data

  await db.tx(async (tx) => {
    const order = await tx.selectForUpdate(orderIdOf(data))   // row lock ต่อ 1 รายการ

    if (RANK[event] < RANK[order.last_event ?? '']) return    // ของเก่ามาช้า → ทิ้ง

    switch (event) {
      case 'PAYMENT_PAID':
        await tx.credit(order, data.payment.amount); break
      case 'WITHDRAWAL_PARTIALLY_FUNDED':
        await tx.updateProgress(order, data.progress); break  // แสดงผลอย่างเดียว
      // event ปลายทางทั้งสามตัวใช้ทางเดียวกัน แยกด้วย outcome.result ไม่ใช่ชื่อ event
      case 'WITHDRAWAL_COMPLETED':
      case 'WITHDRAWAL_COMPLETED_PARTIALLY':
      case 'WITHDRAWAL_REJECT':
        await tx.settle(order, data.summary.withdrawer_received)          // 0 ได้
        await tx.refundCustomer(order, data.summary.withdrawer_pending_refund ?? 0)
        break                                                             // ห้ามใช้ agent_refunded ตรงนี้
    }
    await tx.setLastEvent(order, event)
  })
}
  • ตาราง dedup ใช้ตารางเดียว — คีย์เป็น <system_id>:<EVENT> จึงไม่ชนกันข้ามฝากถอนอยู่แล้ว
  • row lock ต้องล็อกคนละ entity — event ฝากล็อกออเดอร์ฝาก event ถอนล็อกรายการถอน อย่าใช้ล็อกร่วมกันเพราะจะบล็อกกันเองโดยไม่จำเป็น
  • data มีโครงต่างกันต่อ event (payment / withdrawal / matches / progress) — parser ต้องถือว่าทุกบล็อกเป็น optional
  • endpoint นี้ยังรับ event ของช่องทางอื่นด้วย (classic, crypto) — default ของ switch ควรบันทึกไว้เฉย ๆ แล้วตอบ 200 ไม่ใช่โยน error
  • ข้อยกเว้นเดียว: WITHDRAWAL_VERIFY เป็น synchronous ต้องตอบผลในคำขอนั้นเลย ห้ามเข้าคิวเดียวกัน — ดู Withdraw verify
  • ส่งครั้งเดียว ไม่มี retry — ต้องมี job กระทบยอดที่ poll ตามตารางด้านบน
  • ไม่รับประกันลำดับ — COMPLETED มาถึงก่อนขาสุดท้ายได้ ให้ event ปลายทางชนะเสมอ
  • ขอส่งซ้ำได้ที่ POST /v1/webhookRequest/resend/:id — ข้าม dedup และใช้คีย์เดิม จึงต้อง idempotent
  • ทดสอบด้วย POST /v1/p2p/acceptForTesting/:id บน testnet (ใส่ match id หรือ payment id)

เหตุการณ์ที่ไม่มี webhook

เหตุการณ์เหล่านี้ไม่ส่ง webhook สามารถใช้ poll แทนได้

สถานการณ์ต้องรู้ได้อย่างไร
สลิปอยู่ระหว่างตรวจ (PAYMENT_CHECKING)ค้างได้หลายนาที — GET /v1/payment/info
สร้างรายการสำเร็จเก็บจาก response ตอนสร้าง (รวม order_tracking_id ที่ค้นย้อนหลังไม่ได้)
จับคู่ได้แล้ว / คิวหมดอายุGET /v1/p2p/public/flow/:flowId
รายการฝากหมดอายุGET /v1/payment/info
สลิปถูกปฏิเสธHTTP error ที่ API แนบสลิปตอบกลับ
เปิดข้อพิพาท / รอแอดมินตัดสินGET /v1/p2p/match/:matchIdmatch_status — ผลการตัดสินมี webhook แต่การเปิดไม่มี
ถอนถูกธนาคารตีกลับ (FREEZED)GET /v1/withdrawal/detail/:id
คืนเงินส่วนที่ขาดbalance history

สถานะของการจับคู่

สถานะของการจับคู่ P2P

match_status ตรวจสถานะ: GET /v1/p2p/match/:matchId
ระหว่างทาง
WAITING_SLIP

เปิดเผยบัญชีปลายทางแล้ว รอผู้ฝากโอนและแนบสลิป

ไม่มี webhook — ต้อง poll

DISPUTED

ผู้ฝากเปิดข้อพิพาท — ล็อกไว้จนแอดมินตัดสิน ไม่หมดอายุเอง

ไม่มี webhook — ต้อง poll

ปลายทาง
COMPLETED

สลิปผ่าน เครดิตเข้าออเดอร์

webhook: PAYMENT_PAID

DISPUTE_RESOLVED

แอดมินตัดสินแล้ว — ผลไปคนละฝั่งตามคำตัดสิน

webhook

EXPIRED

หมดเวลาโอน ขาถูกปล่อยกลับเข้าคิว

ไม่มี webhook — ต้อง poll

CANCELLED

ผู้ฝากยกเลิกก่อนเห็นบัญชีปลายทาง

ไม่มี webhook — ต้อง poll

CANCELLED_HELD

ยกเลิกขณะยังกันยอดไว้

ไม่มี webhook — ต้อง poll

RELEASED

ปล่อยยอดที่กันไว้กลับคืน

ไม่มี webhook — ต้อง poll

มีแค่ COMPLETED และ DISPUTE_RESOLVED ที่ส่ง webhook — สถานะปลายทางที่เหลือเงียบทั้งหมด

webhook หลังการตัดสิน dispute

ตัดสินโดยแอดมินเท่านั้น ไม่มีระบบตัดสินอัตโนมัติ รายการค้างจนกว่าจะมีคนกด — ผลแต่ละทางส่ง webhook ไปคนละฝั่ง

match_statusDISPUTEDล็อกเงินผู้ถอนไว้ · ไม่มี auto-resolveตัดสินให้ผู้ฝากตัดสินให้ผู้ถอนPAYMENT ของผู้ฝากPAYMENT_PAIDยอดหักค่าธรรมเนียมฝากตามปกติwebhook → ร้านผู้ฝากWITHDRAWAL ฝั่งตรงข้ามส่งต่อรางธนาคาร · ยัง PENDINGยังค้างจ่าย — ฝั่งถอนเงียบที่จุดนี้ไม่มี webhook — poll withdrawal/detailได้ COMPLETED เมื่อธนาคารโอนเสร็จPAYMENT ของผู้ฝากไม่ถูกแตะ · reputation ถูกหักไม่มี webhookWITHDRAWAL ฝั่งตรงข้ามFAILEDคืนเงินเต็ม realized_amountwebhook → ร้านผู้ถอนWITHDRAWAL_FAILED
แอดมินเป็นผู้ตัดสินเท่านั้นและย้อนกลับไม่ได้ — ตัดสินซ้ำจะได้ error P2P_INVALID_STATUS_TRANSITION (38023)
  • ยอดที่ร้านผู้ฝากได้คือยอด payment หักค่าธรรมเนียมฝากตามปกติ
  • ทั้งสองทางย้อนกลับไม่ได้ — ตัดสินซ้ำจะได้ P2P_INVALID_STATUS_TRANSITION (38023)

เมื่อตัดสินให้ผู้ฝาก รายการถอนยังไม่จบ — ถูกส่งต่อไปรางธนาคารและคงสถานะ PENDING ฝั่งถอนจึงยังไม่ได้ webhook จนกว่าธนาคารจะโอนเสร็จ และการตัดสินย้อนกลับไม่ได้

ตัวอย่าง Payload ของฝากอัตโนมัติ

Payment และ Withdrawal ช่องทางปกติ