Skip to content

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:

HeaderDescription
Content-Typeapplication/json
X-Webhook-SignatureHMAC SHA-256 signature สำหรับตรวจสอบความถูกต้อง
X-Webhook-Eventชื่อ event เช่น order.completed
X-Webhook-TimestampUnix timestamp ของเวลาเกิด event
X-Webhook-DeliveryUnique ID ของ delivery นี้

Signature Verification

GameSO ใช้ HMAC SHA-256 เพื่อ sign webhook payload ช่วยให้ Merchant ตรวจสอบได้ว่า request มาจาก GameSO จริงๆ

วิธีตรวจสอบ

  1. รับ X-Webhook-Signature header
  2. คำนวณ HMAC SHA-256 ของ raw request body ด้วย webhook secret ของคุณ
  3. เปรียบเทียบ 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}), 200
php
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 ควร:

  1. บันทึก X-Webhook-Delivery header ไว้
  2. ตรวจสอบว่า delivery ID นี้เคยประมวลผลแล้วหรือยัง
  3. ถ้าเคยแล้ว ให้ตอบกลับ 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_USERUser ID ไม่ถูกต้องหรือไม่พบในระบบ

GameSO API Documentation