Webhook และการส่งมอบของ
เมื่อขายสำเร็จ EZ2Pay จะส่ง POST ที่ลงนามแล้วไปยัง Webhook URL ของเกม บอกว่า SKU ไหนขายได้ ใครซื้อ และสำเร็จแล้ว
สิ่งที่ส่งมา
POST /ez2pay/webhook HTTP/1.1
Host: api.mygame.example
Content-Type: application/json
EZ2Pay-Timestamp: 1794200000
EZ2Pay-Signature: v1=b74045c949f1156897816ae225c88dc346d0c9028743493b164b959d5782f01a
{"order_id":"ord_00000000-0000-4000-8000-000000000001","sku":"gem_pack_100","buyer":"p_123","status":"success"}
มีข้อความอีก 2 แบบที่ห้ามให้ของ:
| ข้อความ | body | ความหมาย |
|---|---|---|
| Ping | {"type":"ping"} | ทดสอบจากปุ่มทดสอบ Webhook ตรวจลายเซ็นแล้วตอบ 200 |
| Voided | "status":"voided" | Order ถูกยกเลิก ตรวจลายเซ็น ไม่ให้ของ แล้วตอบ 200 |
ตรวจลายเซ็น
ใครก็เรียก URL ของคุณได้ ต้องตรวจทุกข้อความก่อนเชื่อ
- อ่าน raw body ก่อน ห้าม parse แล้วแปลงกลับ เพราะลายเซ็นครอบคลุมทุก byte
- คำนวณ HMAC-SHA256 ด้วย Webhook secret จาก
{EZ2Pay-Timestamp}.{raw body}แล้วเขียนเป็นเลขฐานสิบหกตัวพิมพ์เล็ก - เทียบ
v1=กับค่านั้นกับ headerEZ2Pay-Signatureด้วยวิธี timing-safe - ปฏิเสธถ้า
EZ2Pay-Timestampห่างจากเวลาปัจจุบันเกิน 300 วินาที - ปฏิเสธ body ที่ยาวเกิน 16 KiB
Node
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody คือ Buffer หรือ string ที่เป็น body ดิบของ request
function verifyEz2paySignature(secret, timestamp, signature, rawBody, nowMs = Date.now()) {
if (!timestamp || !/^[1-9]\d{8,11}$/.test(timestamp)) return false;
if (!signature || !/^v1=[a-f0-9]{64}$/.test(signature)) return false;
if (Math.abs(Math.floor(nowMs / 1000) - Number(timestamp)) > 300) return false;
const digest = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(`v1=${digest}`));
}
Python
import hashlib
import hmac
import re
import time
# raw_body คือ body ดิบของ request เป็น bytes
def verify_ez2pay_signature(secret, timestamp, signature, raw_body, now=None):
if not timestamp or not re.fullmatch(r"[1-9][0-9]{8,11}", timestamp):
return False
if not signature or not re.fullmatch(r"v1=[a-f0-9]{64}", signature):
return False
current = int(time.time() if now is None else now)
if abs(current - int(timestamp)) > 300:
return False
signed = timestamp.encode("ascii") + b"." + raw_body
digest = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, "v1=" + digest)
ตรวจโค้ดของคุณในภาษาอื่น
โค้ดของคุณต้องได้ลายเซ็นนี้จากค่าเหล่านี้พอดี timestamp นี้เป็นค่าเก่าโดยตั้งใจ ตอนทดสอบให้คำนวณ HMAC ตรงๆ
secret: ez2pay-docs-example-secret-not-a-real-one-00
timestamp: 1794200000
body: {"order_id":"ord_00000000-0000-4000-8000-000000000001","sku":"gem_pack_100","buyer":"p_123","status":"success"}
signature: v1=b74045c949f1156897816ae225c88dc346d0c9028743493b164b959d5782f01a
ถามกลับก่อนให้ของ
ลายเซ็นที่ถูกต้องบอกแค่ว่า EZ2Pay แจ้งว่าขายได้ ก่อนให้ของต้องอ่าน Order:
curl https://api.ez2pay.gg/v1/orders/ord_00000000-0000-4000-8000-000000000001 \
-H "Authorization: Bearer $EZ2PAY_API_KEY"
ให้ของเมื่อครบทุกข้อนี้เท่านั้น:
order_id,skuและbuyerตรงกับ Webhook ทุกตัวstatusเป็นPAID,NOTIFYING,NOTIFIED,NOTIFY_FAILEDหรือDELIVERED- มี
paid_atและเป็นวันที่ที่ถูกต้อง currencyเป็นTHBและamountเป็นสตริงทศนิยม 2 ตำแหน่ง
ถ้าข้อใดไม่ผ่าน ให้ตอบ 409 และห้ามให้ของ
ห้ามเทียบ amount กับราคาที่เก็บไว้ในเกมของคุณ เพราะ amount คือราคาเต็มที่ผู้เล่นจ่าย และราคาเปลี่ยนใน Dashboard ได้หลังจากเกมสร้างเสร็จแล้ว
ให้ของครั้งเดียว
Webhook อาจมาซ้ำ ต้องเก็บ order_id ใน transaction เดียวกับการให้ของ ห้ามเก็บสถานะในหน่วยความจำ
CREATE TABLE fulfillments (
order_id text PRIMARY KEY,
sku text NOT NULL,
buyer text NOT NULL,
granted_at timestamptz NOT NULL DEFAULT now()
);
BEGIN;
INSERT INTO fulfillments (order_id, sku, buyer)
VALUES ($1, $2, $3)
ON CONFLICT (order_id) DO NOTHING
RETURNING order_id;
-- ได้แถวกลับมา 1 แถว: เป็นการส่งครั้งแรก ให้ของที่นี่ใน transaction เดียวกัน
-- ไม่ได้แถวกลับมา: เคยให้แล้ว อ่าน sku และ buyer ที่เก็บไว้
-- ถ้าไม่ตรงกับ event นี้ ให้ ROLLBACK แล้วตอบ error
COMMIT;
ห้ามลบหรือทำให้แถวเหล่านี้หมดอายุ
แจ้งส่งของแล้ว
หลังให้ของ รวมถึงกรณีที่เคยให้แล้ว ให้แจ้ง EZ2Pay:
curl -X POST https://api.ez2pay.gg/v1/orders/ord_00000000-0000-4000-8000-000000000001/delivered \
-H "Authorization: Bearer $EZ2PAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
- ใช้ได้เมื่อ Order เป็น
NOTIFYING,NOTIFIEDหรือDELIVEREDนอกนั้นได้409 ORDER_NOT_NOTIFIED - เรียกซ้ำได้
- ห้ามเรียกก่อนให้ของจริง นี่คือหลักฐานการส่งมอบ
ตอบ HTTP อย่างไร
นับว่าสำเร็จเฉพาะ HTTP 200 ส่วน 201, 204, redirect หรือไม่ตอบภายใน 10 วินาที นับว่าไม่สำเร็จ
| สถานการณ์ | ตอบ |
|---|---|
| ให้ของและแจ้งส่งของแล้ว หรือเคยให้แล้ว | 200 |
| Ping หรือ voided ที่ลายเซ็นถูกต้อง | 200 |
| ลายเซ็นหรือเวลาผิด | 401 |
| ข้อความผิดรูปแบบ | 400 |
| Order ยังไม่ยืนยันว่าจ่ายแล้ว | 409 |
| เซิร์ฟเวอร์หรือฐานข้อมูลของคุณล้มเหลว | 503 |
เมื่อไม่สำเร็จ EZ2Pay จะลองใหม่หลัง 1 นาที 5 นาที 15 นาที 1 ชั่วโมง 6 ชั่วโมง และ 24 ชั่วโมง รวม 7 ครั้ง จากนั้น Order เป็น NOTIFY_FAILED และกดส่งซ้ำใน Dashboard ได้ ข้อความ voided ลองใหม่ตามรอบเดียวกัน แต่ Order ยังเป็น VOIDED และกดส่งซ้ำใน Dashboard ไม่ได้
ตอบ 200 เมื่อทุกอย่างเสร็จจริงเท่านั้น ถ้าแจ้งส่งของแล้วไม่สำเร็จ ให้ตอบ 503 รอบถัดไปจะเห็นว่าเคยให้ของแล้วและแจ้งซ้ำโดยไม่ให้ของเพิ่ม
เปลี่ยน Webhook secret
สร้าง secret ใหม่เมื่อทำหาย secret รั่ว หรือให้ AI สร้างเกมผ่าน MCP เพราะกรณีนี้ไม่มีใครเห็น secret
- เปิดหน้าเกมใน Dashboard กด
สร้าง secret ใหม่แล้วยืนยัน - คัดลอก secret ใหม่ ระบบแสดงครั้งเดียว
- ใส่ในตัวแปรสภาพแวดล้อมของเซิร์ฟเวอร์ แล้วรีสตาร์ตหรือ deploy ใหม่
secret เดิมใช้ไม่ได้ทันทีที่กดยืนยัน Webhook ทุกครั้งหลังจากนั้นจะลงนามด้วย secret ใหม่ รวมถึง ping และการส่งซ้ำของการขายก่อนหน้า ระหว่างที่เซิร์ฟเวอร์ยังไม่เปลี่ยน เซิร์ฟเวอร์จะตอบ 401 และ EZ2Pay จะลองใหม่ตามรอบด้านบน จึงควรเปลี่ยนทันที
ทำได้เฉพาะคนใน Dashboard เครื่องมือ MCP และ AI อ่านหรือเปลี่ยน secret ไม่ได้
ต่อไป: เชื่อมต่อด้วย HTTP