قائمة الأدلة

Webhook

عندما يفتح مستخدم تذكرة من المحادثة، يمكن لـ Kavchat إرسالها فورًا إلى نظامكم: نظام الدعم، أو نظام إدارة علاقات العملاء (CRM)، أو بوت في Slack. يُوقَّع كل إرسال بمفتاحكم السري.

يتوفر webhook ضمن خطة Premium.

الإعداد

  1. أنشئوا على خادمكم عنوان HTTPS يستقبل طلبات POST بمحتوى JSON.
  2. في لوحة التحكم، افتحوا الإعدادات وأدخلوا العنوان ضمن Webhook.
  3. تحققوا من توقيع كل طلب، كما هو موضح أدناه، وأجيبوا بأي رمز حالة من فئة 2xx.

يجب أن يبدأ العنوان بـ https:// وأن يشير إلى عنوان عام. تُحظر العناوين في الشبكات الخاصة.

محتوى الرسالة

يرسل Kavchat نوعين من الأحداث: ticket.created للتذكرة المُرسلة من نموذج التواصل، وlead.created للبيانات التي يتركها الزائر في خطوة جمع بيانات عميل محتمل ضمن السيناريو. يحتوي الحقل data على التذكرة أو بيانات العميل المحتمل، بالبنية نفسها التي تُرجعها واجهة 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. تجدون شرح تثبيت حزم التطوير في تحديد هوية المستخدمين.

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 ثانية). يمكنكم تغييرها في كل حزمة تطوير، مثلًا باستخدام 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 دقيقة: أربع محاولات إجمالًا.
  • لا يتأثر المستخدم بذلك: تُحفظ التذكرة في صندوق التذاكر في لوحة التحكم في جميع الأحوال، ويمكنكم دائمًا قراءتها عبر واجهة API للخادم.
  • إذا استقبل خادمكم الطلب لكنه أجاب بعد أكثر من 8 ثوانٍ، فسيُرسل الطلب مجددًا. استخدموا id الخاص بالتذكرة لتصفية التكرارات.