// Developers
เชื่อมต่อระบบชำระเงินของ Dev365TH เรียนออนไลน์
ให้เกมหรือเว็บเซอร์วิสของคุณขายไอเทม โมดูล หรือฟีเจอร์ได้ทันที โดยใช้เครดิตของ Dev365TH เรียนออนไลน์ เป็นสกุลเงิน — คุณไม่ต้องทำระบบเติมเงิน ตัดบัตร ใบเสร็จ หรือบัญชีเงินเอง
1 เริ่มต้นใน 5 นาที
- ขอ โทเคน และลงทะเบียน ผลิตภัณฑ์ + สินค้า (SKU) จากผู้ดูแลเว็บ (ดู หัวข้อ 3)
- เก็บโทเคนไว้ใน environment variable ของเซิร์ฟเวอร์คุณ
DEV365TH_BILLING_TOKEN - ลองรันคำสั่งด้านล่างจากเครื่องเซิร์ฟเวอร์ของคุณ
# 1) ดูสินค้าที่คุณลงทะเบียนไว้ (ยืนยันว่าโทเคนใช้ได้)
curl -s -H "Authorization: Bearer $TOKEN" \
https://dev365th.com/api/v1/billing/skus
# 2) เช็คยอดเครดิตของผู้ใช้ ก่อนแสดงปุ่มซื้อ
curl -s -H "Authorization: Bearer $TOKEN" \
"https://dev365th.com/api/v1/billing/balance?memberId=$MEMBER_ID"
# 3) ตัดเครดิต (Idempotency-Key กันหักซ้ำเวลาเน็ตหลุด)
curl -s -X POST https://dev365th.com/api/v1/billing/charge \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001" \
-d '{"memberId":"'$MEMBER_ID'","sku":"sword_legendary","qty":1}'
# 4) ตรวจสิทธิ์ทุกครั้งที่ผู้ใช้จะใช้ของ (ห้ามเชื่อฝั่งเบราว์เซอร์)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://dev365th.com/api/v1/billing/entitlement?memberId=$MEMBER_ID&sku=sword_legendary"$MEMBER_ID คือรหัสสมาชิกของ Dev365TH เรียนออนไลน์ — ผลิตภัณฑ์ของคุณจะได้รับค่านี้ตอนผู้ใช้เข้าใช้งานผ่านเว็บ (ไม่ใช่ id ในระบบคุณเอง)
2 แนวคิดที่ต้องเข้าใจ
- เครดิต
- สกุลเงินกลางของเว็บ เป็นจำนวนเต็มเสมอ ผู้ใช้เติมด้วยเงินจริงที่ /credits แล้วใช้ซื้อของได้ทุกผลิตภัณฑ์
- ผลิตภัณฑ์ (merchant)
- ตัวเกม/เซอร์วิสของคุณ · โทเคน 1 ตัวผูกกับ 1 ผลิตภัณฑ์ จึงขายในนามผลิตภัณฑ์อื่นไม่ได้
- สินค้า (SKU)
- ของที่ขาย เช่น
sword_legendary· ตั้งราคาและโมเดลการขายไว้ล่วงหน้า คุณส่งแค่ชื่อ SKU ไม่ต้องส่งราคา (กันฝั่งผู้เรียกแก้ราคาเอง) - สิทธิ์ (entitlement)
- สิ่งที่ผู้ใช้ได้รับหลังจ่าย — ระบบบันทึกให้ คุณถามผ่าน API ทุกครั้งก่อนปล่อยของ
- Idempotency-Key
- รหัสที่คุณสร้างต่อ 1 เจตนาซื้อ · ยิงซ้ำด้วยคีย์เดิมจะไม่หักเงินซ้ำ แต่ได้ผลลัพธ์เดิมกลับมาพร้อม
replayed: true
3 ขอโทเคน
ติดต่อผู้ดูแลเว็บเพื่อขอ พร้อมแจ้ง:
- ชื่อผลิตภัณฑ์ และรหัสสั้นที่ต้องการ (เช่น
dungeon-game) - ประเภท — เกม หรือเว็บเซอร์วิส
- รายการสินค้า ที่จะขาย: ชื่อ SKU, ชื่อที่แสดง, ราคาเครดิต, โมเดลการขาย (ดูหัวข้อ 4)
- บัญชีสมาชิก ของคุณบนเว็บ (สำหรับรับส่วนแบ่งรายได้)
โทเคนใช้ตัดเงินได้ — เก็บไว้ฝั่งเซิร์ฟเวอร์เท่านั้น ห้าม commit ลง git ห้ามฝังในแอปมือถือหรือ JavaScript ที่เบราว์เซอร์เห็น ถ้าหลุดให้แจ้งเพิกถอนทันที
4 โมเดลการขาย 4 แบบ
| ชนิด | เหมาะกับ | พฤติกรรม | ตัวอย่าง |
|---|---|---|---|
once | ซื้อครั้งเดียว | ไอเทม สกิน ปลดล็อกด่าน — ซื้อซ้ำได้ จำนวนสะสมขึ้น | ดาบ, ตัวละคร, ปลดล็อกบท |
timed | มีอายุ | สิทธิ์หมดอายุตามชั่วโมงที่ตั้ง · ซื้อซ้ำก่อนหมด = ต่อเวลาจากเดิม ไม่ริบ | บัฟ 24 ชม., ตั๋วเข้าเล่น 7 วัน |
period | สมาชิกรายรอบ | เหมือน timed แต่สื่อความเป็นแพ็กเกจรายเดือน/รายปี | Battle Pass, VIP รายเดือน |
metered | ตามการใช้งาน | คิดตามจำนวนหน่วยที่ใช้ · รายงานผ่าน POST /usage | คำใบ้ AI, เรนเดอร์, ข้อความ |
5 ตัวช่วยเรียก API (คัดลอกไปใช้ได้เลย)
ไฟล์เดียวครอบทุก endpoint พร้อมจัดการกรณีเว็บตอบ HTML แทน JSON:
// billing.js — เรียกจาก "เซิร์ฟเวอร์" ของคุณเท่านั้น ห้ามให้เบราว์เซอร์เห็นโทเคน
import { randomUUID } from 'node:crypto';
const BASE = 'https://dev365th.com/api/v1/billing';
const TOKEN = process.env.DEV365TH_BILLING_TOKEN;
async function call(path, { method = 'GET', body, idemKey } = {}) {
const headers = { Authorization: `Bearer ${TOKEN}` };
if (body) headers['Content-Type'] = 'application/json';
if (idemKey) headers['Idempotency-Key'] = idemKey;
const res = await fetch(BASE + path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
// เว็บอาจตอบ HTML ได้ (เช่น 503 ตอนปิดปรับปรุง) → กัน JSON.parse พังทั้งระบบ
const text = await res.text();
let data = null;
try { data = JSON.parse(text); } catch { /* ไม่ใช่ JSON */ }
if (!res.ok) {
const err = new Error(data?.error?.message || text.slice(0, 200));
err.code = data?.error?.code || `http_${res.status}`;
err.status = res.status;
throw err;
}
return data;
}
export const listSkus = () => call('/skus');
export const getBalance = (memberId) => call(`/balance?memberId=${memberId}`);
export const getEntitlement = (memberId, sku) => call(`/entitlement?memberId=${memberId}&sku=${sku}`);
export const refund = (chargeId) => call('/refund', { method: 'POST', body: { chargeId } });
export const charge = (memberId, sku, qty = 1, idemKey = randomUUID()) =>
call('/charge', { method: 'POST', body: { memberId, sku, qty }, idemKey });
export const reportUsage = (memberId, sku, qty, idemKey) =>
call('/usage', { method: 'POST', body: { memberId, sku, qty }, idemKey });6 ตัวอย่างจริง: ผู้เล่นซื้อไอเทม
// ตัวอย่างจริง: ผู้เล่นกดซื้อดาบในเกม
import { charge, getEntitlement } from './billing.js';
app.post('/shop/buy', async (req, res) => {
const { memberId } = req.session; // ผู้ใช้ที่ล็อกอินในเกมของคุณ
const { sku, orderId } = req.body;
try {
// ★ orderId ของคุณเป็น Idempotency-Key ที่ดีที่สุด — ผู้เล่นกดปุ่มรัว
// หรือเน็ตหลุดแล้ว retry ก็จะหักเงินครั้งเดียว
const r = await charge(memberId, sku, 1, `order-${orderId}`);
if (r.replayed) {
// คำขอนี้เคยสำเร็จแล้ว (ไม่ได้หักเงินใหม่) — ปล่อยของได้เลยถ้ายังไม่ปล่อย
console.log('คำขอซ้ำ ใช้ผลเดิม', r.chargeId);
}
await giveItemToPlayer(memberId, sku); // ระบบของคุณ
return res.json({ ok: true, balance: r.balance });
} catch (e) {
if (e.status === 402) {
// เครดิตไม่พอ — พาไปเติมที่เว็บ แล้วกลับมาซื้อต่อ
return res.status(402).json({
error: 'เครดิตไม่พอ',
topupUrl: 'https://dev365th.com/credits',
});
}
if (e.code === 'sku_inactive') return res.status(409).json({ error: 'สินค้านี้ปิดขายชั่วคราว' });
throw e;
}
});
// ★ ตรวจสิทธิ์ "ทุกครั้ง" ที่ผู้เล่นจะใช้ของ ไม่ใช่เชื่อ state ในเบราว์เซอร์
app.get('/play/use-sword', async (req, res) => {
const ent = await getEntitlement(req.session.memberId, 'sword_legendary');
if (!ent.has) {
return res.status(403).json({
error: ent.expired ? 'สิทธิ์หมดอายุแล้ว' : 'ยังไม่ได้ซื้อไอเทมนี้',
});
}
// ปล่อยให้ใช้ได้
});7 รายการ API
ทุก endpoint อยู่ใต้ https://dev365th.com/api/v1/billing และต้องส่ง header Authorization: Bearer <โทเคน>
| Method | Path | ใช้ทำอะไร | พารามิเตอร์ |
|---|---|---|---|
| POST | /charge | ตัดเครดิต + มอบสิทธิ์ | memberId, sku, qty?, idemKey?, note? |
| GET | /entitlement | ถามสิทธิ์ (ไม่ส่ง sku = ดูทั้งหมด) | ?memberId=&sku= |
| GET | /balance | ยอดเครดิตของผู้ใช้ | ?memberId= |
| GET | /skus | แคตตาล็อก + ราคาของคุณ | ?all=1 รวมที่ปิดขาย |
| POST | /usage | รายงานการใช้งาน (สินค้าแบบ metered) | memberId, sku, qty |
| POST | /refund | คืนเครดิต + ถอนสิทธิ์ | chargeId |
8 จัดการข้อผิดพลาด
ทุก error ตอบรูปแบบเดียวกัน — อ่าน error.code ไม่ใช่ข้อความ (ข้อความอาจเปลี่ยน):
{ "error": { "code": "insufficient", "message": "เครดิตไม่พอ …" } }| code | HTTP | เกิดเมื่อ | ทำอย่างไร |
|---|---|---|---|
unauthorized | 401 | ไม่ได้ส่ง header Authorization | ส่ง `Authorization: Bearer <โทเคน>` |
invalid_token | 401 | โทเคนผิด/ถูกเพิกถอน | ขอโทเคนใหม่จากผู้ดูแล |
token_expired | 403 | โทเคนหมดอายุ | ขอต่ออายุ |
insufficient_scope | 403 | โทเคนไม่มีสิทธิ์ billing | ขอโทเคน scope `billing` |
merchant_suspended | 403 | ผลิตภัณฑ์ถูกระงับ | ติดต่อผู้ดูแลเว็บ |
insufficient | 402 | ผู้ใช้เครดิตไม่พอ | พาผู้ใช้ไปเติมที่ /credits แล้วลองใหม่ |
sku_not_found | 404 | ไม่มี SKU นี้ในผลิตภัณฑ์ของคุณ | ตรวจชื่อ SKU ด้วย GET /skus |
member_not_found | 404 | ไม่มีสมาชิกรหัสนี้ | ตรวจว่าใช้ memberId ของ dev365th ไม่ใช่ id ในระบบคุณ |
charge_not_found | 404 | ไม่พบรายการ (หรือไม่ใช่ของผลิตภัณฑ์คุณ) | ตรวจ chargeId |
sku_inactive | 409 | สินค้าถูกปิดขายชั่วคราว | ซ่อนปุ่มซื้อ หรือแจ้งผู้ใช้ |
already_refunded | 409 | คืนเงินรายการนี้ไปแล้ว | ถือว่าสำเร็จ ไม่ต้องคืนซ้ำ |
invalid | 400 | ข้อมูลที่ส่งไม่ถูกรูปแบบ | ตรวจ qty (1–1000) และชนิดข้อมูล |
rate_limited | 429 | เรียกถี่เกินโควตา | อ่าน header `Retry-After` แล้วรอ |
9 กฎที่ต้องทำ
- ส่ง
Idempotency-Keyทุกครั้งที่ตัดเงิน (ใช้ order id ของคุณ) - ตรวจสิทธิ์ผ่าน API ทุกครั้งที่ผู้ใช้จะใช้ของ
- เก็บ
chargeIdไว้อ้างอิงตอนคืนเงิน - จัดการ
402ด้วยการพาผู้ใช้ไปเติมเครดิต - เรียก API จากเซิร์ฟเวอร์ของคุณเท่านั้น
- เชื่อสถานะสิทธิ์ที่ส่งมาจากเบราว์เซอร์/แอปผู้ใช้
- ส่งราคาเองในคำขอ (ราคามาจาก SKU ที่ลงทะเบียนไว้)
- ฝังโทเคนในโค้ดฝั่งผู้ใช้ หรือ commit ลง git
- สร้าง Idempotency-Key ใหม่ทุกครั้งที่ retry คำขอเดิม
- ถือว่า response เป็น JSON เสมอโดยไม่ try/catch
10 เช็กลิสต์ก่อนเปิดใช้จริง
- โทเคนอยู่ใน env ของเซิร์ฟเวอร์ ไม่อยู่ใน git และไม่หลุดถึงเบราว์เซอร์
- ทุกจุดที่ตัดเงินส่ง Idempotency-Key ที่ผูกกับคำสั่งซื้อจริง
- ทดลองยิงคำขอเดิมซ้ำ 2 ครั้ง → เงินถูกหักครั้งเดียว
- ทดลองกับผู้ใช้ที่เครดิตไม่พอ → ได้ 402 และ UI พาไปเติมเงินถูกทาง
- ทุกจุดที่ปล่อยของเรียกตรวจสิทธิ์ก่อน ไม่มีทางลัด
- รองรับ
429ด้วยการรอตามRetry-After - บันทึก
chargeIdทุกรายการลงฐานข้อมูลของคุณ - ทดลองคืนเงิน 1 รายการ → สิทธิ์ถูกถอนและเครดิตกลับถึงผู้ใช้
11 คำถามที่พบบ่อย
ผู้ใช้เครดิตไม่พอ ต้องทำอย่างไร
API ตอบ 402 พร้อม code insufficient — พาผู้ใช้ไปหน้า /credits เพื่อเติมเครดิต แล้วให้กดซื้อใหม่ (ใช้ Idempotency-Key ตัวเดิมได้ ปลอดภัย)
ยิงคำขอไปแล้วเน็ตหลุด ไม่รู้ว่าตัดเงินสำเร็จไหม
ยิงซ้ำด้วย Idempotency-Key เดิม ถ้าครั้งแรกสำเร็จไปแล้ว จะได้ผลลัพธ์เดิมกลับมาพร้อม replayed: true โดยไม่หักเงินซ้ำ ถ้าครั้งแรกไม่สำเร็จ ระบบจะดำเนินการให้ปกติ
ผมได้ส่วนแบ่งรายได้เท่าไร และได้เมื่อไร
ตามอัตราที่ตกลงกับผู้ดูแลเว็บ (ตั้งได้ทั้งค่ากลางและรายผลิตภัณฑ์) ส่วนแบ่งจะโอนเข้าบัญชีเครดิตของคุณทันทีในทรานแซกชันเดียวกับการขาย — ตรวจได้ที่ประวัติเครดิตในบัญชีของคุณ
เปลี่ยนราคาสินค้าได้ไหม
ได้ — แจ้งผู้ดูแลเว็บแก้ที่ SKU ราคาใหม่มีผลทันทีกับการซื้อครั้งถัดไป · ให้ผลิตภัณฑ์ของคุณดึงราคาจาก GET /skus แทนการ hardcode เพื่อให้แสดงราคาตรงกับที่ตัดจริงเสมอ
ทดสอบได้ไหม ไม่อยากใช้เงินจริง
ยังไม่มีสภาพแวดล้อม sandbox แยก — วิธีที่ใช้ตอนนี้คือขอผลิตภัณฑ์ทดสอบแยกหนึ่งตัวพร้อม SKU ราคาต่ำ (เช่น 1 เครดิต) และให้ผู้ดูแลเติมเครดิตทดสอบให้บัญชีที่ใช้ทดสอบ
สิทธิ์แบบมีอายุ ถ้าซื้อซ้ำก่อนหมดอายุจะเสียเวลาที่เหลือไหม
ไม่เสีย — ระบบต่อเวลาจากวันหมดอายุเดิม (เช่นเหลือ 5 ชม. ซื้อเพิ่ม 24 ชม. → เหลือ 29 ชม.)
- ถอนส่วนแบ่งเป็นเงินจริง — ปัจจุบันได้เป็นเครดิตในเว็บ
- ตัดบัตรเครดิตตรงจากผลิตภัณฑ์ — ผู้ใช้ต้องเติมเครดิตที่เว็บก่อน
- webhook แจ้งผลกลับ — ยังไม่ส่ง callback ให้ผลิตภัณฑ์ (ให้เชื่อผลจาก response ของ API)
- sandbox แยก — ใช้ผลิตภัณฑ์ทดสอบแทน
มีคำถามหรือต้องการขอโทเคน — ติดต่อเรา