Webhook
Wenn ein Nutzer im Chat ein Ticket erstellt, kann Kavchat es sofort an Ihr System senden: an Ihr Supportsystem, Ihr CRM oder einen Slack-Bot. Jede Übermittlung wird mit Ihrem geheimen Schlüssel signiert.
Der Webhook ist im Premium-Tarif enthalten.
Einrichtung
- Richten Sie auf Ihrem Server einen HTTPS-Endpunkt ein, der
POST-Anfragen mit einem JSON-Body annimmt. - Öffnen Sie im Dashboard die Einstellungen und tragen Sie die URL unter Webhook ein.
- Prüfen Sie die Signatur jeder Anfrage wie unten beschrieben und antworten Sie mit einem beliebigen
2xx-Status.
Die URL muss mit https:// beginnen und auf eine öffentliche Adresse verweisen. Adressen in privaten Netzwerken werden blockiert.
Nachrichteninhalt
Kavchat sendet zwei Ereignistypen: ticket.created für ein Ticket, das über das Kontaktformular gesendet wurde, und lead.created für Angaben, die ein Besucher in einem Lead-Schritt im Skript hinterlassen hat. Das Feld data enthält das Ticket oder den Lead im selben Format, das die Server-API zurückgibt, einschließlich des Gesprächsverlaufs.
POST /kavchat/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Kavchat-Webhook/1
Kavchat-Signature: t=1792915200,v1=5f2b7c...e91a{
"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 entspricht dem Feld sub aus dem Identitätstoken, wenn der Nutzer identifiziert wurde, andernfalls ist es null. Auch name, email, phone und die Themenfelder können null sein.
Leads und personenbezogene Daten
Ein Skript kann Besucher nach Angaben fragen (Name, Telefonnummer, E-Mail-Adresse, Zahl oder Freitext), jede Antwort prüfen und in einer Variablen speichern. Ein Lead-Schritt sendet diese Angaben an den Lead-Posteingang im Dashboard. Beim Ereignis lead.created enthält das Feld data.variables alle erfassten Werte, zugeordnet nach Variablennamen. Gängige Werte stehen zusätzlich in den Feldern data.name, data.email und data.phone. Telefonnummern werden im internationalen Format (E.164) übermittelt.
Diese Angaben sind personenbezogene Daten. Kavchat speichert sie nur im zugehörigen Lead, Ticket oder Gespräch mit einem Supportmitarbeiter, niemals in Berichten oder Protokollen. Leads werden nach Ablauf der von Ihnen gewählten Aufbewahrungsfrist gelöscht (30 Tage im kostenlosen Tarif; mit Premium können Sie sie auch unbegrenzt aufbewahren). Einzelne Leads können Sie jederzeit löschen. Wenn Sie personenbezogene Daten erfassen, weisen Sie in Ihrer Datenschutzerklärung darauf hin. Erklären Sie Besuchern im Einwilligungstext des Lead-Schritts, wie Sie ihre Daten verwenden werden.
Der Header Kavchat-Signature
Der Header hat folgendes Format: t=<unix seconds>,v1=<hex>. Dabei gilt:
tist der Versandzeitpunkt als Unix-Zeitstempel in Sekunden.v1istHMAC-SHA256(secretKey, "<t>.<raw body>")in Hexadezimaldarstellung. Als Schlüssel dient Ihr vollständiger geheimer Schlüssel.
Berechnen Sie zur Prüfung denselben HMAC über den unveränderten Body, genau so, wie er empfangen wurde. Vergleichen Sie die Signaturen in konstanter Zeit und lehnen Sie Anfragen ab, die älter als 5 Minuten sind. Ein Body, der geparst und wieder in JSON umgewandelt wurde, stimmt nicht mehr mit der Signatur überein.
Prüfung mit einem SDK
verifyWebhook übernimmt all diese Schritte und gibt das geparste Ereignis zurück. Bei einem Fehler löst die Funktion eine Ausnahme mit einem dieser Codes aus: malformed_header, timestamp_out_of_range, invalid_signature oder invalid_payload. Antworten Sie in diesen Fällen mit HTTP 400. Die Installation der SDKs wird unter Nutzer identifizieren beschrieben.
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;
}
});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 "", 200use 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);
}@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());
}
}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);
}
});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)
}
})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
endDie Standardtoleranz beträgt 5 Minuten (300 Sekunden). Sie lässt sich in jedem SDK ändern, zum Beispiel mit kavchat.verifyWebhook(body, header, { toleranceSeconds: 300 }) in Node.js.
Ohne 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'));
}Zustellung und Wiederholungsversuche
- Eine Zustellung ist erfolgreich, wenn Ihr Server innerhalb von 8 Sekunden mit einem
2xx-Status antwortet. - Fehlgeschlagene Zustellungen werden nach 5 Sekunden, 30 Sekunden und 2 Minuten erneut versucht: insgesamt vier Versuche.
- Der Nutzer merkt davon nichts: Das Ticket wird in jedem Fall im Ticket-Posteingang im Dashboard gespeichert und lässt sich jederzeit über die Server-API abrufen.
- Wenn Ihr Server die Anfrage empfangen hat, aber erst nach mehr als 8 Sekunden antwortet, wird sie erneut gesendet. Verwenden Sie die
iddes Tickets, um Duplikate herauszufiltern.