เมนูคู่มือ

Webhook

เมื่อผู้ใช้เปิดทิกเก็ตจากแชท Kavchat สามารถส่งทิกเก็ตไปยังระบบของคุณได้ทันที ไม่ว่าจะเป็นระบบบริการลูกค้า CRM หรือบอตใน Slack ทุกครั้งที่ส่งจะมีการลงลายเซ็นด้วยคีย์ลับของคุณ

webhook รวมอยู่ในแพ็กเกจ Premium

การตั้งค่า

  1. สร้างปลายทาง HTTPS บนเซิร์ฟเวอร์ที่รับ POST พร้อมเนื้อหาแบบ JSON
  2. ในแดชบอร์ด ไปที่การตั้งค่า แล้วกรอก URL ในส่วน Webhook
  3. ตรวจสอบลายเซ็นของทุกคำขอตามคำอธิบายด้านล่าง แล้วตอบกลับด้วยรหัสสถานะใดก็ได้ในกลุ่ม 2xx

URL ต้องขึ้นต้นด้วย https:// และชี้ไปยังที่อยู่สาธารณะ ที่อยู่ในเครือข่ายส่วนตัวจะถูกบล็อก

เนื้อหาข้อความ

Kavchat ส่งเหตุการณ์สองประเภท ได้แก่ ticket.created สำหรับทิกเก็ตที่ส่งผ่านแบบฟอร์มติดต่อ และ lead.created สำหรับข้อมูลที่ผู้เยี่ยมชมฝากไว้ในขั้นตอนเก็บลีดของสคริปต์ ฟิลด์ data มีข้อมูลทิกเก็ตหรือลีดในรูปแบบเดียวกับที่ Server API ส่งกลับ รวมถึงข้อความการสนทนา

HTTP
POST /kavchat/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Kavchat-Webhook/1
Kavchat-Signature: t=1792915200,v1=5f2b7c...e91a
JSON
{
  "type": "ticket.created",
  "data": {
    "id": "6718f0c2a4b5c6d7e8f90123",
    "number": 128,
    "status": "open",
    "name": "Dana",
    "email": "dana@example.com",
    "phone": "+972501234567",
    "userId": "user_123",
    "topicId": "billing",
    "topicName": "Billing",
    "scriptId": "returns",
    "sessionId": "k3J9xQ2mP7aB",
    "message": "My refund has not arrived yet.",
    "transcript": [
      { "from": "bot", "text": "Hi! How can I help?", "stepId": "start", "at": "2026-10-25T09:14:02.000Z" },
      { "from": "user", "text": "Returns", "at": "2026-10-25T09:14:05.000Z" }
    ],
    "createdAt": "2026-10-25T09:15:40.000Z",
    "updatedAt": "2026-10-25T09:15:40.000Z"
  }
}

userId คือค่าจากฟิลด์ sub ในโทเค็นระบุตัวตน หากระบุตัวตนผู้ใช้แล้ว มิฉะนั้นจะเป็น null ฟิลด์ name, email, phone และฟิลด์หัวข้อก็อาจเป็น null ได้เช่นกัน

ลีดและข้อมูลส่วนบุคคล

สคริปต์สามารถถามข้อมูลจากผู้เยี่ยมชม (ชื่อ เบอร์โทร อีเมล ตัวเลข หรือข้อความอิสระ) ตรวจสอบแต่ละคำตอบ และบันทึกไว้ในตัวแปร ขั้นตอนเก็บลีดจะส่งข้อมูลเหล่านี้ไปยังกล่องลีดในแดชบอร์ด ในเหตุการณ์ lead.created ฟิลด์ data.variables จะมีค่าทั้งหมดที่เก็บรวบรวมไว้ โดยใช้ชื่อตัวแปรเป็นคีย์ ค่าที่ใช้บ่อยจะปรากฏในฟิลด์ data.name, data.email และ data.phone ด้วย เบอร์โทรจะอยู่ในรูปแบบสากล (E.164)

ข้อมูลเหล่านี้เป็นข้อมูลส่วนบุคคล Kavchat เก็บข้อมูลไว้เฉพาะในลีด ทิกเก็ต หรือการสนทนากับเจ้าหน้าที่ที่เกี่ยวข้องเท่านั้น และจะไม่เก็บไว้ในรายงานหรือบันทึกระบบเด็ดขาด ลีดจะถูกลบเมื่อสิ้นสุดระยะเวลาเก็บรักษาที่คุณเลือก (30 วันในแพ็กเกจฟรี ส่วน Premium สามารถเลือกเก็บไว้โดยไม่ลบได้ด้วย) คุณสามารถลบลีดแต่ละรายการได้ทุกเมื่อ หากเก็บข้อมูลส่วนบุคคล โปรดระบุไว้ในนโยบายความเป็นส่วนตัว และใช้ข้อความขอความยินยอมในขั้นตอนเก็บลีดเพื่ออธิบายให้ผู้เยี่ยมชมทราบว่าจะนำข้อมูลไปใช้อย่างไร

เฮดเดอร์ Kavchat-Signature

เฮดเดอร์มีรูปแบบดังนี้: t=<unix seconds>,v1=<hex> โดย:

  • t คือเวลาที่ส่งในหน่วยวินาทีแบบ Unix
  • v1 คือ HMAC-SHA256(secretKey, "<t>.<raw body>") ที่เข้ารหัสเป็นเลขฐานสิบหก โดยใช้คีย์ลับของคุณแบบเต็ม

ในการตรวจสอบ ให้คำนวณ HMAC เดียวกันจากเนื้อหาดิบตามที่ได้รับโดยไม่เปลี่ยนแปลง เปรียบเทียบแบบใช้เวลาคงที่ และปฏิเสธคำขอที่เก่ากว่า 5 นาที เนื้อหาที่ผ่านการแยกวิเคราะห์แล้วแปลงกลับเป็น JSON จะไม่ตรงกับลายเซ็น

ตรวจสอบด้วย SDK

verifyWebhook ทำทั้งหมดนี้และส่งกลับเหตุการณ์ที่แยกวิเคราะห์แล้ว หากไม่สำเร็จ จะโยนข้อผิดพลาดพร้อมรหัส: malformed_header, timestamp_out_of_range, invalid_signature หรือ invalid_payload ให้ตอบกลับด้วย HTTP 400 สำหรับข้อผิดพลาดเหล่านี้ ดูวิธีติดตั้ง SDK ได้ที่การระบุตัวตนผู้ใช้

Node.js
import express from 'express';
import { Kavchat, WebhookVerificationError } from '@kavchat/node';

const kavchat = new Kavchat(process.env.KAVCHAT_SECRET_KEY!);

// Use the raw body. A body that was parsed and serialized again will not match the signature.
app.post('/kavchat/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = kavchat.verifyWebhook(req.body, req.get('Kavchat-Signature'));
    if (event.type === 'ticket.created') console.log('New ticket', event.data.number);
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.status(400).send(err.code);
    throw err;
  }
});
Python
from flask import Flask, request
from kavchat import WebhookVerificationError

app = Flask(__name__)

@app.post("/kavchat/webhook")
def webhook():
    try:
        # raw bytes: request.get_data() in Flask, request.body in Django
        event = kavchat.verify_webhook(request.get_data(), request.headers.get("Kavchat-Signature"))
    except WebhookVerificationError as err:
        return err.code, 400
    if event["type"] == "ticket.created":
        print("New ticket", event["data"]["number"])
    return "", 200
PHP
use Kavchat\WebhookVerificationException;

try {
    $event = $kavchat->verifyWebhook(
        file_get_contents('php://input'),
        $_SERVER['HTTP_KAVCHAT_SIGNATURE'] ?? null,
    );
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit($e->errorCode);
}

if ($event->type === 'ticket.created') {
    error_log('New ticket ' . $event->data->number);
}
Java
@PostMapping("/kavchat/webhook")
public ResponseEntity<String> webhook(@RequestBody byte[] body,
                                      @RequestHeader(value = "Kavchat-Signature", required = false) String signature) {
    try {
        WebhookEvent event = kavchat.verifyWebhook(body, signature);
        if ("ticket.created".equals(event.getType())) log.info("New ticket {}", event.getData().getNumber());
        return ResponseEntity.ok("");
    } catch (WebhookVerificationException e) {
        return ResponseEntity.badRequest().body(e.getCode());
    }
}
.NET
app.MapPost("/kavchat/webhook", async (HttpRequest request, KavchatClient kavchat) =>
{
    using var ms = new MemoryStream();
    await request.Body.CopyToAsync(ms);
    try
    {
        var evt = kavchat.VerifyWebhook(ms.ToArray(), request.Headers[KavchatWebhook.SignatureHeader]);
        if (evt.Type == "ticket.created") Console.WriteLine($"New ticket {evt.Data.Number}");
        return Results.Ok();
    }
    catch (WebhookVerificationException e)
    {
        return Results.BadRequest(e.Code);
    }
});
Go
http.HandleFunc("/kavchat/webhook", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		http.Error(w, "read error", http.StatusBadRequest)
		return
	}
	event, err := client.VerifyWebhook(body, r.Header.Get(kavchat.SignatureHeader))
	var werr *kavchat.WebhookError
	if errors.As(err, &werr) {
		http.Error(w, werr.Code, http.StatusBadRequest)
		return
	}
	if event.Type == "ticket.created" {
		log.Printf("new ticket %d", event.Data.Number)
	}
})
Ruby
class KavchatWebhooksController < ActionController::API
  def create
    event = KAVCHAT.verify_webhook(request.raw_post, request.headers["Kavchat-Signature"])
    Rails.logger.info("New ticket #{event.data.number}") if event.type == "ticket.created"
    head :ok
  rescue Kavchat::WebhookVerificationError => e
    render plain: e.code, status: :bad_request
  end
end

ค่าเริ่มต้นของช่วงเวลาที่ยอมรับคือ 5 นาที (300 วินาที) สามารถเปลี่ยนค่าได้ในทุก SDK เช่น kavchat.verifyWebhook(body, header, { toleranceSeconds: 300 }) ใน Node.js

ไม่ใช้ SDK

Node.js (no SDK)
import crypto from 'node:crypto';

// header: "t=<unix seconds>,v1=<hex>", rawBody: the request body exactly as received
function verifyKavchatSignature(rawBody, header, secretKey) {
  const parts = Object.fromEntries(String(header).split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes
  const expected = crypto.createHmac('sha256', secretKey).update(`${t}.${rawBody}`).digest('hex');
  const given = Buffer.from(parts.v1 || '', 'hex');
  return given.length === 32 && crypto.timingSafeEqual(given, Buffer.from(expected, 'hex'));
}

การส่งและการลองส่งซ้ำ

  • การส่งถือว่าสำเร็จเมื่อเซิร์ฟเวอร์ของคุณตอบกลับด้วยรหัสสถานะ 2xx ภายใน 8 วินาที
  • หากส่งไม่สำเร็จ จะลองส่งซ้ำหลังจาก 5 วินาที 30 วินาที และ 2 นาที รวมทั้งหมดสี่ครั้ง
  • ผู้ใช้จะไม่ได้รับผลกระทบ ไม่ว่าการส่งจะสำเร็จหรือไม่ ทิกเก็ตจะถูกบันทึกไว้ในกล่องทิกเก็ตในแดชบอร์ด และสามารถอ่านผ่าน Server API ได้เสมอ
  • หากเซิร์ฟเวอร์ของคุณได้รับคำขอแต่ใช้เวลาตอบกลับเกิน 8 วินาที ระบบจะส่งซ้ำ ใช้ id ของทิกเก็ตเพื่อกรองรายการซ้ำ