Withdraw verify

ขั้นตอนฝั่งร้านค้า สำหรับยืนยันคำขอถอนเงินอีกชั้นก่อนระบบทำรายการจริง

Withdraw verify คืออะไร? เป็นระบบยืนยันการถอนเงินสองชั้น (second-layer verification) — ก่อนระบบจะถอนเงินออกจริง ระบบจะส่งคำขอ (webhook) ไปถาม backend ของร้านก่อนว่า “คำขอถอนนี้ถูกต้องไหม” ถ้าร้านตอบ อนุมัติ (HTTP 200) ระบบจึงทำรายการถอน ถ้าตอบอย่างอื่น = ปฏิเสธ. ช่วยกันกรณีมีคนแอบใช้ API key ของร้านสั่งถอนเงิน

  1. 1

    เปิดใช้งานในหน้า API Key & Webhook

    SETUP

    ไปที่ Developer → ตั้งค่า Webhook แล้วตั้ง 3 อย่างนี้:

    หน้า Developer — ตั้งค่า Webhook: Webhook URL, App Secret Verify (HMAC) และ Automatic Approve Withdrawal
    หน้า API Key & Webhook → ตั้งค่า Webhook
    1. 1Webhook URLURL ที่ระบบจะส่งคำขอยืนยันมาหา (ตัวเดียวกับ webhook ของร้าน) — ควรเป็น HTTPS และ whitelist IP ตามที่หน้าจอแจ้ง
    2. 2App Secret Verify (HMAC)กด “สร้าง Secret” เพื่อสร้าง secret สำหรับตรวจ signature — แสดงครั้งเดียว คัดลอกเก็บทันที (เช่น WPAY_VERIFY_SECRET)
    3. 3Automatic Approve Withdrawalสวิตช์เปิดการยืนยัน — เปิดแล้วระบบจะอนุมัติเมื่อร้านตอบ HTTP 200 เท่านั้น แล้วกด “บันทึก”

    ต้องมี Webhook URL + Secret ครบก่อนเปิดสวิตช์ (ไม่งั้นได้ error 10061). การปิดสวิตช์หรือเปลี่ยน Webhook URL จะระงับการถอน 24 ชม. (cooldown) — สร้าง Secret ใหม่ไม่โดน

  2. 2

    ระบบส่ง webhook มาขอให้ร้านยืนยัน

    RUNTIME

    เมื่อมีคำขอถอน ระบบจะ POST มาที่ Webhook URL ของร้าน แล้วรอคำตอบไม่เกิน 10 วินาที — เกิดขึ้นก่อนสร้างรายการถอนจริง

    Requestระบบส่งมา — ร้านเป็นฝ่ายรับ
    POST /wpay/withdrawal-verify
    Trust-X-Event: WITHDRAWAL_VERIFY
    Trust-X-Request-ID: verify_ORDER-DEMO-00111
    x-timestamp: 1750072460
    x-signature: sha256=4fdf737a8354e312619911b45a406bab75fb1e4fe956e439e26edbbfcfa2fdc7
    Content-Type: application/json
    
    {
      "event": "WITHDRAWAL_VERIFY",
      "type": "FIAT",
      "request_id": "verify_ORDER-DEMO-00111",
      "data": {
        "order_id": "ORDER-DEMO-00111",
        "order_user_reference": null,
        "withdrawal_mode": "FIAT",
        "amount": 311,
        "currency": "THB",
        "withdrawal_address": "9999999999",
        "receiver_bank": "SCB",
        "receiver_name": "MR. John Snow",
        "chain": null,
        "asset_type": null,
        "agent_id": "d272d889-886f-41ba-98cf-52d55c1fbe21"
      }
    }

    เก็บ x-timestamp (Unix วินาที), x-signature และ body ดิบ (raw) ไว้ใช้ตรวจ signature ในขั้นถัดไป

  3. 3

    ร้านตรวจ signature แล้วตัดสินใจอนุมัติ/ปฏิเสธ

    RUNTIME

    คำนวณ signature ด้วย HMAC-SHA256 จาก เฉพาะ object data (ไม่ใช่ทั้ง body) ตามสูตร HMAC_SHA256(secret, "{x-timestamp}.{JSON ของ data}") แล้วเทียบกับ x-signature ตรงแล้วค่อยเช็คออเดอร์/ยอดเงิน

    signing stringtimestamp ต่อด้วย JSON ของ data — จากตัวอย่างขั้น 2
    1750072460.{"order_id":"ORDER-DEMO-00111","order_user_reference":null,"withdrawal_mode":"FIAT","amount":311,"currency":"THB","withdrawal_address":"9999999999","receiver_bank":"SCB","receiver_name":"MR. John Snow","chain":null,"asset_type":null,"agent_id":"d272d889-886f-41ba-98cf-52d55c1fbe21"}
    # ทดสอบยิง verify webhook เข้า endpoint ของคุณ
    curl -X POST https://merchant.example.com/wpay/withdrawal-verify \
      -H "Trust-X-Event: WITHDRAWAL_VERIFY" \
      -H "x-timestamp: 1750072460" \
      -H "x-signature: sha256=<hmac>" \
      -H "Content-Type: application/json" \
      -d '{"event":"WITHDRAWAL_VERIFY","data":{"order_id":"ORDER-DEMO-00111","amount":311,"currency":"THB"}}'
    Response → ระบบ wpayร้านเป็นฝ่ายตอบ
    HTTP/1.1 200 OK
    Content-Type: application/json
    
    { "ok": true }

    ตัวตัดสินคือ HTTP status code ไม่ใช่ body — 2xx = อนุมัติ; non-2xx, ตอบช้าเกิน 10 วิ หรือเชื่อมต่อไม่ได้ = ปฏิเสธ (fail-closed: ถ้ามีอะไรพลาดถือว่าปฏิเสธไว้ก่อนเพื่อความปลอดภัย)

  4. 4

    ระบบตอบผลคำขอถอนกลับให้ผู้เรียก API (PENDING)

    RUNTIME

    ร้านตอบ 200 → ระบบกันยอดเงิน (reserve) แล้วสร้างรายการถอนสถานะ PENDING นี่คือ response ที่ ผู้เรียก createRequest ได้รับ

    Response 200แสดงเฉพาะฟิลด์หลัก
    {
      "success": true,
      "code": 0,
      "data": {
        "id": "4d59bdf7-c762-4f09-94f2-cfaabefc52e7",
        "order_id": "ORDER-DEMO-00111",
        "withdrawal_mode": "FIAT",
        "amount": "311",
        "currency": "THB",
        "receiver_bank": "SCB",
        "receiver_name": "MR. John Snow",
        "address": "9999999999",
        "withdrawal_status": "PENDING",
        "created_at": "2026-06-16T11:14:20.366Z",
        "agent_id": "d272d889-886f-41ba-98cf-52d55c1fbe21"
      }
    }

    response นี้ ไม่ต้องตรวจ signature — เก็บ id/order_id ไว้ติดตามสถานะ. ถ้าขั้น 3 ตอบ non-2xx จะได้ error WITHDRAWAL_VERIFY_REJECTED (10060) และไม่มีรายการถอน

  5. 5

    รับผลการถอนจริงผ่าน webhook (ภายหลัง)

    RUNTIME

    หลังโอนเสร็จ ระบบส่ง webhook สรุปผลมาอีกครั้ง — WITHDRAWAL_COMPLETED (สำเร็จ) หรือ WITHDRAWAL_REJECTED (ไม่สำเร็จ)

    Webhookระบบส่งมา — ร้านเป็นฝ่ายรับ
    POST /wpay/withdrawal-verify
    Trust-X-Event: WITHDRAWAL_COMPLETED
    Content-Type: application/json
    
    {
      "event": "WITHDRAWAL_COMPLETED",
      "type": "FIAT",
      "data": {
        "withdrawal": {
          "id": "4d59bdf7-c762-4f09-94f2-cfaabefc52e7",
          "order_id": "ORDER-DEMO-00111",
          "amount": "311",
          "currency": "THB",
          "withdrawal_status": "COMPLETED"
        },
        "idempotency_key": "4d59bdf7-...:WITHDRAWAL_COMPLETED"
      }
    }

    อัปเดตสถานะออเดอร์ฝั่งร้านด้วย id/order_id จากขั้น 4 และรองรับการส่งซ้ำ (idempotent) ด้วย idempotency_key. payload เต็มดูที่หน้า Webhook Examples