Appearance
Webhook Payload & Signature
Webhook Payload
เมื่อ order เปลี่ยนสถานะ GameSO จะส่ง POST request ไปยัง Webhook URL ของ Merchant พร้อม payload ดังนี้:
order.completed
json
{
"event": "order.completed",
"timestamp": 1771834095,
"data": {
"order_number": "ord_Np3O7rcsqNmBTwD7",
"reference_id": "ref_RLUFNUHGXXX",
"user_id": "Fiona.DAmore-Bauch",
"server_id": null,
"character_id": null,
"status": "completed",
"failure_code": null,
"failure_message": null,
"subtotal_amount": "101.00",
"discount_amount": "0.00",
"total_amount": "101.00",
"created_at": "2026-02-23T08:07:44.000Z",
"updated_at": "2026-02-23T08:08:15.000Z",
"source_type": "api",
"created_by": {
"id": 0,
"name": "API"
},
"completed_at": "2026-02-23T08:08:15.000Z",
"items": [
{
"sku": "ROMC-Z-1800K",
"quantity": 1,
"completed_quantity": 1,
"failed_quantity": 0,
"subtotal_amount": "101.00",
"discount_amount": "0.00",
"total_amount": "101.00",
"status": "completed",
"failure_code": null,
"failure_message": null
}
]
}
}order.failed
json
{
"event": "order.failed",
"timestamp": 1771834095,
"data": {
"order_number": "ord_xyz789",
"reference_id": "ref_xyz789",
"user_id": "user_002",
"server_id": null,
"character_id": null,
"status": "failed",
"failure_code": "ITEM_NOT_FOUND",
"failure_message": "Product SKU not found in provider system",
"subtotal_amount": "100.00",
"discount_amount": "0.00",
"total_amount": "100.00",
"created_at": "2026-02-23T08:07:44.000Z",
"updated_at": "2026-02-23T08:08:15.000Z",
"source_type": "api",
"created_by": {
"id": 0,
"name": "API"
},
"items": [
{
"sku": "DIAMOND_999",
"quantity": 1,
"completed_quantity": 0,
"failed_quantity": 1,
"subtotal_amount": "100.00",
"discount_amount": "0.00",
"total_amount": "100.00",
"status": "failed",
"failure_code": "ITEM_NOT_FOUND",
"failure_message": "Product SKU not found in provider system"
}
]
}
}Webhook Headers
GameSO ส่ง headers เหล่านี้พร้อมกับทุก webhook request:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | HMAC SHA-256 signature สำหรับตรวจสอบความถูกต้อง |
X-Webhook-Event | ชื่อ event เช่น order.completed |
X-Webhook-Timestamp | Unix timestamp ของเวลาเกิด event |
X-Webhook-Delivery | Unique ID ของ delivery นี้ |
Signature Verification
GameSO ใช้ HMAC SHA-256 เพื่อ sign webhook payload ช่วยให้ Merchant ตรวจสอบได้ว่า request มาจาก GameSO จริงๆ
วิธีตรวจสอบ
- รับ
X-Webhook-Signatureheader - คำนวณ HMAC SHA-256 ของ raw request body ด้วย webhook secret ของคุณ
- เปรียบเทียบ signature ที่ได้กับ header
signature = "sha256=" + HMAC_SHA256(raw_body, webhook_secret)สำคัญมาก
ต้องตรวจสอบ signature ทุกครั้ง ก่อนประมวลผล webhook payload เพื่อป้องกัน request ปลอม
ตัวอย่างการตรวจสอบ
javascript
const crypto = require("crypto");
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
// ใช้ timingSafeEqual เพื่อป้องกัน timing attack
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
// ใน Express handler
app.post(
"/webhooks/gameso",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-webhook-signature"];
const rawBody = req.body.toString("utf8");
if (
!verifyWebhookSignature(
rawBody,
signature,
process.env.GAMESO_WEBHOOK_SECRET,
)
) {
return res.status(401).json({ error: "Invalid signature" });
}
const data = JSON.parse(rawBody);
// ประมวลผล...
res.status(200).json({ received: true });
},
);python
import hmac
import hashlib
def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# ใช้ compare_digest เพื่อป้องกัน timing attack
return hmac.compare_digest(signature, expected)
# ใน Flask handler
@app.route('/webhooks/gameso', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature', '')
raw_body = request.get_data(as_text=True)
if not verify_webhook_signature(raw_body, signature, os.environ['GAMESO_WEBHOOK_SECRET']):
return jsonify({'error': 'Invalid signature'}), 401
data = request.json
# ประมวลผล...
return jsonify({'received': True}), 200php
function verifyWebhookSignature(string $payload, string $signature, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}
// ใน webhook handler
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
if (!verifyWebhookSignature($rawBody, $signature, getenv('GAMESO_WEBHOOK_SECRET'))) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$data = json_decode($rawBody, true);
// ประมวลผล...
http_response_code(200);
echo json_encode(['received' => true]);ข้อควรระวัง
ต้องใช้ raw request body (ก่อน parse JSON) ในการคำนวณ signature ถ้าใช้ parsed JSON อาจทำให้ signature ไม่ตรงกัน
Idempotency
Webhook อาจถูกส่งซ้ำในกรณีที่ระบบ retry ดังนั้น Merchant ควร:
- บันทึก
X-Webhook-Deliveryheader ไว้ - ตรวจสอบว่า delivery ID นี้เคยประมวลผลแล้วหรือยัง
- ถ้าเคยแล้ว ให้ตอบกลับ
200โดยไม่ต้องประมวลผลซ้ำ
javascript
// ตัวอย่าง idempotency check
app.post("/webhooks/gameso", async (req, res) => {
const deliveryId = req.headers["x-webhook-delivery"];
// ตรวจสอบว่าเคยประมวลผลแล้วหรือยัง
const alreadyProcessed = await db.webhookDeliveries.findOne({ deliveryId });
if (alreadyProcessed) {
return res.status(200).json({ received: true, duplicate: true });
}
// บันทึก delivery ID
await db.webhookDeliveries.create({ deliveryId, processedAt: new Date() });
// ประมวลผล...
res.status(200).json({ received: true });
});Failure Codes
| Code | ความหมาย |
|---|---|
ITEM_NOT_FOUND | ไม่พบ SKU ในระบบ Provider |
INSUFFICIENT_BALANCE | ยอดเงินไม่เพียงพอ |
PROVIDER_ERROR | ข้อผิดพลาดจาก Provider |
TIMEOUT | หมดเวลาการประมวลผล |
INVALID_USER | User ID ไม่ถูกต้องหรือไม่พบในระบบ |
