Menu delle guide

Webhook

Quando un utente apre un ticket dalla chat, Kavchat può inviarlo subito al tuo sistema: help desk, CRM o bot su Slack. Ogni invio è firmato con la tua chiave segreta.

Il webhook è incluso nel piano Premium.

Configurazione

  1. Crea sul tuo server un endpoint HTTPS che accetti richieste POST con un corpo JSON.
  2. Nel pannello di controllo, apri Impostazioni e inserisci l'URL alla voce Webhook.
  3. Verifica la firma di ogni richiesta, come spiegato qui sotto, e rispondi con un qualsiasi codice di stato 2xx.

L'URL deve iniziare con https:// e puntare a un indirizzo pubblico. Gli indirizzi su reti private vengono bloccati.

Contenuto del messaggio

Kavchat invia due tipi di eventi: ticket.created per un ticket inviato dal modulo di contatto e lead.created per i dati lasciati da un visitatore in un passaggio di raccolta lead dello script. Il campo data contiene il ticket o il lead nello stesso formato restituito dalla Server API, inclusa la trascrizione della conversazione.

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 corrisponde al campo sub del token di identità se l'utente è stato identificato, altrimenti è null. Anche name, email, phone e i campi relativi all'argomento possono essere null.

Lead e dati personali

Uno script può chiedere al visitatore nome, telefono, email, un numero o un testo libero, verificare ogni risposta e salvarla in una variabile. Un passaggio di raccolta lead invia questi dati alla casella dei lead nel pannello di controllo. Nell'evento lead.created, il campo data.variables contiene tutti i valori raccolti, associati al nome della variabile. I valori più comuni compaiono anche nei campi data.name, data.email e data.phone. I numeri di telefono sono in formato internazionale (E.164).

Queste informazioni sono dati personali. Kavchat le conserva solo nel lead, nel ticket o nella conversazione con l'operatore a cui appartengono, mai nei report o nei log. I lead vengono eliminati al termine del periodo di conservazione che hai scelto (30 giorni nel piano gratuito; con Premium puoi anche conservarli senza scadenza). Puoi eliminare un singolo lead in qualsiasi momento. Se raccogli dati personali, indicalo nella tua informativa sulla privacy. Nel testo del consenso del passaggio di raccolta lead, spiega ai visitatori come userai i loro dati.

L'header Kavchat-Signature

L'header ha questo formato: t=<unix seconds>,v1=<hex>, dove:

  • t è l'ora di invio, espressa in secondi Unix.
  • v1 è HMAC-SHA256(secretKey, "<t>.<raw body>") in formato esadecimale. La chiave è la tua chiave segreta completa.

Per verificare la firma, calcola lo stesso HMAC sul corpo grezzo, esattamente come è stato ricevuto, confronta le firme in tempo costante e rifiuta le richieste più vecchie di 5 minuti. Un corpo decodificato e poi riconvertito in JSON non corrisponderà alla firma.

Verifica con un SDK

verifyWebhook esegue tutte queste operazioni e restituisce l'evento decodificato. In caso di errore, genera un'eccezione con uno di questi codici: malformed_header, timestamp_out_of_range, invalid_signature o invalid_payload. Rispondi a questi errori con HTTP 400. L'installazione degli SDK è descritta in Identificazione degli utenti.

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 tolleranza predefinita è di 5 minuti (300 secondi). Puoi modificarla in ogni SDK, ad esempio con kavchat.verifyWebhook(body, header, { toleranceSeconds: 300 }) in Node.js.

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

Invio e nuovi tentativi

  • L'invio va a buon fine quando il tuo server risponde con un codice di stato 2xx entro 8 secondi.
  • Se l'invio fallisce, viene ripetuto dopo 5 secondi, 30 secondi e 2 minuti: quattro tentativi in totale.
  • Per l'utente non cambia nulla: il ticket viene comunque salvato nella casella dei ticket del pannello di controllo e puoi sempre leggerlo tramite la Server API.
  • Se il tuo server ha ricevuto la richiesta ma ha risposto dopo più di 8 secondi, la richiesta verrà inviata di nuovo. Usa l'id del ticket per filtrare i duplicati.