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 หรือ failed
dataobject | nullผลสำเร็จแบบ sync, บริบทเมื่อ not_found หรือ null เมื่อ failed
errorobjectมีเฉพาะ failed: code และ message ที่ปลอดภัย
timestampstringเวลา ISO-8601 ที่สร้าง webhook

ตรวจสอบล้มเหลว ​

json
{
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "batchId": null,
  "status": "failed",
  "data": null,
  "error": {
    "code": "API_SERVER_ERROR",
    "message": "External API service is temporarily unavailable"
  },
  "timestamp": "2026-09-11T00:00:00.000Z"
}

งานที่รับเข้าคิวแล้วแต่ตรวจต่อไม่ได้จะส่ง failed เช่น ข้อจำกัดบริการ, QUOTA_EXCEEDED, VALIDATION_ERROR และ API_SERVER_ERROR ส่วน error ที่ไม่รู้จักใช้ INTERNAL_SERVER_ERROR โดยไม่เปิดเผย exception ภายใน คำขอที่ถูกปฏิเสธก่อนรับเข้าคิวจะไม่มี callback

failed callback ใช้ signature และ retry ส่งแบบเดียวกับผลอื่น ส่งสำเร็จแล้ว polling ยังเป็น failed ถ้าส่งไม่สำเร็จจนครบรอบจะคง error.code/message ต้นเหตุไว้ และเพิ่ม error.webhook: "failed" พร้อมรายละเอียดการส่ง

การ 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