รับเงินด้วย Stripe ทั้งบัตรและ PromptPay: ระบบจ่ายเงินจริงของ Boom Field Lab ตั้งแต่กดซื้อจนได้สิทธิ์
เบื้องหลังระบบรับเงินของเว็บ workshop นี้แบบ build in public — ทำไมเคยจงใจปิด PromptPay ใน Stripe แล้วกลับมาเปิด, ทำไมราคาต้องคิดฝั่ง server และล็อกไว้ 60 นาที, webhook ตัวเดียวเปิดสิทธิ์ยังไง และค่าธรรมเนียมจริงต่อการขาย 1 ครั้ง
โดย วรัญชัย ยิ่งคำนึง (Boom) · เผยแพร่ครั้งแรก · ตรวจทานล่าสุด
ระบบรับเงินเป็นส่วนที่คนทำ product เล็กๆ มักทำทีหลังสุด แล้วมาพังตอนมีลูกค้าจริง บทความนี้เปิดเบื้องหลังของ Boom Field Lab — เว็บ workshop ที่คุณกำลังอ่านอยู่ — ว่าเงินเดินทางยังไงตั้งแต่กดซื้อจนได้สิทธิ์ดูวิดีโอ พร้อมเหตุผลของแต่ละการตัดสินใจแบบคนทำงาน risk
ทั้งระบบนี้เขียนด้วย AI agent เป็นหลัก (เหตุผลที่เลือกเครื่องมืออยู่ใน ทำไมผมสร้าง Terminal ด้วย Claude Code) แต่ทุกกฎในบทความนี้เป็นการตัดสินใจของคน — agent แค่เขียนตามกฎ
ทำไมเคยจงใจปิด PromptPay ใน Stripe แล้วตอนนี้กลับมาเปิด?
เวอร์ชันแรก (มิถุนายน 2026) ผมรับเงิน 2 ทาง: PromptPay แบบโอนแล้วแนบสลิป (ค่าธรรมเนียม 0 บาท) กับ บัตรผ่าน Stripe และจงใจปิดปุ่ม PromptPay ใน Stripe ทิ้ง เพราะไม่อยากจ่ายค่าธรรมเนียมให้เงินที่รับฟรีได้อยู่แล้ว
ฟังดูประหยัด แต่ต้นทุนจริงของ "ฟรี" ซ่อนอยู่ที่อื่น:
| PromptPay แนบสลิป (เดิม) | PromptPay ผ่าน Stripe (ปัจจุบัน) | |
|---|---|---|
| ค่าธรรมเนียม | 0 บาท | 1.65% ≈ ฿5.76 ต่อ ฿349 |
| ใครยืนยันว่าเงินเข้า | คน (เปิดดูสลิปทีละใบ) | Stripe ยืนยันเอง |
| ลูกค้าได้สิทธิ์เมื่อไหร่ | เมื่อคนว่างมาอนุมัติ | ทันทีที่เงินเข้า |
| ที่เก็บสลิป + หน้าอนุมัติ | ต้องมี ต้องดูแล | ลบทิ้งทั้งหมด |
| ความเสี่ยงสลิปปลอม | มี | ไม่มี (ไม่ได้ดูสลิป) |
พอตัดสินใจทำเว็บ workshop ราคาเดียว ฿349 ที่ขาย 24 ชั่วโมง การมีคนเป็นคอขวดในเส้นทางเงินกลายเป็นจุดอ่อนที่แพงที่สุด ผมเลยยอมจ่าย ~฿5.76 ต่อรายการเพื่อซื้อ 3 อย่าง: ลูกค้าได้ของทันที, ไม่มีงานมือ, และโค้ดสั้นลงเพราะลบระบบสลิปทั้งชุดทิ้ง หลักคิดเดิมยังเหมือนเดิม — จ่ายค่าธรรมเนียมเฉพาะตอนที่มันซื้ออะไรกลับมา — แค่ของที่ได้กลับมาเปลี่ยนไป
เงินเดินทางยังไง ตั้งแต่กดซื้อจนได้สิทธิ์?

- ตะกร้า — ผู้ซื้อกดซื้อ ฟอร์มส่งไปแค่รายชื่อ slug ของ workshop ไม่มีราคาติดไปด้วย
- Server คิดราคา — ตัด slug ที่ไม่มีจริงหรือยังเป็น draft ทิ้ง, ตัดตัวที่ผู้ซื้อ มีสิทธิ์อยู่แล้ว ทิ้ง (ไม่ให้จ่ายซ้ำ), แล้วคำนวณราคาจากกฎกลาง: ฿349 ปกติ หรือ ฿249 ถ้ายังอยู่ใน 7 วันแรกหลังเปิดตัว
- Stripe Checkout — สร้างหน้าชำระเงินของ Stripe (เราไม่เก็บข้อมูลบัตรเองเลย) เปิดบัตร + PromptPay, บังคับติ๊กยอมรับข้อกำหนดและนโยบายคืนเงิน, หมดอายุใน 60 นาที
- Webhook — Stripe ยิงเหตุการณ์กลับมา เราตรวจลายเซ็นก่อนเชื่อทุกครั้ง
- เปิดสิทธิ์ — บันทึกสิทธิ์ + คำสั่งซื้อ แล้วส่งอีเมลยืนยัน ครั้งเดียวเท่านั้นไม่ว่าจะถูกเรียกกี่รอบ
ทำไมราคาต้องคิดฝั่ง server และล็อกไว้ 60 นาที?
เพราะอะไรก็ตามที่มาจากเบราว์เซอร์ แก้ได้ ถ้าฟอร์มส่งราคามาด้วย ใครก็เปิด DevTools เปลี่ยน 349 เป็น 1 ได้ ระบบนี้เลยรับจากผู้ซื้อแค่ "อยากได้ตัวไหน" ส่วน "ต้องจ่ายเท่าไหร่" คำนวณที่ server ทุกครั้ง หน้าตาโค้ดโดยย่อ:
// ฟอร์มส่งมาแค่ slug — ราคาคำนวณใหม่ที่ server ทุกครั้ง
const { lines } = priceCart(items, activeCount);
form.set("payment_method_types[0]", "card");
form.set("payment_method_types[1]", "promptpay");
// ล็อกราคาไว้ใน session นี้ 60 นาที
form.set("expires_at", String(nowSec + 60 * 60));
ทำไม 60 นาที? ราคา early-bird ฿249 มีวันหมดอายุ ถ้าผู้ซื้อเปิดหน้าชำระเงินตอน 23:50 ของวันสุดท้ายแล้วจ่ายตอน 00:10 เขาควรได้ราคาที่เห็นตอนกดซื้อ Stripe กำหนดอายุ session ขั้นต่ำไว้ 30 นาที ผมเลือก 60 นาทีเพื่อให้พอสำหรับคนที่ต้องสลับไปเปิดแอปธนาคาร และจำกัด "ความเสียหายสูงสุด" ไว้ชัดๆ: early-bird ยืดได้เกินกำหนดอย่างมาก 1 ชั่วโมง — ความเสี่ยงที่รู้ขนาดและยอมรับได้
PromptPay ต่างจากบัตรตรงไหนในสายตาของ webhook?
นี่คือกับดักที่ทำให้หลายระบบเปิดสิทธิ์ผิด บัตรจ่ายเสร็จในหน้าเดียว แต่ PromptPay ผู้ซื้อต้องไปสแกน QR ในแอปธนาคาร เงินจึง มาทีหลัง

| วิธีจ่าย | เหตุการณ์แรก | สถานะที่เราตีความ | เปิดสิทธิ์เมื่อ |
|---|---|---|---|
| บัตร | checkout.session.completed (paid) | paid | ทันที |
| PromptPay | checkout.session.completed (unpaid) | pending — ห้ามเปิดสิทธิ์ | checkout.session.async_payment_succeeded |
| PromptPay ไม่สำเร็จ | checkout.session.async_payment_failed | failed | ไม่เปิด |
คอมเมนต์ในโค้ดจริงเขียนกฎนี้ไว้ตรงบรรทัดที่เปิด PromptPay:

PromptPay: async — the buyer scans a QR, so the sale is confirmed later via the
checkout.session.async_payment_succeededwebhook, not on redirect back.
ถ้าเขียนแบบมักง่ายว่า "เห็น completed = เปิดสิทธิ์" ผู้ซื้อ PromptPay ทุกคนจะได้ของ ก่อน จ่ายเงิน และถ้าเขาไม่สแกน QR เลย เราก็แจกฟรี
ค่าธรรมเนียมจริงต่อการขาย 1 ครั้งเท่าไหร่?

ตามหน้าราคามาตรฐานของ Stripe ประเทศไทย (ตรวจเมื่อ 27 ก.ย. 2026): บัตรในประเทศ 3.65% + ฿10 · บัตรต่างประเทศ 4.75% + ฿10 (+2% ถ้าต้องแปลงสกุลเงิน) · PromptPay 1.65% (+฿10 ต่อการคืนเงิน) · ไม่มีค่าแรกเข้าหรือค่ารายเดือน
| ราคาขาย | PromptPay | บัตรในประเทศ | บัตรต่างประเทศ |
|---|---|---|---|
| ฿349 (ปกติ) | ≈ ฿5.76 | ≈ ฿22.74 | ≈ ฿26.58 |
| ฿249 (early-bird) | ≈ ฿4.11 | ≈ ฿19.09 | — |
สองข้อที่ตารางนี้บอก:
- ค่าธรรมเนียมคงที่ ฿10 ของบัตรกินหนักกับของราคาถูก — ที่ ฿249 บัตรในประเทศกินไปราว 7.7% ของยอดขาย ขณะที่ PromptPay ยังอยู่ที่ 1.65% สำหรับตลาดไทยที่คนมีแอปธนาคารแทบทุกคน การเปิด PromptPay จึงไม่ใช่แค่ความสะดวกของลูกค้า แต่ลดต้นทุนเราด้วย
- ลูกค้าจ่ายราคาเดียวกันทุกช่องทาง — ผมไม่บวกค่าธรรมเนียมบัตรเข้าไปในราคา ให้ลูกค้าเลือกตามความสะดวก ส่วนต่างเป็นต้นทุนที่เรารับเอง และเพราะไม่มีค่ารายเดือน เดือนที่ไม่มีคนซื้อเลยต้นทุนก็เป็นศูนย์
Webhook ตัวเดียวต้องกันอะไรบ้าง?
Webhook คือประตูเดียวที่เปิดสิทธิ์ได้ จึงต้องแข็งที่สุดในระบบ:
- ตรวจลายเซ็นทุกครั้ง — คำนวณ HMAC-SHA256 จาก body ดิบที่ Stripe ส่งมา เทียบกับ header ลายเซ็น ไม่ตรง = ตอบ 400 ทิ้ง ใครยิง request ปลอมมาที่ endpoint นี้ก็เปิดสิทธิ์ไม่ได้
- กัน replay — ถ้า timestamp ในลายเซ็นเก่ากว่า 5 นาที ปฏิเสธ แม้ลายเซ็นจะถูก
- กดซ้ำกี่ครั้งก็ได้ผลเดิม (idempotent) — Stripe ส่งเหตุการณ์ซ้ำได้เสมอ สิทธิ์จึงไม่ซ้ำตามคู่ (ผู้ใช้, workshop) และคำสั่งซื้อไม่ซ้ำตาม session อีเมลยืนยันจึงออกครั้งเดียว
- มีทางสำรองที่ใช้กฎเดียวกัน — ถ้าผู้ซื้อกลับมาถึงหน้า "ชำระเงินสำเร็จ" ก่อน webhook มาถึง หน้านั้นเรียกฟังก์ชันเปิดสิทธิ์ ตัวเดียวกัน ด้วยกฎตัดสินว่า "จ่ายแล้ว" ชุดเดียวกัน — ไม่มีเส้นทางที่สองที่เขียนกฎแยกแล้วเพี้ยน
- ตอบ 200 กับเหตุการณ์ที่ไม่สนใจ — ไม่งั้น Stripe จะพยายามส่งซ้ำไปเรื่อยๆ
มีอีกกรณีที่เจอจริงตอนต่อระบบ: บัญชี Stripe ใช้ร่วมกับเว็บอื่นในเครือ webhook ของเว็บอื่นจึงได้รับเหตุการณ์ซื้อ workshop ไปด้วย วิธีแก้คือให้ทุกคำสั่งซื้อของ Field Lab ติดป้ายสินค้าขึ้นต้นด้วย lab: แล้วให้ webhook ของเว็บอื่นเห็นป้ายนี้แล้วตอบ 200 ผ่านไปเฉยๆ ไม่แตะอะไร
ยังมีอะไรที่ยังไม่ได้พิสูจน์?
ตรงไปตรงมา: ในโหมดทดสอบของ Stripe การจ่าย PromptPay settle ทันที ในเหตุการณ์แรก เส้นทาง async_payment_succeeded ของจริงจึงยังไม่เคยถูกทดสอบด้วยเงินจริง แผนคือทดสอบซื้อจริงยอดต่ำก่อนเปิดขาย (ปัจจุบันปุ่มซื้อยังปิดอยู่) บันทึกไว้ตรงนี้เพราะระบบเงินที่ "น่าจะใช้ได้" กับ "พิสูจน์แล้วว่าใช้ได้" ต่างกันมาก
สรุป
ระบบรับเงินที่ดีสำหรับ product เล็กไม่ต้องซับซ้อน แต่ต้องถูกในไม่กี่จุด: ไม่เชื่อราคาจากเบราว์เซอร์ · ล็อกราคาในเวลาที่จำกัด · ให้ webhook ที่ตรวจลายเซ็นเป็นประตูเดียวที่เปิดสิทธิ์ · เข้าใจว่า PromptPay มาทีหลัง และ ทำให้ทุกอย่างกดซ้ำได้โดยไม่พัง ค่าธรรมเนียม ~฿5.76 ต่อรายการของ PromptPay ถูกกว่าค่าคนนั่งอนุมัติสลิปเสมอ