ยกเลิกรายการฝาก
POST /v1/pay/deposits/{id}/cancel ปิดรายการที่ผู้ใช้ยังไม่ได้โอน
กฎเดียวที่ตัดสินทุกอย่าง
มีสลิปเข้ามาแล้ว = ยกเลิกไม่ได้ ไม่ว่าสถานะจะเป็นอะไร ไม่ว่าจะเพิ่งอัปมาเมื่อวินาทีที่แล้ว
เพราะเมื่อมีสลิป แปลว่าเงินออกจากบัญชีผู้ใช้ไปแล้ว การยกเลิกจะทำให้เงินก้อนนั้นไม่มีรายการรองรับ
ยกเลิกได้เมื่อไหร่
status | ยกเลิกได้ | เงื่อนไข |
|---|---|---|
awaiting_liquidity | ✅ | ได้เสมอ — ยังไม่เคยมีปลายทางให้โอนด้วยซ้ำ |
awaiting_payment | ✅ | เฉพาะเมื่อยังไม่มีสลิป · บางรายการติดเงื่อนไขปลายทางชั่วคราวและยกเลิกไม่ได้ → 409 |
slip_processing | ❌ | โอนแล้ว กำลังตรวจ |
under_review | ❌ | โอนแล้ว รอเจ้าหน้าที่ |
settled | ❌ | เครดิตไปแล้ว |
expired · canceled · rejected · reversed | ❌ | ปิดไปแล้ว |
Request
curl -X POST https://api.p2peasypay.com/v1/pay/deposits/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/cancel \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755750330" \
-H "PG-Nonce: 01JQA5TF2S4U6W8Y0A2C4E6G8J" \
-H "PG-Version: 2026-08-01" \
-H "Idempotency-Key: cancel-ORD-9981-01" \
-H "PG-Signature: v1=3a70...b2c5" \
-H "Content-Type: application/json" \
-d '{ "reason": "user_changed_sender" }'| field | ชนิด | บังคับ | หมายเหตุ |
|---|---|---|---|
reason | string | ✅ | เหตุผลฝั่งคุณ เก็บไว้ให้ support อ่าน · เขียนให้เข้าใจได้ เช่น user_changed_sender · user_changed_amount · abandoned_checkout |
Response 200
{
"id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"object": "deposit_order",
"version": 3,
"status": "canceled",
"is_terminal": false,
"merchant_reference": "ORD-9981",
"amount_minor": 500000,
"currency": "THB",
"canceled_by": "merchant",
"cancel_reason_code": "merchant_request",
"canceled_at": "2026-08-21T04:25:30.118Z",
"late_slip_accepted_until": "2026-08-22T04:30:47.902Z",
"created_at": "2026-08-21T04:10:47.902Z",
"metadata": { "campaign": "aug-cashback", "order_id": "ORD-9981" }
}| field | ค่าที่เป็นไปได้ | ความหมาย |
|---|---|---|
canceled_by | merchant · end_user · system | ใครเป็นคนปิด |
cancel_reason_code | merchant_request · end_user_request · no_liquidity_available · approval_ttl_expired | 🔴 enum เปิด — switch ต้องมี default: |
late_slip_accepted_until | timestamp หรือ null | ถึงเมื่อไหร่ที่ยังรับสลิปย้อนหลังได้ · null = ปิดสนิทแล้ว |
is_terminal | false หรือ true | 🔴 ตัวนี้เท่านั้นที่บอกว่าปิดออร์เดอร์ได้หรือยัง |
canceled ไม่ใช่จุดจบเสมอไป
เคสที่ทำให้ผู้ใช้เสียเงินจริง
ผู้ใช้กดยกเลิกบนหน้าเว็บ ในขณะที่นิ้วอีกข้างกดยืนยันโอนในแอปธนาคารพอดี — เกิดสัปดาห์ละหลายครั้งในระบบที่มี volume จริง
ถ้าคุณปิดออร์เดอร์ทันทีที่เห็นคำว่า canceled ผู้ใช้คนนั้นจะไม่มีที่ให้อัปสลิป และเงินที่โอนไปแล้วจะกลายเป็นเรื่องที่ต้องตามคืนด้วยมือ
⇒ รายการที่ถูกยกเลิกโดยที่เคยมีปลายทางให้โอน จะมี late_slip_accepted_until และ is_terminal: false ต้องเหลือปุ่มอัปสลิปไว้จนถึงเวลานั้น
// ❌ ผิด
if (d.status === 'canceled') closeOrderAndHideUploadButton()
// ✅ ถูก
if (d.is_terminal) {
closeOrder()
} else if (d.late_slip_accepted_until) {
showUploadButton({ until: d.late_slip_accepted_until })
}สลิปที่มาหลังยกเลิก จะเข้าคิวเจ้าหน้าที่เสมอ
อัปสลิปเข้ารายการที่ canceled ภายในหน้าต่างจะได้ 202 ตามปกติ แต่ผลลัพธ์ถูกบังคับให้เป็น under_review (CANCELED_ORDER_SLIP) เสมอ ไม่มีทางผ่านอัตโนมัติ — เพราะเป็นเคสที่ต้องมีคนดูว่าเงินก้อนนั้นเป็นของรายการนี้จริงไหม
แต่ก็จะ ไม่ถูกปฏิเสธอัตโนมัติเช่นกัน
เมื่อหน้าต่างปิดจริง
เมื่อถึง late_slip_accepted_until เราจะยิง deposit.canceled ซ้ำอีกครั้ง ด้วย is_terminal: true และ version ใหม่ · นั่นคือจุดที่ปิดออร์เดอร์และซ่อนปุ่มอัปสลิปได้
หลังจากนั้นการอัปสลิปจะได้ 410 late_slip_window_closed พร้อม details.claim_url
ยกเลิกโดยระบบ — ไม่มีสภาพคล่อง
รายการที่ค้าง awaiting_liquidity จนหมดเวลารอโดยไม่เจอปลายทางเลย จะถูกปิดเป็น canceled ไม่ใช่ expired
{
"id": "dep_01K3M8B2VQ7HYD4NKR0XF9TW6E",
"object": "deposit_order",
"version": 3,
"status": "canceled",
"is_terminal": true,
"merchant_reference": "ORD-9983",
"amount_minor": 500000,
"currency": "THB",
"canceled_by": "system",
"cancel_reason_code": "no_liquidity_available",
"canceled_at": "2026-08-21T04:12:15.660Z",
"late_slip_accepted_until": null,
"created_at": "2026-08-21T04:10:47.902Z",
"metadata": { "order_id": "ORD-9983" }
}ทำไมไม่ใช่ expired
expired เปิดหน้าต่างอัปสลิปย้อนหลัง 24 ชั่วโมง และ UI จะขึ้นปุ่ม "ฉันโอนไปแล้ว" · แต่รายการนี้ไม่เคยมีปลายทางให้โอนเลย ผู้ใช้ไม่เคยเห็น QR ⇒ ปุ่มนั้นจะสร้างได้แค่ ticket เปล่าให้ support
สังเกตว่าเส้นนี้ late_slip_accepted_until เป็น null และ is_terminal เป็น true ตั้งแต่แรก
ข้อความที่ควรขึ้นให้ผู้ใช้: ขณะนี้ช่องทางโอนไม่ว่าง กรุณาลองใหม่อีกครั้ง พร้อมปุ่มสร้างรายการใหม่ · 🔴 อย่าขึ้นว่า "รายการถูกยกเลิก" ลอย ๆ เพราะผู้ใช้จะคิดว่าตัวเองทำอะไรผิด
Error
| HTTP | code | เกิดเมื่อ | ต้องทำอะไร |
|---|---|---|---|
409 | deposit_not_cancelable | มีสลิปเข้ามาแล้ว หรือรายการติดเงื่อนไขปลายทางที่ยกเลิกไม่ได้ชั่วคราว | ปล่อยให้เดินต่อ อย่า retry เป็นลูป · อ่านสถานะล่าสุดด้วย GET แล้วอัปเดตหน้าจอ |
404 | resource_not_found | id ไม่มี หรือไม่ใช่ของ merchant คุณ | ตรวจ id |
409 | idempotency_in_flight | ผู้ใช้กดยกเลิกรัว ๆ | รอ 1 วินาทีแล้วยิงซ้ำด้วย key เดิม |
// Node — ยกเลิกแบบที่รับมือ 409 ได้
async function cancelDeposit(depositId, reason) {
try {
return await post(`/v1/pay/deposits/${depositId}/cancel`, { reason })
} catch (err) {
if (err.code === 'deposit_not_cancelable') {
// สายไปแล้ว — ผู้ใช้โอนไปแล้ว ให้แสดงสถานะจริงแทนการขึ้น error
const d = await getDeposit(depositId)
return { canceled: false, deposit: d }
}
throw err
}
}หน้าจอที่ควรขึ้นเมื่อได้ 409 deposit_not_cancelable
ยกเลิกไม่ได้แล้ว เพราะเราได้รับสลิปของคุณแล้ว
กำลังตรวจสอบ เงินของคุณไม่หายเคสที่ใช้ cancel เป็นขั้นแรก
การ "แก้ไข" รายการที่สร้างแล้วทำไม่ได้เลย ทุกการเปลี่ยนใจคือ ยกเลิกใบเดิม แล้วสร้างใบใหม่
| ผู้ใช้อยากทำอะไร | ขั้นตอน |
|---|---|
| เปลี่ยนบัญชีผู้โอน | cancel → POST /v1/pay/deposits ด้วย sender ใหม่ + Idempotency-Key ใหม่ |
| เปลี่ยนยอด (เช่นเลือกยอดที่แนะนำตอนรอสภาพคล่อง) | cancel → สร้างใหม่ด้วยยอดใหม่ + merchant_reference ใหม่ + key ใหม่ |
| ปิดรายการค้างเพื่อสร้างใบใหม่ | cancel ใบที่ค้างก่อน ไม่งั้นชนเพดานแล้วได้ 409 too_many_open_deposits |
body เปลี่ยน = Idempotency-Key ต้องเปลี่ยน
ใช้ key เดิมกับ body ใหม่จะได้ 409 idempotency_key_reused · และการ gen key ใหม่ทุกครั้งที่ retry ก็ผิดคนละทาง — จะได้รายการซ้ำและ QR สองใบ
กติกา: หนึ่งเจตนา = หนึ่ง key เขียนลง DB ก่อนยิง แล้ว retry อ่านค่าเดิมจาก DB
ยกเลิกแล้วสร้างใหม่ทันทีอาจติดคิว
รายการบางเส้นทางมีช่วงพักสั้น ๆ (~3 นาที) หลังยกเลิก ก่อนที่ยอดเดิมจากบัญชีผู้โอนเดิมจะจับคู่ได้อีกครั้ง · ถ้าคุณทำปุ่ม "เปลี่ยนบัญชีผู้โอน" หรือ "เปลี่ยนยอด" ให้ track เวลาที่ยกเลิกไว้ฝั่งคุณ แล้วบอกผู้ใช้ตรง ๆ ว่ารายการใหม่อาจใช้เวลาจัดหาช่องทางนานกว่าปกติ แทนที่จะปล่อยให้เขางงกับหน้ารอ
ต้องยิง cancel เองไหม ถ้าผู้ใช้แค่ปิดเบราว์เซอร์
ไม่ต้อง — รายการจะหมดอายุเองที่ expires_at แล้วกลายเป็น expired
ให้ยิง cancel เฉพาะเมื่อ ผู้ใช้ตั้งใจจะไม่โอน เช่น กดปุ่มยกเลิก หรือเลือกเปลี่ยนบัญชี/เปลี่ยนยอด · การยิง cancel กับรายการที่ผู้ใช้แค่หายไปเฉย ๆ ไม่ได้ช่วยอะไร และทำให้แยกไม่ออกว่าเคสไหนคือเจตนาจริง
ขั้นต่อไป
- รายการที่ยังค้างอยู่ — หาว่าใบไหนควรถูกยกเลิกก่อนสร้างใบใหม่
- ตัวเลือกของผู้ใช้ — ทุกการ "เปลี่ยนใจ" ที่เริ่มด้วย cancel
- อัปโหลดสลิป — สลิปย้อนหลังบนรายการที่ยกเลิกไปแล้ว
- สถานะรายการฝาก —
is_terminalกับชื่อสถานะ ต่างกันตรงไหน - บัญชีผู้โอน — ปุ่ม "เปลี่ยนบัญชีผู้โอน" ที่ควรมีบนหน้า QR
