Webhook
Lorsqu’un utilisateur ouvre un ticket depuis le chat, Kavchat peut l’envoyer immédiatement à votre système : votre outil de support, votre CRM ou un bot Slack. Chaque envoi est signé avec votre clé secrète.
Le webhook est inclus dans l’offre Premium.
Configuration
- Créez sur votre serveur un point de terminaison HTTPS qui accepte les requêtes
POSTavec un corps JSON. - Dans le tableau de bord, ouvrez les Paramètres et saisissez l’URL sous Webhook.
- Vérifiez la signature de chaque requête, comme expliqué ci-dessous, et répondez avec un code de statut
2xx.
L’URL doit commencer par https:// et pointer vers une adresse publique. Les adresses des réseaux privés sont bloquées.
Contenu du message
Kavchat envoie deux types d’événements : ticket.created pour un ticket envoyé depuis le formulaire de contact, et lead.created pour les informations laissées par un visiteur à une étape de collecte de prospects dans un script. Le champ data contient le ticket ou le prospect, dans le même format que celui renvoyé par l’API serveur, avec la transcription de la conversation.
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 correspond au champ sub du jeton d’identité si l’utilisateur a été identifié, sinon sa valeur est null. Les champs name, email, phone et les champs relatifs au sujet peuvent également valoir null.
Prospects et données personnelles
Un script peut poser des questions au visiteur (nom, téléphone, e-mail, nombre ou texte libre), valider chaque réponse et l’enregistrer dans une variable. Une étape de collecte de prospects envoie ces informations dans la boîte de réception des prospects du tableau de bord. Dans l’événement lead.created, le champ data.variables contient toutes les valeurs recueillies, indexées par nom de variable. Les valeurs courantes figurent aussi dans les champs data.name, data.email et data.phone. Les numéros de téléphone sont au format international (E.164).
Ces informations sont des données personnelles. Kavchat les conserve uniquement dans la fiche prospect, le ticket ou la conversation avec un agent auxquels elles se rapportent, jamais dans les rapports ni les journaux. Les prospects sont supprimés à la fin de la durée de conservation choisie (30 jours avec l’offre gratuite ; l’offre Premium permet aussi de les conserver sans limite de durée). Vous pouvez supprimer un prospect à tout moment. Si vous recueillez des données personnelles, indiquez-le dans votre politique de confidentialité. Dans le texte de consentement de l’étape de collecte de prospects, expliquez aux visiteurs comment vous utiliserez leurs données.
L’en-tête Kavchat-Signature
L’en-tête se présente ainsi : t=<unix seconds>,v1=<hex>, où :
test l’horodatage d’envoi, en secondes Unix.v1estHMAC-SHA256(secretKey, "<t>.<raw body>")encodé en hexadécimal. La clé utilisée est votre clé secrète complète.
Pour vérifier la signature, calculez le même HMAC sur le corps brut, exactement tel qu’il a été reçu, comparez les signatures en temps constant et rejetez les requêtes datant de plus de 5 minutes. Un corps décodé puis reconverti en JSON ne correspondra pas à la signature.
Vérification avec un SDK
verifyWebhook effectue toutes ces vérifications et renvoie l’événement décodé. En cas d’échec, la fonction lève une erreur avec l’un des codes suivants : malformed_header, timestamp_out_of_range, invalid_signature ou invalid_payload. Répondez à ces erreurs avec le statut HTTP 400. L’installation des SDK est expliquée dans la section Identification des utilisateurs.
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
endLa tolérance par défaut est de 5 minutes (300 secondes). Chaque SDK permet de la modifier, par exemple avec kavchat.verifyWebhook(body, header, { toleranceSeconds: 300 }) dans Node.js.
Sans 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'));
}Envoi et nouvelles tentatives
- Un envoi réussit si votre serveur répond avec un code de statut
2xxdans un délai de 8 secondes. - En cas d’échec, l’envoi est retenté après 5 secondes, 30 secondes et 2 minutes, soit quatre tentatives au total.
- L’utilisateur n’en est pas affecté : le ticket est enregistré dans la boîte de réception des tickets du tableau de bord dans tous les cas, et vous pouvez toujours le consulter via l’API serveur.
- Si votre serveur a reçu la requête mais a répondu après plus de 8 secondes, elle sera envoyée à nouveau. Utilisez l’
iddu ticket pour filtrer les doublons.