Skip to content

Webhooks

เมื่อคุณส่งสลิปเข้าตรวจสอบแบบ async ผลลัพธ์จะถูกส่งมาที่ server ของคุณผ่าน webhook — HTTP POST ไปยัง URL ที่คุณควบคุม หน้านี้อธิบาย payload, วิธี verify signature และพฤติกรรมการส่ง/retry

การตั้งค่า callback URL

ปลายทางถูก resolve ตามลำดับนี้:

  1. field callbackUrl ใน request body ของ async ถ้ามี
  2. ถ้าไม่มี ใช้ webhook URL ที่ตั้งไว้ที่ branch

ถ้าไม่มีทั้งคู่ request แบบ async จะถูกปฏิเสธด้วย VALIDATION_ERROR URL ต้องเป็น HTTPS และ resolve ไปยัง address สาธารณะ (URL ที่ชี้ไป IP ภายใน/ส่วนตัวจะถูกปฏิเสธ)

แต่ละ branch อาจมี webhook secret ด้วย เมื่อมี secret ทุก webhook จะถูก sign (ดูการ verify signature)

Payload

body ของ webhook เป็น JSON:

json
{
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "batchId": null,
  "status": "success",
  "data": {
    "isDuplicate": false,
    "rawSlip": {
      "transRef": "68370160657749I376388B35",
      "amount": { "amount": 1500.00, "local": { "amount": 1500.00, "currency": "THB" } },
      "sender": { "bank": { "id": "004", "short": "KBANK" } },
      "receiver": { "bank": { "id": "014", "short": "SCB" } }
    }
  },
  "timestamp": "2024-01-15T14:30:00.000Z"
}
Fieldประเภทคำอธิบาย
jobIdstringjob ที่ผลลัพธ์นี้เป็นของ
batchIdstring | nullbatch ที่ job นี้อยู่ หรือ null เมื่อเป็น async แบบเดี่ยว
statusstringsuccess (ตรวจสอบสลิปได้) หรือ not_found (ตรวจสอบสลิปไม่ได้)
dataobjectผลการตรวจสอบ — shape เดียวกับ data ของsync verify กรณี not_found จะมีรายละเอียด error
timestampstringเวลา ISO-8601 ที่สร้าง webhook

การ verify signature

เมื่อ branch มี webhook secret request จะมี header:

http
X-EasySlip-Signature: sha256=<hex>

<hex> คือ HMAC-SHA256 ของ raw request body (bytes ที่ได้รับจริง — อย่า re-serialize) โดยใช้ webhook secret เป็น key แล้ว encode เป็น hex ให้คำนวณใหม่แล้วเทียบ ด้วยการเปรียบเทียบแบบ constant-time ถ้า signature ไม่ตรงให้ปฏิเสธ request

javascript
import crypto from 'crypto';

// ตัวอย่าง Express — เก็บ RAW body: express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })
function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signatureHeader || '', 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/easyslip', (req, res) => {
  if (!verifyWebhook(req.rawBody, req.get('X-EasySlip-Signature'), process.env.EASYSLIP_WEBHOOK_SECRET)) {
    return res.status(401).send('invalid signature');
  }
  const event = JSON.parse(req.rawBody.toString('utf8'));
  // ... จัดการ event.jobId / event.status / event.data
  res.sendStatus(200);
});
typescript
import crypto from 'crypto';

function verifyWebhook(rawBody: Buffer, signatureHeader: string | undefined, secret: string): boolean {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signatureHeader ?? '', 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
php
function verifyWebhook(string $rawBody, ?string $signatureHeader, string $secret): bool
{
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return is_string($signatureHeader) && hash_equals($expected, $signatureHeader);
}

// การใช้งาน
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_EASYSLIP_SIGNATURE'] ?? null;
if (!verifyWebhook($rawBody, $signature, getenv('EASYSLIP_WEBHOOK_SECRET'))) {
    http_response_code(401);
    exit('invalid signature');
}
$event = json_decode($rawBody, true);
python
import hmac
import hashlib

def verify_webhook(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return signature_header is not None and hmac.compare_digest(expected, signature_header)

# ตัวอย่าง Flask
@app.post('/webhooks/easyslip')
def easyslip_webhook():
    raw = request.get_data()  # raw bytes
    if not verify_webhook(raw, request.headers.get('X-EasySlip-Signature'), os.environ['EASYSLIP_WEBHOOK_SECRET']):
        return 'invalid signature', 401
    event = request.get_json()
    # ... จัดการ event
    return '', 200

ใช้ raw body

signature คำนวณจาก bytes ที่ส่งจริง ถ้าคุณ verify กับ JSON object ที่ serialize ใหม่ (สลับลำดับ key, เปลี่ยน whitespace) signature จะไม่ตรง ให้ sign จาก raw request body เสมอ

การส่ง & retry

  • การส่ง webhook สำเร็จเมื่อได้ response 2xx ให้ตอบเร็ว (งานหนักทำแบบ async) และคืน 2xx ทันทีที่รับ event แล้ว
  • เมื่อได้ non-2xx, timeout หรือ network error จะ retry สูงสุด 3 ครั้ง ด้วย backoff ประมาณ 10 วิ, 30 วิ, 2 นาที (1 ครั้งแรก + 3 retry)
  • ไม่ตาม redirect — ให้ชี้ callbackUrl ไปที่ปลายทางสุดท้าย
  • jobId คงที่ทุก retry การส่งจึงเป็น idempotent — ให้ dedupe ด้วย jobId และถือเป็น at-least-once
  • ถ้าทุกครั้งล้มเหลว ผลลัพธ์ยังดึงได้ผ่าน GET /verify/bank/jobs/:jobId จนกว่า job จะหมดอายุ (7 วัน)

หมายเหตุ

  • verify signature ก่อนเชื่อ webhook เสมอ (เมื่อมี secret)
  • คืน 2xx เร็ว ๆ handler ที่ช้าเสี่ยง timeout และเกิด retry โดยไม่จำเป็น
  • ใช้ jobId (และ batchId สำหรับ batch) จับคู่ผลลัพธ์กับ request เดิม

Bank Slip Verification API for Thai Banking