תפריט המדריכים

Webhook

כשמשתמש פותח פנייה מהצ'אט, Kavchat יכול לשלוח אותה מיד למערכת שלכם: מערכת התמיכה, ה-CRM או בוט ב-Slack. כל שליחה חתומה במפתח הסודי שלכם.

ה-webhook כלול בחבילת הפרימיום.

הגדרה

  1. יוצרים בשרת כתובת HTTPS שמקבלת POST עם גוף JSON.
  2. בלוח הבקרה נכנסים להגדרות ומזינים את הכתובת תחת Webhook.
  3. מאמתים את החתימה של כל בקשה, כמו שמוסבר בהמשך, ועונים בכל סטטוס 2xx.

הכתובת חייבת להתחיל ב-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 יום בתוכנית החינמית, ובפרימיום אפשר גם לשמור בלי למחוק). אפשר למחוק ליד בודד בכל רגע. אם אתם אוספים פרטים, ציינו זאת במדיניות הפרטיות שלכם. בשורת ההסכמה של שלב הליד, הסבירו למבקרים מה תעשו בפרטים.

הכותרת Kavchat-Signature

הכותרת נראית כך: t=<unix seconds>,v1=<hex>, כאשר:

  • t הוא זמן השליחה, בשניות Unix.
  • v1 הוא HMAC-SHA256(secretKey, "<t>.<raw body>") בקידוד hex. המפתח הוא המפתח הסודי המלא שלכם.

כדי לאמת, מחשבים את אותו HMAC על הגוף הגולמי, בדיוק כפי שהגיע, משווים בזמן קבוע, ודוחים בקשות ישנות מ-5 דקות. גוף שעבר פענוח והמרה חזרה ל-JSON לא יתאים לחתימה.

אימות עם ערכת פיתוח

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.

בלי ערכת פיתוח

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 של הפנייה כדי לסנן כפילויות.