Menu des guides

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

  1. Créez sur votre serveur un point de terminaison HTTPS qui accepte les requêtes POST avec un corps JSON.
  2. Dans le tableau de bord, ouvrez les Paramètres et saisissez l’URL sous Webhook.
  3. 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.

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 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ù :

  • t est l’horodatage d’envoi, en secondes Unix.
  • v1 est HMAC-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.

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

La 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

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'));
}

Envoi et nouvelles tentatives

  • Un envoi réussit si votre serveur répond avec un code de statut 2xx dans 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’id du ticket pour filtrer les doublons.