Skip to content

Webhook Callback ​

เมื่อ Job การตรวจสอบแบบ async เสร็จสิ้น EasySlip จะส่งผลลัพธ์ไปยัง callbackUrl ของคุณเป็น HTTP POST หน้านี้อธิบาย payload ของ callback, ลายเซ็นที่คุณต้องตรวจสอบ และพฤติกรรมการ retry

การตรวจสอบแบบ async รองรับ สลิปธนาคารเท่านั้น เมื่อ success ฟิลด์ data ตรงกับ Response ของ POST /verify/bank แบบ sync เมื่อ failed จะมี data: null และ error อธิบายสาเหตุ

การส่ง (Delivery) ​

  • 1 ผลลัพธ์ต่อ 1 สลิป แต่ละสลิป รวมถึงใน batch มี callback ของตัวเอง การ retry ส่งอาจทำให้ได้รับ callback เดิมซ้ำ
  • Method: POST พร้อม Content-Type: application/json
  • ปลายทาง: callbackUrl จากคำขอ หรือ Default Webhook URL ของ branch หากไม่ได้ระบุมา
  • ปลายทางของคุณควรตอบด้วยสถานะ 2xx ใดก็ได้ การตอบที่ไม่ใช่ 2xx (หรือ timeout) จะทำให้เกิดการ retry

Request Body ​

json
{
  "jobId": "3f2b1c8a-9d4e-4f10-b7a2-6c5d4e3f2a1b",
  "batchId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "status": "success",
  "data": {
    "remark": "Order #1001",
    "isDuplicate": false,
    "amountInSlip": 1500.00,
    "isAmountMatched": true,
    "rawSlip": {
      "payload": "00000000000000000000000000000000000000",
      "transRef": "68370160657749I376388B35",
      "date": "2024-01-15T14:30:00+07:00",
      "countryCode": "TH",
      "amount": {
        "amount": 1500.00,
        "local": { "amount": 1500.00, "currency": "THB" }
      },
      "fee": 0,
      "ref1": "",
      "ref2": "",
      "ref3": "",
      "sender": {
        "bank": { "id": "004", "name": "กสิกรไทย", "short": "KBANK" },
        "account": {
          "name": { "th": "นาย ผู้โอน ทดสอบ", "en": "MR. SENDER TEST" },
          "bank": { "type": "BANKAC", "account": "123-4-xxxxx-5" }
        }
      },
      "receiver": {
        "bank": { "id": "014", "name": "ไทยพาณิชย์", "short": "SCB" },
        "account": {
          "name": { "th": "บริษัท ตัวอย่าง จำกัด" },
          "bank": { "type": "BANKAC", "account": "xxx-x-x5678-x" }
        },
        "merchantId": null
      }
    }
  },
  "timestamp": "2024-01-15T14:32:05+07:00"
}

ฟิลด์ ​

ฟิลด์ประเภทคำอธิบาย
jobIdstringUUID ของ Job (ตรงกับ jobId ที่คุณได้ตอนส่งเข้าคิว)
batchIdstring | nullUUID ของ batch หากสลิปเป็นส่วนหนึ่งของ batch; เป็น null หากไม่ใช่
statusstringsuccess, not_found หรือ failed (ดูด้านล่าง)
dataobject | nullผลตรวจแบบ sync เมื่อสำเร็จ; บริบทเมื่อ not_found; null เมื่อ failed
errorobjectมีเฉพาะ failed: code และ message ที่ไม่เปิดเผยข้อมูลภายใน
timestampstringเวลา ISO 8601 ที่สร้างผลลัพธ์

ค่าของ status ​

statusความหมายdata
successตรวจสอบสลิปสำเร็จผลการตรวจสอบเต็มรูปแบบ
not_foundยังไม่พบสลิปหลัง retry ครบแล้วบริบทการตรวจ ไม่มีสลิปที่ยืนยันแล้ว
failedตรวจต่อไม่ได้เนื่องจาก error หรือข้อจำกัดบัญชี/บริการnull; ดู error

Type definition

typescript
type WebhookPayload = {
  jobId: string;
  batchId: string | null;
  timestamp: string; // ISO 8601
} & (
  | { status: 'success'; data: VerifyBankData }
  | { status: 'not_found'; data: unknown }
  | { status: 'failed'; data: null; error: { code: string; message: string } }
);

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

json
{
  "jobId": "3f2b1c8a-9d4e-4f10-b7a2-6c5d4e3f2a1b",
  "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"
}

ตัวอย่าง code ได้แก่ QUOTA_EXCEEDED, SERVICE_EXPIRED, BRANCH_INACTIVE, VALIDATION_ERROR, API_SERVER_ERROR และ RENEWAL_TEMPORARILY_UNAVAILABLE ส่วน error ที่ไม่รู้จักใช้ INTERNAL_SERVER_ERROR โดยไม่เปิดเผย exception ภายใน กรณี billing จะส่ง failed หลัง retry ของ billing ครบแล้วเท่านั้น ไม่ส่งระหว่าง retry

ส่ง failure callback สำเร็จแล้ว Job ยังเป็น failed ไม่เปลี่ยนเป็น done ถ้าส่ง callback ไม่สำเร็จจนหมดรอบ polling ยังคง error.code/message ต้นเหตุไว้ และเพิ่ม error.webhook: "failed" พร้อมรายละเอียดการส่ง HTTP error ที่ปฏิเสธคำขอ ก่อนรับเข้าคิว จะไม่มี callback

การตรวจสอบลายเซ็น (Signature Verification) ​

ทุก Webhook จะมี Header ลายเซ็นแนบมา คุณต้องตรวจสอบมัน เพื่อยืนยันว่า callback มาจาก EasySlip จริง

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

ลายเซ็นคือ HMAC-SHA256 ของ raw JSON request body โดยใช้ Webhook Secret ของ Branch เป็นกุญแจ เข้ารหัสเป็น hex

วิธีตรวจสอบ: คำนวณ HMAC-SHA256 ของ raw body ที่ได้รับ (byte ตามจริง ก่อนการ parse/serialize JSON ใหม่) ด้วย secret ของคุณ แล้วเปรียบเทียบ — โดยใช้การเปรียบเทียบแบบ constant-time — กับค่า hex ใน Header

ใช้ raw body

คำนวณ HMAC จาก byte ของ raw request body ไม่ใช่ออบเจกต์ที่ serialize ใหม่ การเข้ารหัส JSON ใหม่อาจเปลี่ยน whitespace/ลำดับ key และทำให้ลายเซ็นผิด จับ raw body ไว้ก่อน parse

javascript
import express from 'express';
import crypto from 'crypto';

const WEBHOOK_SECRET = process.env.EASYSLIP_WEBHOOK_SECRET;
const app = express();

// จับ RAW body ไว้เพื่อตรวจสอบลายเซ็น
app.post('/webhooks/easyslip',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.get('X-EasySlip-Signature') || '';
    const expected = 'sha256=' + crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(req.body)                 // req.body เป็น Buffer (raw bytes)
      .digest('hex');

    const ok =
      header.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));

    if (!ok) return res.status(401).send('invalid signature');

    const event = JSON.parse(req.body.toString('utf8'));
    // ... จัดการ event.jobId / event.status / event.data ...

    res.sendStatus(200);
  });
php
<?php
$secret = getenv('EASYSLIP_WEBHOOK_SECRET');
$raw    = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_EASYSLIP_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);

if (!hash_equals($expected, $header)) {
    http_response_code(401);
    exit('invalid signature');
}

$event = json_decode($raw, true);
// ... จัดการ $event['jobId'] / $event['status'] / $event['data'] ...

http_response_code(200);
python
import hmac, hashlib, os
from flask import Flask, request, abort

WEBHOOK_SECRET = os.environ["EASYSLIP_WEBHOOK_SECRET"].encode()
app = Flask(__name__)

@app.post("/webhooks/easyslip")
def easyslip_webhook():
    raw = request.get_data()                      # raw bytes
    header = request.headers.get("X-EasySlip-Signature", "")
    expected = "sha256=" + hmac.new(WEBHOOK_SECRET, raw, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, header):
        abort(401)

    event = request.get_json()
    # ... จัดการ event["jobId"] / event["status"] / event["data"] ...

    return "", 200

Retries ​

การ retry ตรวจสลิปและการส่ง callback แยกงบกัน:

  • ตรวจสลิป: not_found ทุกธนาคารและ BBL pending ใช้งบร่วมกัน ครั้งแรก + retry 4 ครั้ง เว้น 30 / 60 / 120 / 240 วินาที พบแล้วจบทันที ครบแล้วยังไม่พบส่ง not_found ไม่ใช่ failed เวลารอตามตารางรวม 7 นาที 30 วินาที ไม่รวมประมวลผล รอคิวและการเลื่อนอื่น ๆ จึงไม่ใช่เวลาจบงานสูงสุด

  • ส่ง callback: ทั้ง success, not_found และ failed ส่งครั้งแรก + retry 3 ครั้ง เว้น 10 / 30 / 120 วินาที ใช้กับทุกสถานะที่ไม่ใช่ 2xx รวม 4xx และ timeout/network error

  • แม้การส่ง Webhook จะล้มเหลวทั้งหมด ผลลัพธ์ยังดึงได้ผ่าน GET /verify/bank/jobs/:jobId นาน ~7 วัน

  • ทำ handler ให้ idempotent — การ retry อาจส่ง jobId เดิมมามากกว่าหนึ่งครั้ง กันซ้ำด้วย jobId

  • ตอบ 2xx เร็ว ๆ แล้วค่อยประมวลผลหนัก ๆ แบบ asynchronous เพื่อไม่ให้ timeout และกระตุ้น retry ที่ไม่จำเป็น

แนวทางที่แนะนำ ​

  1. ตรวจสอบลายเซ็น ทุกคำขอก่อนเชื่อถือ body
  2. ตอบ 2xx เร็ว ๆ แล้วค่อยประมวลผลนอกรอบ
  3. กันซ้ำด้วย jobId — ถือว่าการส่งเป็นแบบ at-least-once
  4. กระทบยอดด้วย polling — หากไม่ได้รับ Webhook ภายในช่วงเวลาที่คาดไว้ ให้เรียก GET .../jobs/:jobId
  5. เก็บ secret เป็นความลับ — จัดเก็บ Webhook Secret อย่างปลอดภัย อย่าเปิดเผยฝั่ง client

Bank Slip Verification API for Thai Banking