คู่มือ API
การเรียกใช้งานจริงต้องมี API key — สมัครและรออนุมัติก่อน
สมัครใช้งาน APIภาพรวม
REST API สำหรับดึงสินค้า ID Offline สั่งซื้อ และรับข้อมูลบัญชีอัตโนมัติ ทุก endpoint รับ-ส่ง JSON และตัดเงินจากยอดเงินในบัญชี store ของคุณตามเรทที่ได้รับ
https://store.499k-network.com/api/v1เรียกใช้จากฝั่ง server ของคุณเท่านั้น — ห้ามฝัง API key ในหน้าเว็บหรือแอปฝั่งลูกค้าเด็ดขาด
อัปเดตราคาสินค้า (GET /api/v1/products) อย่างน้อยทุก 30 นาที — ราคาเปลี่ยนตามดีล Flash Sale (ราคาดีลยังสั่งซื้อได้ถึง 1 ชั่วโมงหลังดีลจบ ถ้าอัปเดตทุก 30 นาทีจะไม่พลาดราคา)
การยืนยันตัวตน
สร้างคีย์ได้ที่หน้า API Store — ส่งคีย์ผ่าน header ได้ 2 แบบ (เลือกอย่างใดอย่างหนึ่ง)
HTTP Headers
Authorization: Bearer 499k_live_xxxxxxxxxxxxxxxxxxxxxxxx
# หรือ
x-api-key: 499k_live_xxxxxxxxxxxxxxxxxxxxxxxxSandbox (โหมดทดสอบ)
สมัครแล้วสร้างคีย์ทดสอบ (ขึ้นต้น
499k_test_) ใช้ได้ทันทีไม่ต้องรออนุมัติ — endpoint เดียวกันทั้งหมด แต่เห็นเฉพาะสินค้าทดสอบ ไม่ตัดเงินจริง ข้อมูลบัญชีและโค้ด Steam Guard เป็นของจำลอง ทดสอบสั่งซื้อสำเร็จอย่างน้อย 1 ครั้งแล้วจึงส่งคำขอใช้งานจริงได้ที่หน้า API Store| คีย์ทดสอบ (499k_test_) | คีย์จริง (499k_live_) | |
|---|---|---|
| สินค้า | สินค้าทดสอบ — type sandbox product_id 999001 · เช่า type rental 999002 · ชุด type bundle 999003 | สินค้าจริงทั้งหมด |
| การตัดเงิน | ยอดจำลอง ไม่ตัดเงินจริง | ตัดจากยอดเงินในบัญชี |
| เงื่อนไข | ใช้ได้ทันทีหลังสมัคร | ต้องอนุมัติ + ยอดเติมสะสมถึงขั้นต่ำ |
| เลขออเดอร์ | ขึ้นต้น TEST- | ขึ้นต้น API- |
ข้อจำกัดการเรียกใช้ (Rate Limit)
| ประเภท | จำกัด |
|---|---|
| คำขอทั่วไป (ต่อคีย์) | 60 ครั้ง/นาที |
| สั่งซื้อ (ต่อคีย์) | 10 ครั้ง/นาที |
| ระดับ IP | 120 ครั้ง/นาที |
| ขอโค้ด Steam Guard | 3 รอบ/ออเดอร์ (รอบละ 60 วิ) |
เกินจำกัดจะได้ HTTP 429 พร้อม header Retry-After (วินาที) — แนะนำให้รอตามค่านั้นแล้วลองใหม่แบบ exponential backoff และใช้ ref เดิมเมื่อ retry การสั่งซื้อ (ระบบ idempotent ไม่ตัดเงินซ้ำ)
รูปแบบ Response และ Error Codes
Envelope
สำเร็จ: {"success": true, "data": { ... }}
ผิดพลาด: {"success": false, "error": {"code": "OUT_OF_STOCK", "message": "สินค้าหมดชั่วคราว"}}| Code | HTTP | ความหมาย |
|---|---|---|
UNAUTHORIZED | 401 | ไม่พบ API key หรือคีย์ไม่ถูกต้อง |
KEY_REVOKED | 401 | คีย์ถูกเพิกถอนแล้ว (ถูกรีเซ็ตหรือถูกแอดมินปิด) |
CLIENT_NOT_APPROVED | 403 | บัญชี API ยังไม่ได้รับอนุมัติ |
CLIENT_SUSPENDED | 403 | บัญชี API ถูกระงับ |
MIN_TOPUP_REQUIRED | 403 | ยอดเติมเงินสะสมยังไม่ถึงขั้นต่ำ |
FORBIDDEN | 403 | บัญชีผู้ใช้ถูกปิดการใช้งาน |
SYSTEM_DISABLED | 503 | ระบบ API ปิดให้บริการชั่วคราว |
VALIDATION_ERROR | 422 | ข้อมูลที่ส่งมาไม่ถูกต้อง (ดู message) |
PRODUCT_NOT_FOUND | 404 | ไม่พบสินค้า |
PRODUCT_DISABLED | 404 | สินค้านี้ไม่เปิดขายผ่าน API |
OUT_OF_STOCK | 409 | สินค้าหมดชั่วคราว |
BUNDLE_UNAVAILABLE | 409 | ชุดนี้ยังไม่พร้อมขายชั่วคราว (ดู reason) — ยังไม่ได้ตัดเงิน |
BUNDLE_CHANGED | 409 | เกมในชุดไม่ตรงกับ items ที่ส่งมา (ดู items ปัจจุบัน) — ยังไม่ได้ตัดเงิน |
PRICE_CHANGED | 409 | ราคาตอนนี้สูงกว่า max_price ที่ส่งมา (ดู price) — ยังไม่ได้ตัดเงิน |
INSUFFICIENT_BALANCE | 400 | ยอดเงินไม่พอ (ดู required / balance) |
MASTER_KEY_REQUIRED | 403 | ใช้ได้เฉพาะคีย์แม่ (เช่น ส่ง username) |
PRICE_NOT_ALLOWED | 403 | คีย์นี้ส่ง price เองไม่ได้ (เฉพาะคีย์แม่) |
PRICE_TOO_LOW | 422 | คีย์แม่ส่ง price ต่ำกว่าขั้นต่ำ (ดู floor) |
CODE_LIMIT_REACHED | 403 | ขอโค้ดครบ 3 รอบแล้ว |
CODE_UNAVAILABLE | 409 | บัญชีนี้ไม่รองรับการขอโค้ดอัตโนมัติ |
ORDER_REFUNDED | 403 | ออเดอร์ถูกคืนเงินแล้ว ขอโค้ด/ดูข้อมูลบัญชีไม่ได้ |
NOT_FOUND | 404 | ไม่พบออเดอร์ |
RATE_LIMITED | 429 | คำขอถี่เกินไป — ดู header Retry-After |
INTERNAL_ERROR | 500 | ข้อผิดพลาดภายในระบบ |
Endpoints
GET
/api/v1/meข้อมูลบัญชี API: ยอดเงิน ยอดซื้อสะสม เรทปัจจุบัน และสถานะยอดเติมขั้นต่ำ
ตัวอย่าง Response
{
"success": true,
"data": {
"client": { "first_name": "สมชาย", "last_name": "ใจดี", "website_name": "GameShop", "status": "approved" },
"balance": 5000,
"volume": 12500,
"rate_percent": 95,
"current_tier": null,
"next_tier": { "min_volume": 20000, "rate": 93, "remaining": 7500 },
"min_topup": { "required": 2000, "current": 3500, "passed": true },
"key_prefix": "499k_live_a1b2c3"
}
}GET
/api/v1/productsรายการสินค้าทั้งหมดที่เปิดขายผ่าน API — price คือราคาที่คุณจ่ายจริงตามเรทของคุณ (กรองได้: ?type=offline, ?platform=steam|epic|uplay|other) — field "type" คือประเภทสินค้า ปัจจุบันมี offline, rental และ bundle และอาจเพิ่มประเภทใหม่ในอนาคต (ดูรายการได้จาก field types) · ชุดสินค้า (bundle) ไม่รวมใน list ปกติ ต้องขอด้วย
?type=bundleFlash Sale (ต้องเปิดรับก่อน — ค่าเริ่มต้นปิด): เปิดได้ที่หน้า API Store หัวข้อ "รับราคา Flash Sale" — ปิดอยู่ = ได้ราคาปกติเสมอ และ
flash_sale เป็น null ทุกตัว · เปิดแล้ว web_price คือราคาหน้าเว็บ ณ ตอนนี้ — เกมที่กำลัง Flash Sale จะเป็นราคาหลังลดแล้ว และ price ก็คิดจากราคาหลังลด (ตอนสั่งซื้อด้วย) · field flash_sale เป็น null ถ้าไม่ได้ลด หรือบอกป้ายส่วนลด ราคาตอนไม่ลด (normal_web_price / normal_price ไว้ทำราคาขีดฆ่า) และเวลาจบ ends_at — เลยเวลาแล้วหน้าเว็บกลับเป็นราคาปกติ แต่สั่งซื้อผ่าน API ด้วยราคาดีลได้อีก 1 ชั่วโมง (ถึง order_until) กันราคาที่คุณ cache ไว้พลาด · อัปเดตราคาอย่างน้อยทุก 30 นาที เลิกขายราคาดีลเมื่อเลย order_until และส่ง max_price ตอนสั่งซื้อตัวอย่างสินค้าที่กำลัง Flash Sale (ตัดมาเฉพาะ field ราคา)
{
"type": "offline",
"product_id": "934700",
"web_price": 9,
"rate_percent": 80,
"price": 7.2,
"flash_sale": {
"label": "-85%",
"normal_web_price": 60,
"normal_price": 48,
"ends_at": "2026-09-27T20:11:05.000Z",
"order_until": "2026-09-27T21:11:05.000Z"
}
}
// ไม่ได้ลดอยู่: "flash_sale": null
// order_until = ends_at + 1 ชม. — สั่งซื้อราคาดีลผ่าน API ได้ถึงเวลานี้field
steam คือข้อมูลเกมจาก Steam ไว้ทำหน้าสินค้าเอง ปกติได้ชุดเบา (ชื่อ / หมวดหมู่ / แพลตฟอร์ม) ใส่ ?expand=steam จะได้ชุดเต็มเพิ่ม คือคำบรรยาย (ไทย + อังกฤษ) สกรีนช็อต สเปกเครื่อง รายชื่อภาษา ผู้พัฒนา/ผู้จัดจำหน่าย และหมวดหมู่ย่อย (มีชื่อไทยให้ด้วย) — ชุดเต็มของทั้ง list หนักประมาณ 3.3 MB ควร sync เป็นรอบแล้วเก็บลง DB ตัวเอง ไม่ใช่เรียกทุกครั้งที่ลูกค้าเปิดหน้าเว็บตัวอย่าง Response
{
"success": true,
"data": {
"products": [
{
"type": "offline",
"product_id": "1030300",
"platform": "steam",
"name": "Hollow Knight: Silksong",
"image": "https://...",
"stock": 3,
"web_price": 25,
"full_price": 690,
"price": 23.75,
"rate_percent": 95,
"created_at": "2026-08-05T09:12:33.000Z",
"denuvo": false,
"steam": {
"name": "Hollow Knight: Silksong",
"type": "game",
"genres": ["Action", "Adventure", "Indie"],
"genres_th": ["แอ็คชัน", "ผจญภัย", "อินดี้"],
"platforms": { "windows": true, "mac": true, "linux": true },
"has_thai": true,
"thai_audio": false,
"release_date": "2025-09-04",
"metacritic_score": 91,
"header_image": "https://shared.akamai.steamstatic.com/.../header.jpg"
}
}
],
"total": 1,
"types": ["offline", "rental", "bundle"]
}
}ตัวอย่าง field steam ตอนใส่ ?expand=steam
"steam": {
"name": "Hollow Knight: Silksong",
"type": "game",
"genres": ["Action", "Adventure", "Indie"],
"genres_th": ["แอ็คชัน", "ผจญภัย", "อินดี้"],
"platforms": { "windows": true, "mac": true, "linux": true },
"has_thai": true,
"thai_audio": false,
"release_date": "2025-09-04",
"metacritic_score": 91,
"header_image": "https://shared.akamai.steamstatic.com/.../header.jpg",
"categories": ["Single-player", "Steam Achievements", "Full controller support"],
"categories_th": ["ผู้เล่นคนเดียว", "รางวัลความสำเร็จบน Steam", "รองรับคอนโทรลเลอร์เต็มรูปแบบ"],
"languages": ["English", "Thai", "Japanese", "Simplified Chinese"],
"supported_languages": "English<strong>*</strong>, Thai, ...",
"developers": ["Team Cherry"],
"publishers": ["Team Cherry"],
"recommendations": 128470,
"is_free": false,
"required_age": 0,
"coming_soon": false,
"short_description": "คำโปรยสั้นของเกม (ข้อความล้วน ไม่มี HTML)",
"screenshots": [
"https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1030300/ss_....1920x1080.jpg"
],
"pc_requirements": {
"minimum": "<strong>Minimum:</strong><br>...",
"recommended": "<strong>Recommended:</strong><br>..."
},
"description": "<h2>...</h2><p>คำบรรยายภาษาอังกฤษจาก Steam (HTML)</p>",
"description_th": "<section><h2>...</h2>คำบรรยายภาษาไทยที่ทางเราเขียนเอง</section>",
"updated_at": "2026-08-05T09:12:33.000Z"
}
// steam เป็น null ได้ — เกมที่ไม่ใช่ platform steam หรือยังไม่มีข้อมูล ต้องเช็คก่อนใช้เสมอ
// has_thai / thai_audio: true = มีภาษาไทย, false = ไม่มี, null = ยังไม่ได้เก็บข้อมูลเกมนี้
// languages เป็นชื่อภาษาแบบที่ Steam ใช้ (English, Thai, Simplified Chinese, ...)
// description / pc_requirements เป็น HTML ดิบจาก Steam ต้อง sanitize ก่อน render
// description_th คือคำบรรยายไทยที่ทางเราเขียนเอง ถ้า null ให้ fallback ไป descriptionGET
/api/v1/products/{product_id}รายละเอียดสินค้าตัวเดียว + สถานะว่าง (available) — เหมาะสำหรับเช็คสต็อกก่อนสั่งซื้อ (?type= ไม่บังคับ default offline) — field
steam ของ endpoint นี้ให้ชุดเต็มเสมอ ไม่ต้องส่ง expandตัวอย่าง Response
{
"success": true,
"data": {
"type": "offline",
"product_id": "1030300",
"platform": "steam",
"name": "Hollow Knight: Silksong",
"image": "https://...",
"stock": 3,
"available": true,
"web_price": 25,
"full_price": 690,
"price": 23.75,
"rate_percent": 95,
"created_at": "2026-08-05T09:12:33.000Z",
"denuvo": false,
"steam": {
"name": "Hollow Knight: Silksong",
"genres": ["Action", "Adventure", "Indie"],
"platforms": { "windows": true, "mac": true, "linux": true },
"categories": ["Single-player", "Steam Achievements"],
"screenshots": ["https://shared.akamai.steamstatic.com/..."],
"pc_requirements": { "minimum": "<strong>Minimum:</strong>...", "recommended": "..." },
"description": "<h2>...</h2>",
"description_th": "<section>...</section>",
"updated_at": "2026-08-05T09:12:33.000Z"
},
"flash_sale": null
}
}POST
/api/v1/ordersสั่งซื้อสินค้า — ตัดเงินจากยอดเงินของคุณและได้ข้อมูลบัญชีทันที ต้องส่ง ref (เลขอ้างอิงฝั่งคุณ ไม่ซ้ำ, case-sensitive) เพื่อกันการสั่งซ้ำ: สั่งใหม่สำเร็จได้ HTTP 201 — ยิงซ้ำด้วย ref เดิมได้ HTTP 200 พร้อม idempotent:true โดยไม่ตัดเงินอีก (response แบบ 200 จะไม่มี balance_after) ดังนั้นให้เช็คผลจาก json.success เสมอ อย่าอิงจาก HTTP status code
max_price (ไม่บังคับ แต่แนะนำ) = ยอดสูงสุดที่ยอมให้ตัด เช่น price ที่ลูกค้าเห็นตอนจ่าย — ถ้าราคาเปลี่ยนจนแพงกว่านี้ (เช่น เลยช่วงราคาดีล order_until ไปแล้ว) จะได้ 409 PRICE_CHANGED พร้อม price ปัจจุบัน และยังไม่ตัดเงิน · บัญชีที่เปิดรับราคา Flash Sale สั่งช่วงดีล web_price = ราคาหลังลด ณ ตอนสั่งตัวอย่าง curl
curl -X POST "https://store.499k-network.com/api/v1/orders" \
-H "Authorization: Bearer 499k_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"product_id": "1030300", "ref": "my-order-0001"}'ตัวอย่าง JavaScript
const res = await fetch("https://store.499k-network.com/api/v1/orders", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.API_KEY_499K,
"Content-Type": "application/json",
},
body: JSON.stringify({ product_id: "1030300", ref: myOrderId }),
});
const json = await res.json();
if (!json.success) throw new Error(json.error.code + ": " + json.error.message);
const { order_no, account } = json.data; // account.username / account.passwordตัวอย่าง Response (201)
{
"success": true,
"data": {
"order_no": "API-260714-A1B2C3D4E5",
"type": "offline",
"product_id": "1030300",
"game_title": "Hollow Knight: Silksong",
"platform": "steam",
"web_price": 25,
"rate_percent": 95,
"tier_price": 23.75,
"price": 23.75,
"price_source": "tier",
"ref": "my-order-0001",
"balance_after": 4976.25,
"account": { "username": "steamuser01", "password": "p@ssw0rd" },
"code_requests": { "used": 0, "max": 3 },
"created_at": "2026-07-14T09:00:00.000Z"
}
}GET
/api/v1/orders?page=1&limit=20รายการออเดอร์ของคุณ (ใหม่สุดก่อน, limit สูงสุด 100) — ไม่รวมข้อมูลบัญชี ใช้ GET /orders/{order_no} เพื่อดูรายละเอียด
ตัวอย่าง Response
{
"success": true,
"data": {
"orders": [
{
"order_no": "API-260714-A1B2C3D4E5",
"type": "offline",
"product_id": "1030300",
"game_title": "Hollow Knight: Silksong",
"price": 23.75,
"rate_percent": 95,
"status": "success",
"code_requests_used": 1,
"ref": "my-order-0001",
"created_at": "2026-07-14T09:00:00.000Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 1 }
}
}GET
/api/v1/orders/{order_no}รายละเอียดออเดอร์ + ข้อมูลบัญชี + ประวัติการขอโค้ด
ตัวอย่าง Response
{
"success": true,
"data": {
"order_no": "API-260714-A1B2C3D4E5",
"type": "offline",
"product_id": "1030300",
"game_title": "Hollow Knight: Silksong",
"platform": "steam",
"web_price": 25,
"rate_percent": 95,
"tier_price": 23.75,
"price": 23.75,
"price_source": "tier",
"ref": "my-order-0001",
"status": "success",
"account": { "username": "steamuser01", "password": "p@ssw0rd" },
"code_requests": {
"used": 1,
"max": 3,
"history": [{ "reason": "ลูกค้าล็อกอินครั้งแรก", "created_at": "2026-07-14T09:05:00.000Z" }]
},
"created_at": "2026-07-14T09:00:00.000Z"
}
}POST
/api/v1/orders/{order_no}/codeขอโค้ด Steam Guard แบบหน้าต่างเวลา (เหมือนหน้าเว็บ): เรียกครั้งแรกพร้อม reason (5-500 ตัวอักษร) = เปิด 1 รอบ เปิดหน้าต่าง 60 วินาที — ภายในหน้าต่างเรียกซ้ำ (ไม่ต้องส่ง reason) เพื่อรับโค้ดล่าสุดได้ไม่จำกัด ไม่เผาโควตา (TOTP หมุนทุก 30 วิ) มีทั้งหมด 3 รอบต่อออเดอร์ พอหน้าต่างปิด เรียกอีกครั้งพร้อม reason = เปิดรอบใหม่
ตัวอย่าง Request Body (เปิดรอบใหม่)
{ "reason": "ลูกค้าล็อกอินเข้าเครื่องครั้งแรก" }ตัวอย่าง Request Body (ดึงซ้ำในหน้าต่าง)
{} // ไม่ต้องส่ง reasonตัวอย่าง Response
{
"success": true,
"data": {
"code": "ABC12",
"valid_for_sec": 21,
"window": { "expires_in_sec": 54, "new_round": true },
"code_requests": { "used": 1, "max": 3 }
}
}ออเดอร์เช่า (type rental) ไม่ใช้ระบบรอบ — ขอได้ไม่จำกัดครั้ง ไม่ต้องส่ง reason จำกัดแค่ต้องอยู่ในช่วงเวลาเช่า (นอกช่วง: 403 RENTAL_NOT_STARTED / RENTAL_EXPIRED) และ
max ใน response เป็น nullเช่าไอดี Steam (type rental)
สินค้าเช่าเป็น ช่วงเวลา ไม่ใช่ซื้อขาด — เวลาเป็นบล็อกครึ่งชั่วโมง (start_at ต้องเป็นนาที 00/30 เป๊ะ) ระบบเว้น 30 นาทีระหว่างคิว และจองล่วงหน้าได้ไม่เกิน 7 วัน · เช่าตอนนี้ = ส่งต้นบล็อกปัจจุบัน (ตอนนี้ 19:25 → ส่ง 19:00) · flow: จอง → ถึงเวลาลูกค้ากด activate → ขอโค้ดไม่จำกัด → ต่ออายุได้ก่อนหมดเวลา
GET
/api/v1/products/{appid}/availabilityคิวเช่ารายไอดีของเกมนี้ —
busy ขยายหัวท้ายด้วย buffer ให้แล้ว ช่วงที่ไม่ทับ busy เลยคือจองได้แน่นอน แต่ละไอดีมีเรทของตัวเอง (rates) อยากได้ไอดีไหนส่ง account_id ตอนสั่งตัวอย่าง Response
{
"success": true,
"data": {
"product_id": "251570",
"type": "rental",
"name": "7 Days to Die",
"bookable_from": "2026-08-12T13:00:00.000Z",
"bookable_until": "2026-08-19T13:25:00.000Z",
"accounts": [
{
"account_id": 57,
"games": [{ "app_id": 251570, "name": "7 Days to Die", "image": "https://..." }],
"rates": { "1": { "web_price": 15, "price": 14.25 }, "3": { "web_price": 30, "price": 28.5 } },
"busy": [{ "from": "2026-08-12T17:30:00.000Z", "to": "2026-08-13T18:30:00.000Z" }]
}
]
}
}POST
/api/v1/orders (type rental)จองคิวเช่า —
account_id ไม่บังคับ (ไม่ส่ง = ระบบเลือกไอดีเรทถูกสุดที่ว่างตลอดช่วง) · account เป็น null จนกว่าจะถึงเวลาเช่า · error ที่ต้องรองรับ: INVALID_START_AT, DURATION_UNAVAILABLE, SLOT_UNAVAILABLE (โดนจองตัดหน้า — รีเฟรช availability แล้วเลือกใหม่)ตัวอย่าง Request Body
{
"type": "rental",
"product_id": "251570",
"duration_days": 3,
"start_at": "2026-08-14T12:00:00Z",
"ref": "rental-uuid-0001",
"account_id": 57
}ตัวอย่าง Response (HTTP 201)
{
"success": true,
"data": {
"order_no": "API-260814-XXXXXXXXXX",
"type": "rental",
"product_id": "251570",
"game_title": "7 Days to Die",
"duration_days": 3,
"start_at": "2026-08-14T12:00:00.000Z",
"end_at": "2026-08-17T12:00:00.000Z",
"rental_status": "waiting",
"account_ref": 57,
"web_price": 30,
"tier_price": 28.5,
"price": 28.5,
"price_source": "tier",
"balance_after": 123.45,
"account": null,
"code_requests": { "used": 0, "max": null }
}
}POST
/api/v1/orders/{order_no}/activateเปิดใช้งานไอดี (เตะ session ผู้เช่าคนก่อน) — ให้ลูกค้ากดเองเมื่อพร้อมเล่น ภายในช่วงเวลาเช่าเท่านั้น เรียกซ้ำได้ (already_active) · 409 ACTIVATION_BUSY = รอ retry_after_sec แล้วยิงใหม่ · 502 ACTIVATION_FAILED = retry ได้เรื่อย ๆ หรือใช้ username/password ล็อกอินแล้วขอโค้ดได้เลยโดยไม่ต้องรอ (รหัสผ่านไม่ได้เปลี่ยน)
ตัวอย่าง Response
{
"success": true,
"data": {
"order_no": "API-260814-XXXXXXXXXX",
"rental_status": "active",
"already_active": false,
"activated_at": "2026-08-14T12:03:11.000Z",
"account": { "username": "steamuser", "password": "xxxxx" }
}
}POST
/api/v1/orders/{order_no}/renewต่ออายุ — ได้เฉพาะช่วงที่ยังเช่าอยู่ (400 RENEW_WINDOW_CLOSED) และปลายทางใหม่ต้องไม่ชนคิวถัดไป (409 SLOT_UNAVAILABLE) · ต่อคิวติดกันของออเดอร์เดิมไม่ติด buffer · ไม่ idempotent — อย่า retry อัตโนมัติ
ตัวอย่าง Request Body
{ "duration_days": 3 }ตัวอย่าง Response
{
"success": true,
"data": {
"order_no": "API-260814-XXXXXXXXXX",
"duration_days": 3,
"end_at": "2026-08-20T12:00:00.000Z",
"web_price": 30,
"tier_price": 28.5,
"price": 28.5,
"price_source": "tier",
"balance_after": 94.95
}
}ชุดสินค้า (type bundle)
หลายเกมในราคาเดียว ถูกกว่าซื้อแยก — ชุดเดียวกับหน้าเว็บ ราคาเดียวกัน · ซื้อทั้งชุดในคำสั่งเดียว ตัดเงินครั้งเดียว ได้ครบทุกเกมหรือไม่ได้เลย · ได้ออเดอร์ลูก 1 ใบต่อเกม (
API-) ใช้ขอโค้ด Steam Guard / ดูรหัสผ่านกับ endpoint เดิมได้เลย ส่วนเลขชุด (BDL-) ใช้ดูทั้งชุด · ราคาชุดตายตัว ไม่เกี่ยวกับ Flash Sale ของเกมในชุด · sandbox: ชุดทดสอบ 999003GET
/api/v1/products?type=bundleรายการชุดที่ขายได้ตอนนี้ (ไม่รวมใน list ปกติ) —
web_price = ราคาชุดหน้าเว็บ, price = ราคาที่คุณจ่ายตามเรท, list_price = ราคาปกติรวมทุกเกม · stock = เกมที่เหลือไอดีน้อยสุดในชุด · denuvo = true ถ้ามีเกมไหนในชุดมี Denuvo (ต้องขึ้นคำเตือนเหมือนเกมเดี่ยว) · ชุดเดียว: GET /api/v1/products/{เลขชุด}?type=bundle เพิ่ม available / unavailable_reason และ steam ชุดเต็มของแต่ละเกม (ชุดที่ปิดขายแล้ว = 404)ตัวอย่าง Response
{
"success": true,
"data": {
"products": [
{
"type": "bundle",
"product_id": "28",
"platform": "steam",
"name": "JSP 2+3 Bundle",
"slug": "jsp-bundle",
"description": null,
"image": "https://...",
"stock": 1,
"web_price": 45,
"list_price": 60,
"saving": 15,
"saving_percent": 25,
"full_price": 690,
"denuvo": false,
"item_count": 2,
"items": [
{
"product_id": "4193270",
"platform": "steam",
"name": "Japan Stigmatized Property 2",
"image": "https://...",
"web_price": 30,
"full_price": 345,
"denuvo": false,
"steam": { "name": "...", "genres": ["Simulation"], "has_thai": false }
},
{ "product_id": "4611990", "...": "เกมที่ 2" }
],
"created_at": "2026-09-20T10:00:00.000Z",
"rate_percent": 80,
"price": 36
}
],
"total": 1,
"types": ["offline", "rental", "bundle"]
}
}POST
/api/v1/orders (type bundle)ซื้อทั้งชุด —
items (ไม่บังคับ แต่แนะนำ) = product_id ของเกมในชุดที่ลูกค้าเห็น ถ้าชุดถูกแก้ไปแล้วได้ 409 BUNDLE_CHANGED พร้อม items ปัจจุบัน (ไม่ตัดเงิน) · max_price / ref / price และ username ของคีย์แม่ ใช้ได้เหมือนออเดอร์เดี่ยว (คีย์แม่ขั้นต่ำ 50% ของราคาชุด) · price ของแต่ละเกมคือส่วนแบ่งของราคาชุด (หารตามราคาปกติของเกม) รวมกันเท่า price ของชุดพอดี · ชุดไม่พร้อมขายชั่วคราว = 409 BUNDLE_UNAVAILABLE (reason: out_of_stock / seller_suspended / missing / no_saving / invalid) · ยิง ref เดิมซ้ำได้ทั้งชุดเดิมกลับมา ไม่ตัดเงินซ้ำตัวอย่าง Request Body
{
"type": "bundle",
"product_id": "28",
"ref": "my-bundle-0001",
"items": ["4193270", "4611990"],
"max_price": 36
}ตัวอย่าง Response (HTTP 201)
{
"success": true,
"data": {
"order_no": "BDL-260927-A1B2C3D4E5",
"type": "bundle",
"env": "live",
"product_id": "28",
"bundle_name": "JSP 2+3 Bundle",
"web_price": 45,
"rate_percent": 80,
"tier_price": 36,
"price": 36,
"price_source": "tier",
"username": "myshop",
"ref": "my-bundle-0001",
"balance_after": 4964,
"items": [
{
"order_no": "API-260927-0C8A2725EC",
"type": "offline",
"product_id": "4193270",
"game_title": "Japan Stigmatized Property 2",
"platform": "steam",
"web_price": 22.5,
"tier_price": 18,
"price": 18,
"account": { "username": "steamuser01", "password": "p@ssw0rd" },
"code_requests": { "used": 0, "max": 3 }
},
{
"order_no": "API-260927-5529679039",
"type": "offline",
"product_id": "4611990",
"game_title": "Japan Stigmatized Property 3",
"platform": "steam",
"web_price": 22.5,
"tier_price": 18,
"price": 18,
"account": { "username": "steamuser02", "password": "p@ssw0rd" },
"code_requests": { "used": 0, "max": 3 }
}
],
"created_at": "2026-09-27T09:00:00.000Z"
}
}GET
/api/v1/orders/{bundle_order_no}ใส่เลขชุด (
BDL-...) จะได้ทั้งชุดรูปทรงเดียวกับตอนสั่งซื้อ + status ของชุด (success / partially_refunded / refunded) และของแต่ละเกม — account ของเกมที่คืนเงินแล้วเป็น null · ใน GET /api/v1/orders เกมที่ซื้อมาเป็นชุดจะขึ้นเป็นออเดอร์ลูกรายเกม พร้อม bundle_order_noAI Prompt สำเร็จรูป
คัดลอกทั้งก้อนไปวางใน AI (Claude, ChatGPT ฯลฯ) เพื่อสร้างเว็บที่เชื่อมต่อ API นี้ — ตอนตั้งค่าค่อยแทน YOUR_API_KEY ด้วยคีย์จริง (อย่าส่งคีย์จริงให้ AI)
AI Prompt (ภาษาไทย)
คุณคือนักพัฒนาเว็บมืออาชีพ ช่วยสร้างเว็บไซต์ขายไอดีเกม Steam แบบ Offline ที่เชื่อมต่อกับ API ของร้านค้าต้นทางให้หน่อย
ข้อมูล API (REST, JSON ทั้งหมด):
- Base URL: https://store.499k-network.com
- การยืนยันตัวตน: ทุก request ต้องส่ง header "Authorization: Bearer YOUR_API_KEY" (หรือ "x-api-key: YOUR_API_KEY")
- Sandbox: คีย์ทดสอบ (ขึ้นต้น 499k_test_) ใช้ endpoint เดียวกันทั้งหมด แต่เห็นเฉพาะสินค้าทดสอบ type "sandbox" product_id "999001" และไม่ตัดเงินจริง — ให้ทำระบบด้วยคีย์ทดสอบก่อน เสร็จแล้วสลับเป็นคีย์จริงได้เลยโดยไม่ต้องแก้โค้ด (เก็บคีย์ใน env variable)
- สำคัญมาก: ต้องเรียก API จากฝั่ง server ของเว็บเท่านั้น (API route / backend) ห้ามใส่ API key ในโค้ดฝั่ง browser เด็ดขาด
- รูปแบบ response สำเร็จ: {"success":true,"data":{...}} / ผิดพลาด: {"success":false,"error":{"code":"...","message":"..."}}
Endpoints:
1) GET https://store.499k-network.com/api/v1/me
ข้อมูลบัญชี: data = { client:{first_name,last_name,website_name,status}, balance, volume, rate_percent, current_tier, next_tier:{min_volume,rate,remaining}|null, min_topup:{required,current,passed}, key_prefix }
2) GET https://store.499k-network.com/api/v1/products (optional ?type=offline|rental|bundle, ?platform=steam|epic|uplay|other, ?expand=steam)
รายการสินค้า: data = { products:[{ type, product_id, platform, name, image, stock, web_price, full_price, price, rate_percent, created_at, denuvo, steam, flash_sale }], total, types }
- price คือราคาที่เราต้องจ่ายจริง (หักเรทแล้ว) — เว็บของเราควรบวกกำไรเองก่อนแสดงขายต่อ
- ราคา Flash Sale ต้องเปิดรับก่อน (ค่าเริ่มต้นปิด) ที่หน้า API Store "รับราคา Flash Sale" — ปิดอยู่ = web_price เป็นราคาปกติเสมอ และ flash_sale เป็น null ทุกตัว
- เปิดแล้ว web_price คือราคาหน้าเว็บร้านต้นทาง "ตอนนี้" — ถ้าเกมกำลัง Flash Sale จะเป็นราคาหลังลดแล้ว และ price (รวมถึงยอดตอนสั่งซื้อ) ก็คิดจากราคาหลังลด
- flash_sale: null = ไม่ได้ลดอยู่ / { label:"-30%", normal_web_price, normal_price, ends_at, order_until } = กำลังลดถึง ends_at (ISO 8601 UTC)
normal_price คือ price ตอนไม่ลด (ไว้ทำราคาขีดฆ่า) — เลย ends_at แล้วหน้าเว็บกลับเป็นราคาปกติ แต่สั่งซื้อผ่าน API ด้วยราคาดีลได้อีก 1 ชั่วโมง (ถึง order_until)
⚠️ ต้องอัปเดตราคา (ดึง /api/v1/products ใหม่) อย่างน้อยทุก 30 นาที และเลิกขายราคาดีลเมื่อเลย order_until
- full_price คือราคาเต็มของเกม (ราคาป้าย Steam) ไว้เอาไปแสดงเป็นราคาขีดฆ่า — อาจเป็น null ถ้าไม่ได้ตั้งไว้ ต้องเช็คก่อนใช้
- image เป็น URL เต็มพร้อมใช้ โหลดตรงได้เลย ไม่ต้องต่อ path เอง — เป็นรูปปกแนวตั้ง (สัดส่วน 2:3 เช่น 600x900) เหมือนที่หน้าร้านเราใช้
บางเกมเป็นรูปที่เราอัพเอง ที่เหลือดึงจาก Steam ให้อัตโนมัติ
- created_at คือเวลาที่ลงของล่าสุดของเกมนั้น (ISO 8601 UTC ลงท้าย Z) ไว้เรียง "เกมใหม่" ให้ตรงกับหน้าร้านเรา
- denuvo (true/false) — เกมที่มีระบบ Denuvo จำกัดสิทธิ์เข้าเกมครั้งแรก 5 สิทธิ์/วัน ลูกค้าอาจต้องรอคิว 1-3 วันช่วงเกมเปิดตัว
⚠️ ถ้า true ต้องขึ้นคำเตือนในหน้าสินค้าก่อนลูกค้ากดซื้อ ไม่งั้นลูกค้าจะเข้าใจผิดว่าไอดีเสียแล้วเปิด Ticket
- type คือประเภทสินค้า (ตอนนี้มี "offline", "rental", "bundle" — อาจมีประเภทใหม่เพิ่มในอนาคต ดูได้จาก field types)
⚠️ ชุดสินค้า (bundle) ไม่รวมใน list ปกติ ต้องขอด้วย ?type=bundle (ดูข้อ 7.6)
- product_id ของ type offline คือ appid ของเกม (string ตัวเลข)
- steam คือข้อมูลเกมจาก Steam ไว้ทำหน้าสินค้าเอง — เป็น null ได้ (เกมที่ไม่ใช่ platform steam หรือยังไม่มีข้อมูล ~2 จาก 244 เกม) ต้องเช็คก่อนใช้เสมอ
ปกติได้ชุดเบา: { name, type, genres, genres_th, platforms, has_thai, thai_audio, release_date, metacritic_score, header_image } — เอาไปทำแท็ก/ตัวกรองได้เลย
⚠️ has_thai / thai_audio เป็น true/false/null — null แปลว่า "ยังไม่ได้เก็บข้อมูลเกมนี้" ไม่ใช่ "ไม่มีภาษาไทย" อย่าเอาไปกรองออกตรงๆ
ใส่ ?expand=steam จะได้ชุดเต็มเพิ่ม: { categories, categories_th, languages, supported_languages, developers, publishers,
recommendations, is_free, required_age, coming_soon, short_description, screenshots, pc_requirements, description, description_th, updated_at }
⚠️ ชุดเต็มของทั้ง list หนักประมาณ 3.3 MB (ชุดเบา 56 KB) อย่าเรียกทุกครั้ง — ให้ sync เป็นรอบแล้วเก็บลง DB ตัวเอง
ถ้าต้องการแค่บางเกม ใช้ /api/v1/products/{product_id} ซึ่งให้ชุดเต็มอยู่แล้ว
description / pc_requirements เป็น HTML ดิบจาก Steam ต้อง sanitize ก่อน render ไม่งั้นเสี่ยง XSS
description_th คือคำบรรยายภาษาไทยที่ทางเราเขียนเอง (มี 238 จาก 242 เกม) ถ้า null ให้ fallback ไป description
3) GET https://store.499k-network.com/api/v1/products/{product_id} (optional ?type=offline, default offline)
สินค้าตัวเดียว: data = { type, product_id, platform, name, image, stock, available, web_price, full_price, price, rate_percent, created_at, denuvo, steam }
- steam ของ endpoint นี้ให้ชุดเต็มเสมอ ไม่ต้องส่ง expand
4) POST https://store.499k-network.com/api/v1/orders
body: {"product_id":"123456","ref":"เลขออเดอร์ฝั่งเราที่ไม่ซ้ำ","type":"offline"} (type ไม่บังคับ default "offline")
- ref จำเป็น: a-z A-Z 0-9 _ - ยาวไม่เกิน 64 ตัว (case-sensitive) แนะนำใช้ UUID ต่อ 1 ออเดอร์
- สั่งซื้อใหม่สำเร็จ (HTTP 201): data = { order_no, type, product_id, game_title, platform, web_price, rate_percent, tier_price, price, price_source, ref, balance_after, account:{username,password}, code_requests:{used,max}, created_at }
- ยิงซ้ำด้วย ref เดิม (HTTP 200): ได้ออเดอร์เดิมกลับมา ไม่ตัดเงินซ้ำ — data เพิ่ม idempotent:true และ status แต่ "ไม่มี" balance_after ดังนั้นเช็คผลจาก json.success เสมอ อย่าเช็คจาก HTTP status code หรือ balance_after
- ต้องเก็บ order_no ไว้เสมอ และแสดง username/password ให้ลูกค้าปลายทาง
- price คือยอดที่ถูกตัดจริง (เอาไปบันทึกยอดฝั่งเราได้เลย), tier_price คือยอดตามเรทปกติ, price_source = "tier" | "master"
- max_price (ไม่บังคับ แต่แนะนำให้ส่งเสมอ): ยอดสูงสุดที่ยอมให้ตัด เช่น price ที่ลูกค้าเห็นตอนจ่าย
ถ้าราคาเปลี่ยนจนแพงกว่านี้ (เช่น เลยช่วงราคาดีล order_until ไปแล้ว หรือราคาปกติของเกมถูกปรับ) จะได้ 409 PRICE_CHANGED (แนบ price ปัจจุบัน) และยังไม่ตัดเงิน
- ออเดอร์ที่สั่งช่วง Flash Sale (เฉพาะบัญชีที่เปิดรับราคา Flash Sale): web_price ในออเดอร์ = ราคาหลังลด ณ ตอนสั่ง
4.1) คีย์แม่ (master key) — สำหรับพาร์ทเนอร์ที่เปิดร้านย่อยหลายร้าน เรทไม่เท่ากัน
เฉพาะคีย์ที่ถูกตั้งเป็นคีย์แม่เท่านั้น ส่ง price มาเองได้ ระบบจะตัดเงินตามนั้นแทนเรท tier
body: {"product_id":"123456","ref":"...","price":21.50}
- คีย์ทั่วไปส่ง price มา -> 403 PRICE_NOT_ALLOWED
- price ต่ำกว่าขั้นต่ำ -> 422 PRICE_TOO_LOW (details บอก floor, min_rate_percent, web_price)
- ขั้นต่ำปกติ 50% ของ web_price (กันคีย์หลุดแล้วซื้อของถูกๆ) ปรับรายคีย์ได้
- ไม่ส่ง price มา = ใช้เรท tier ตามปกติ (ของเดิมไม่กระทบ)
- ส่ง price มา = ตัดตามนั้นเป๊ะ ไม่ลดให้อัตโนมัติ — ช่วง Flash Sale (บัญชีที่เปิดรับ) ต้องคิด price จาก web_price ตอนนั้นเอง ถ้าส่งราคาปกติมาจะโดนตัดราคาปกติ
5) GET https://store.499k-network.com/api/v1/orders?page=1&limit=20
รายการออเดอร์ (ไม่มีข้อมูลบัญชี): data = { orders:[{order_no, type, env, product_id, game_title, price, rate_percent, price_source, status, username, code_requests_used, ref, created_at}], pagination:{page,limit,total} }
- เกมที่ซื้อมาเป็นชุดจะขึ้นเป็นออเดอร์ลูกรายเกม และมี bundle_order_no (เลขชุดเดียวกัน = คำสั่งซื้อเดียวกัน)
6) GET https://store.499k-network.com/api/v1/orders/{order_no}
รายละเอียด + ข้อมูลบัญชี: data = { ...เหมือนตอนสั่งซื้อ, status, account:{username,password}, code_requests:{used,max,history:[{reason,created_at}]} }
- ใส่เลขชุด (BDL-...) ได้ด้วย จะได้ทั้งชุด (ดูข้อ 7.6.4) · ออเดอร์ลูกของชุดมี bundle_order_no
7) POST https://store.499k-network.com/api/v1/orders/{order_no}/code
ขอโค้ด Steam Guard สำหรับล็อกอิน: body {"reason":"เหตุผล 5-500 ตัวอักษร เช่น ลูกค้าล็อกอินครั้งแรก"}
- โมเดลหน้าต่างเวลา (เหมือนหน้าเว็บ): เรียกครั้งแรกต้องส่ง reason = "เปิด 1 รอบ" เปิดหน้าต่าง 60 วินาที
- ภายในหน้าต่าง 60 วิ เรียก endpoint นี้ซ้ำได้ไม่จำกัดเพื่อรับโค้ดล่าสุด (TOTP หมุนทุก 30 วิ) โดยไม่ต้องส่ง reason และไม่เผาโควตา
- มีทั้งหมด 3 รอบต่อออเดอร์ — พอหน้าต่างปิดแล้วเรียกอีกครั้ง (พร้อม reason) = เปิดรอบใหม่ (เผาโควตา 1)
- data = { code, valid_for_sec, window:{expires_in_sec, new_round}, code_requests:{used,max} } — used = จำนวนรอบที่เปิดไปแล้ว, window.expires_in_sec = หน้าต่างเหลือกี่วินาที
7.5) เช่าไอดี Steam (type "rental") — สินค้าเป็น "ช่วงเวลา" ไม่ใช่ซื้อขาด
กติกาเวลา: บล็อกครึ่งชั่วโมง (start_at ต้องเป็นนาที 00 หรือ 30 วินาที 00 เป๊ะ ไม่งั้น 422 INVALID_START_AT)
เช่าตอนนี้ = ส่งต้นบล็อกปัจจุบัน (ตอนนี้ 19:25 ส่ง 19:00 ได้เลย) / จองล่วงหน้าไม่เกิน 7 วัน
ระบบเว้น buffer 30 นาทีระหว่างคิว (คิวก่อนจบ 18:00 → คิวใหม่เริ่มเร็วสุด 18:30)
7.5.1) GET /api/v1/products?type=rental — รายการเกมให้เช่า
รายการเหมือน offline แต่ไม่มี price เดี่ยว — มี durations:{"1":{web_price,price},"3":{...}} ราคาแยกตามระยะ (1/3/7/15/30 วัน)
stock = จำนวนไอดี, ความว่างจริงต้องดู availability
7.5.2) GET /api/v1/products/{appid}/availability — คิวเช่ารายไอดี
data = { bookable_from, bookable_until, accounts:[{account_id, games:[...], rates:{...}, busy:[{from,to}]}] }
busy ขยายหัวท้ายด้วย buffer ให้แล้ว — ช่วง [start,end) ที่ไม่ทับ busy เลย = จองได้แน่นอน
7.5.3) POST /api/v1/orders — เพิ่ม field: {"type":"rental","product_id":"...","duration_days":3,"start_at":"2026-08-14T12:00:00Z","ref":"...","account_id":57}
account_id ไม่บังคับ (ไม่ส่ง = ระบบเลือกไอดีเรทถูกสุดที่ว่าง), price/username ของคีย์แม่ใช้ได้เหมือนเดิม
ตอบเพิ่ม: duration_days, start_at, end_at, rental_status ("waiting"), account_ref
⚠️ account เป็น null จนกว่าจะถึงเวลาเช่า — พอ now ≥ start_at ค่อยได้ {username,password} จาก GET /orders/{order_no}
error ใหม่: INVALID_START_AT(422), DURATION_UNAVAILABLE(422), SLOT_UNAVAILABLE(409 = ชนคิว/โดนจองตัดหน้า → รีเฟรช availability แล้วเลือกใหม่)
7.5.4) POST /api/v1/orders/{order_no}/activate — เปิดใช้งานไอดี (ให้ลูกค้ากดเองตอนพร้อมเล่น ภายในช่วงเวลาเช่า)
สำเร็จ = { rental_status:"active", activated_at, account:{username,password} } เรียกซ้ำได้ (already_active:true)
409 ACTIVATION_BUSY (รอ retry_after_sec แล้วยิงใหม่) / 502 ACTIVATION_FAILED = retry ได้ หรือให้ลูกค้าใช้ username/password ล็อกอินแล้วขอโค้ดได้เลยไม่ต้องรอ
ห้าม auto-activate ตอนสั่ง — เวลาเช่านับจาก start_at อยู่แล้ว
7.5.5) POST /api/v1/orders/{order_no}/code สำหรับ rental: ไม่จำกัดจำนวนครั้ง ไม่ต้องส่ง reason (max ใน response = null)
จำกัดแค่ต้องอยู่ในช่วงเวลาเช่า — นอกช่วง: 403 RENTAL_NOT_STARTED / RENTAL_EXPIRED (ของ offline ยังเป็นระบบ 3 รอบเหมือนเดิม)
7.5.6) POST /api/v1/orders/{order_no}/renew — ต่ออายุ body {"duration_days":3, "price":..., "username":"..."}
ได้เฉพาะช่วงที่ยังเช่าอยู่ (400 RENEW_WINDOW_CLOSED) และต้องไม่ชนคิวถัดไป (409 SLOT_UNAVAILABLE)
⚠️ ไม่ idempotent — ยิงซ้ำ = ตัดเงินซ้ำ อย่า retry อัตโนมัติ
7.5.7) GET /orders/{order_no} ของออเดอร์เช่า มีเพิ่ม: duration_days, start_at, end_at, rental_status(waiting|active|expired|cancelled), activated_at, account_ref
account มีค่าเฉพาะช่วง start_at ≤ now ≤ end_at
sandbox: product_id "999002" (ระยะ 1/3/7 เรท 5/12/25) ครบทุก endpoint คิวว่างเสมอ
7.6) ชุดสินค้า (type "bundle") — หลายเกมในราคาเดียว ถูกกว่าซื้อแยก
7.6.1) GET /api/v1/products?type=bundle — รายการชุดที่ขายได้ตอนนี้ (ไม่รวมใน list ปกติ ต้องส่ง ?type=bundle)
products = [{ type:"bundle", product_id (เลขชุด), name, slug, description, image, stock, web_price (ราคาชุดหน้าเว็บ), price (ราคาที่เราจ่าย),
list_price (ราคาปกติรวมทุกเกม), saving, saving_percent, full_price, denuvo, item_count,
items:[{ product_id (appid), platform, name, image, web_price, full_price, denuvo, steam }] }]
- ราคาชุดตายตัว ไม่เกี่ยวกับ Flash Sale ของเกมในชุด · denuvo = true ถ้ามีเกมไหนในชุดมี Denuvo (ต้องขึ้นคำเตือนเหมือนเกมเดี่ยว)
7.6.2) GET /api/v1/products/{เลขชุด}?type=bundle — ชุดเดียว + available / unavailable_reason (steam ของแต่ละเกมเป็นชุดเต็ม)
ชุดที่ปิดขายแล้ว -> 404 PRODUCT_NOT_FOUND
7.6.3) POST /api/v1/orders body: {"type":"bundle","product_id":"28","ref":"...","items":["4193270","4611990"],"max_price":36}
- ซื้อทั้งชุดในครั้งเดียว ตัดเงินครั้งเดียว ได้ครบทุกเกมหรือไม่ได้เลย (ห้ามซื้อแยกทีละเกมแล้วหวังราคาชุด)
- items (ไม่บังคับ แต่แนะนำ) = product_id ของเกมในชุดที่ลูกค้าเห็น — ถ้าชุดถูกแก้ไปแล้วจะได้ 409 BUNDLE_CHANGED (แนบ items ปัจจุบัน) ไม่ตัดเงิน
- price / username ของคีย์แม่ใช้ได้เหมือนเดิม (ขั้นต่ำ 50% ของราคาชุด)
- สำเร็จ (HTTP 201): data = { order_no (เลขชุด BDL-...), type:"bundle", product_id, bundle_name, web_price, rate_percent, tier_price, price, price_source, username, ref, balance_after,
items:[{ order_no (ออเดอร์ลูก API-...), type:"offline", product_id, game_title, platform, web_price, tier_price, price, account:{username,password}, code_requests:{used,max} }], created_at }
- price ของแต่ละ items คือส่วนแบ่งของราคาชุด (หารตามราคาปกติของเกม) รวมกันเท่า price ของชุดพอดี
- ขอโค้ด Steam Guard / ดูรหัสผ่าน ใช้ order_no ของออเดอร์ลูกแต่ละเกมกับ endpoint เดิม (3 รอบต่อเกม)
- ยิง ref เดิมซ้ำ -> ได้ทั้งชุดเดิมกลับมา (idempotent:true, status) ไม่ตัดเงินซ้ำ
- error เพิ่ม: BUNDLE_UNAVAILABLE(409 ชุดยังไม่พร้อมขายชั่วคราว ดู reason), BUNDLE_CHANGED(409)
7.6.4) GET /api/v1/orders/{เลขชุด BDL-...} — ทั้งชุด + items ทุกเกม (status: success | partially_refunded | refunded)
sandbox: ชุดทดสอบ product_id "999003" (2 เกม ราคาชุด 15) — ออเดอร์ลูกเป็น type "sandbox" เลขชุดขึ้นต้น TEST-BDL-
8) ระบบสมาชิก (เฉพาะคีย์แม่) — สำหรับพาร์ทเนอร์ที่ทำหน้าเว็บครอบ ให้ลูกค้าสมัคร/เติมเงิน/ซื้อในนามตัวเอง
บัญชีที่สร้างเป็นบัญชี 499K จริง ลูกค้าล็อกอินหน้าเว็บ 499K ได้เหมือนสมัครเอง
8.1) POST https://store.499k-network.com/api/v1/users — สมัครสมาชิกแทนลูกค้า
body: {"email":"[email protected]","password":"อย่างน้อย 8 ตัว"}
- username จะถูกตั้งจากส่วนหน้า @ ของอีเมลอัตโนมัติ ([email protected] -> "someone")
- data = { user_id, username, email, balance }
- อีเมลซ้ำ -> 409 EMAIL_EXISTS | ชื่อผู้ใช้ซ้ำ -> 409 USERNAME_TAKEN (ต้องใช้อีเมลอื่น)
8.2) GET https://store.499k-network.com/api/v1/users/{username} — ดูยอดเงิน/ข้อมูลลูกค้า
data = { user_id, username, email, balance }
8.2.1) GET https://store.499k-network.com/api/v1/users?usernames=a,b,c — ดูยอดเงินหลายคนพร้อมกัน (สูงสุด 100 ชื่อ)
data = { users:[{user_id,username,email,balance,disabled}], not_found:[...], ambiguous:[...], total_balance }
- ต้องเช็ค not_found ด้วย (ชื่อที่ไม่มีในระบบ) ลำดับใน users ไม่รับประกันว่าตรงกับที่ส่งไป
8.2.2) PATCH https://store.499k-network.com/api/v1/users/{username}/password — เปลี่ยนรหัสผ่าน
body: {"password":"อย่างน้อย 8 ตัว"}
- เปลี่ยนได้เฉพาะบัญชีที่สมัครผ่าน API นี้ — บัญชีที่ลูกค้าสมัครเองบนเว็บ -> 403 NOT_API_ACCOUNT
(กันคีย์แม่หลุดแล้วยึดบัญชีลูกค้าเดิมของเว็บ)
8.2.3) GET https://store.499k-network.com/api/v1/users/{username}/transactions?page=1&limit=20&type=buy
ประวัติการเงินของลูกค้า (รวมที่ทำเองบนเว็บ 499K ด้วย) — type: buy|refill|bonus|withdraw|point
data = { user_id, username, balance, transactions:[{transaction_id, type, product_type, direction, amount, status, title, order_no, created_at}], pagination }
- direction = "out" (ซื้อ/ถอน) หรือ "in" (เติม/โบนัส)
8.5) endpoint ออเดอร์รองรับ username ด้วย (คีย์แม่) — ใช้กันข้อมูลข้ามลูกค้า
GET /api/v1/orders?username=xxx -> เห็นเฉพาะออเดอร์ของลูกค้าคนนั้น (ทุกออเดอร์มี field username กลับมาด้วย)
GET /api/v1/orders/{order_no}?username=xxx -> ถ้าออเดอร์ไม่ใช่ของคนนั้น -> 403 ORDER_OWNER_MISMATCH
POST /api/v1/orders/{order_no}/code -> ใส่ "username" ในบอดี้ ระบบจะยืนยันเจ้าของก่อนปล่อยโค้ด Steam Guard
- ทั้งหมดเป็น optional: ไม่ส่ง username = พฤติกรรมเดิม (เห็นทุกออเดอร์ของ client)
- แนะนำให้ส่งเสมอ ถ้าเว็บพาร์ทเนอร์มีระบบล็อกอินผู้ใช้ — กันบั๊กทำให้ลูกค้า A เห็นรหัสไอดีของลูกค้า B
8.3) POST https://store.499k-network.com/api/v1/users/{username}/topup — เติมเงินด้วยสลิป
body: {"qrcode":"ข้อความที่อ่านได้จาก QR ในสลิปโอนเงิน"}
- ตรวจสลิปด้วยระบบเดียวกับหน้าเว็บ (กันสลิปซ้ำ ตรวจบัญชีผู้รับ ยอดเงินอ่านจากสลิปจริง)
- data = { user_id, username, amount, balance_before, balance_after }
- สลิปไม่ผ่าน -> 422 SLIP_REJECTED (message บอกสาเหตุ เช่น "สลิปใช้ไปแล้ว")
- ⚠️ ทำรายการผิดเกิน 3 ครั้งต่อผู้ใช้ ระบบจะระงับการเติมของผู้ใช้นั้น ต้องติดต่อแอดมิน
8.4) สั่งซื้อในนามลูกค้า: POST /api/v1/orders เพิ่ม "username" ในบอดี้
body: {"product_id":"123456","ref":"...","username":"someone","price":21.50}
- ตัดเงินจาก "กระเป๋าของลูกค้าคนนั้น" ไม่ใช่กระเป๋าเจ้าของคีย์
- balance_after ใน response = ยอดคงเหลือของลูกค้าคนนั้น
- ออเดอร์จะขึ้นในประวัติการซื้อของลูกค้าบนเว็บ 499K ด้วย
- ไม่ส่ง username = ซื้อในนามเจ้าของคีย์เอง (พฤติกรรมเดิม)
Error codes ที่ต้องรองรับ: UNAUTHORIZED(401), KEY_REVOKED(401), MASTER_KEY_REQUIRED(403), EMAIL_EXISTS(409), USERNAME_TAKEN(409), USER_NOT_FOUND(404), USER_DISABLED(403), SLIP_REJECTED(422), CLIENT_NOT_APPROVED(403), CLIENT_SUSPENDED(403), MIN_TOPUP_REQUIRED(403), SYSTEM_DISABLED(503), VALIDATION_ERROR(422), PRODUCT_NOT_FOUND(404), PRODUCT_DISABLED(404), OUT_OF_STOCK(409), INSUFFICIENT_BALANCE(400 พร้อม required/balance), PRICE_NOT_ALLOWED(403), PRICE_TOO_LOW(422 พร้อม floor), PRICE_CHANGED(409 พร้อม price), BUNDLE_UNAVAILABLE(409), BUNDLE_CHANGED(409 พร้อม items), CODE_LIMIT_REACHED(403), CODE_UNAVAILABLE(409), ORDER_REFUNDED(403), NOT_FOUND(404), RATE_LIMITED(429 พร้อม header Retry-After), INTERNAL_ERROR(500)
เฉพาะเช่า (type rental): INVALID_START_AT(422), DURATION_UNAVAILABLE(422), SLOT_UNAVAILABLE(409), ACTIVATION_BUSY(409), ACTIVATION_FAILED(502), ACTIVATION_NOT_STARTED(400), ACTIVATION_EXPIRED(400), RENEW_WINDOW_CLOSED(400), RENTAL_NOT_STARTED(403), RENTAL_EXPIRED(403)
Rate limit: ทั่วไป 60 req/นาที/คีย์, สั่งซื้อ 10 ครั้ง/นาที — ถ้าเจอ 429 ให้รอตาม Retry-After แล้วค่อยลองใหม่ (exponential backoff) และ retry การสั่งซื้อด้วย ref เดิมเสมอ
สิ่งที่ต้องมีในเว็บ:
1. หน้ารายการสินค้า: ดึงจาก /api/v1/products ผ่าน backend ของเราเอง และอัปเดตราคา/สต็อกอย่างน้อยทุก 30 นาที (ตั้ง cron) แสดงรูป ชื่อเกม ราคาขาย (price ของเรา + กำไรที่เราตั้ง) และสถานะสต็อก
2. หน้าสั่งซื้อ/ชำระเงิน: ตามระบบชำระเงินของเว็บเรา เมื่อลูกค้าจ่ายเงินสำเร็จค่อยยิง POST /orders ด้วย ref ที่ gen ไว้ (UUID) — ถ้า error ให้แจ้งลูกค้าและ retry ด้วย ref เดิมได้อย่างปลอดภัย
3. หน้าออเดอร์ของลูกค้า: แสดง username/password (มีปุ่มคัดลอก) + ปุ่ม "ขอโค้ด Steam Guard" ที่ให้กรอกเหตุผลก่อนขอ (เปิดรอบ) จากนั้นภายใน 60 วิ auto-refresh โค้ดจาก endpoint เดิม (ไม่ต้อง reason) แสดงนับถอยหลัง valid_for_sec + หน้าต่างเหลือ window.expires_in_sec และจำนวนรอบคงเหลือ (max 3)
4. จัดการ error ทุก code ข้างต้นเป็นข้อความภาษาไทยที่ลูกค้าเข้าใจง่าย โดยเฉพาะ OUT_OF_STOCK และ INSUFFICIENT_BALANCE (อันหลังคือเงินของ "เรา" ในร้านต้นทางหมด — แจ้งแอดมินเว็บเรา ไม่ใช่ลูกค้า)
5. เก็บ log การเรียก API ฝั่งเราไว้ตรวจสอบย้อนหลัง
เริ่มจากถามเทคโนโลยีที่ฉันใช้ (เช่น Next.js/PHP/Node) แล้วเขียนโค้ดให้ครบทุกส่วนจัดการคีย์และดูเรทได้ที่หน้า API Store — ติดปัญหาติดต่อทีมงานได้เลย