คู่มือ API
การเรียกใช้งานจริงต้องมี API key — สมัครและรออนุมัติก่อน
สมัครใช้งาน APIภาพรวม
REST API สำหรับดึงสินค้า ID Offline สั่งซื้อ และรับข้อมูลบัญชีอัตโนมัติ ทุก endpoint รับ-ส่ง JSON และตัดเงินจากยอดเงินในบัญชี store ของคุณตามเรทที่ได้รับ
https://store.499k-network.com/api/v1เรียกใช้จากฝั่ง server ของคุณเท่านั้น — ห้ามฝัง API key ในหน้าเว็บหรือแอปฝั่งลูกค้าเด็ดขาด
การยืนยันตัวตน
สร้างคีย์ได้ที่หน้า 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_) | |
|---|---|---|
| สินค้า | สินค้าทดสอบ 1 ตัว — type sandbox product_id 999001 | สินค้าจริงทั้งหมด |
| การตัดเงิน | ยอดจำลอง ไม่ตัดเงินจริง | ตัดจากยอดเงินในบัญชี |
| เงื่อนไข | ใช้ได้ทันทีหลังสมัคร | ต้องอนุมัติ + ยอดเติมสะสมถึงขั้นต่ำ |
| เลขออเดอร์ | ขึ้นต้น 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 | สินค้าหมดชั่วคราว |
INSUFFICIENT_BALANCE | 400 | ยอดเงินไม่พอ (ดู required / balance) |
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 และอาจเพิ่มประเภทใหม่ในอนาคต (ดูรายการได้จาก field types)
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",
"genres": ["Action", "Adventure", "Indie"],
"platforms": { "windows": true, "mac": true, "linux": true }
}
}
],
"total": 1,
"types": ["offline"]
}
}ตัวอย่าง field steam ตอนใส่ ?expand=steam
"steam": {
"name": "Hollow Knight: Silksong",
"genres": ["Action", "Adventure", "Indie"],
"platforms": { "windows": true, "mac": true, "linux": true },
"categories": ["Single-player", "Steam Achievements", "Full controller support"],
"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 หรือยังไม่มีข้อมูล
// (ปัจจุบัน 242 จาก 244 เกมมีข้อมูล) ต้องเช็คก่อนใช้เสมอ
// description / pc_requirements เป็น HTML ดิบจาก Steam ต้อง sanitize ก่อน render
// description_th มี 238 จาก 242 เกม ถ้า 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"
}
}
}POST
/api/v1/ordersสั่งซื้อสินค้า — ตัดเงินจากยอดเงินของคุณและได้ข้อมูลบัญชีทันที ต้องส่ง ref (เลขอ้างอิงฝั่งคุณ ไม่ซ้ำ, case-sensitive) เพื่อกันการสั่งซ้ำ: สั่งใหม่สำเร็จได้ HTTP 201 — ยิงซ้ำด้วย ref เดิมได้ HTTP 200 พร้อม idempotent:true โดยไม่ตัดเงินอีก (response แบบ 200 จะไม่มี balance_after) ดังนั้นให้เช็คผลจาก json.success เสมอ อย่าอิงจาก HTTP status code
ตัวอย่าง 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
}
}AI 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, ?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 }], total, types }
- price คือราคาที่เราต้องจ่ายจริง (หักเรทแล้ว) — เว็บของเราควรบวกกำไรเองก่อนแสดงขายต่อ
- 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" — อาจมีประเภทใหม่เพิ่มในอนาคต ดูได้จาก field types)
- product_id ของ type offline คือ appid ของเกม (string ตัวเลข)
- steam คือข้อมูลเกมจาก Steam ไว้ทำหน้าสินค้าเอง — เป็น null ได้ (เกมที่ไม่ใช่ platform steam หรือยังไม่มีข้อมูล ~2 จาก 244 เกม) ต้องเช็คก่อนใช้เสมอ
ปกติได้ชุดเบา: { name, genres:["Action","Indie"], platforms:{windows,mac,linux} } — เอาไปทำแท็ก/ตัวกรองได้เลย
ใส่ ?expand=steam จะได้ชุดเต็มเพิ่ม: { categories:[...], screenshots:["url",...], pc_requirements:{minimum,recommended}, 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"
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 ตามปกติ (ของเดิมไม่กระทบ)
5) GET https://store.499k-network.com/api/v1/orders?page=1&limit=20
รายการออเดอร์ (ไม่มีข้อมูลบัญชี): data = { orders:[{order_no, type, product_id, game_title, price, rate_percent, price_source, status, code_requests_used, ref, created_at}], pagination:{page,limit,total} }
6) GET https://store.499k-network.com/api/v1/orders/{order_no}
รายละเอียด + ข้อมูลบัญชี: data = { ...เหมือนตอนสั่งซื้อ, account:{username,password}, code_requests:{used,max,history:[{reason,created_at}]} }
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 คิวว่างเสมอ
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), 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 ของเราเอง + cache สั้น ๆ ประมาณ 1 นาที) แสดงรูป ชื่อเกม ราคาขาย (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 — ติดปัญหาติดต่อทีมงานได้เลย