Webhook
Cuando un usuario abre un ticket desde el chat, Kavchat puede enviarlo de inmediato a tu sistema de soporte, tu CRM o un bot de Slack. Cada envío se firma con tu clave secreta.
El webhook está incluido en el plan Premium.
Configuración
- Crea en tu servidor un endpoint HTTPS que acepte
POSTcon un cuerpo JSON. - En el panel de control, abre Configuración e introduce la URL en Webhook.
- Verifica la firma de cada solicitud, como se explica a continuación, y responde con cualquier código de estado
2xx.
La URL debe empezar por https:// y apuntar a una dirección pública. Las direcciones de redes privadas se bloquean.
Contenido del mensaje
Kavchat envía dos tipos de eventos: ticket.created para los tickets enviados desde el formulario de contacto y lead.created para los datos que un visitante deja en un paso de captación de leads del guion. El campo data contiene el ticket o el lead con la misma estructura que devuelve la API de servidor, incluida la transcripción de la conversación.
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 corresponde al campo sub del token de identidad si el usuario se ha identificado; de lo contrario, es null. Los campos name, email, phone y los campos de asunto también pueden ser null.
Leads y datos personales
Un guion puede hacer preguntas al visitante (nombre, teléfono, correo electrónico, número o texto libre), validar cada respuesta y guardarla en una variable. Un paso de captación de leads envía estos datos a la bandeja de leads del panel de control. En el evento lead.created, el campo data.variables contiene todos los valores recopilados, organizados por nombre de variable. Los valores habituales también aparecen en los campos data.name, data.email y data.phone. Los números de teléfono llegan en formato internacional (E.164).
Estos datos son información personal. Kavchat solo los guarda en el lead, el ticket o la conversación con el agente al que pertenecen, nunca en informes ni registros. Los leads se eliminan al terminar el periodo de conservación que hayas elegido (30 días en el plan gratuito; con Premium también puedes conservarlos sin eliminarlos). Puedes eliminar un lead individual en cualquier momento. Si recopilas datos personales, indícalo en tu política de privacidad. En el texto de consentimiento del paso de captación de leads, explica a los visitantes cómo usarás sus datos.
El encabezado Kavchat-Signature
El encabezado tiene este formato: t=<unix seconds>,v1=<hex>, donde:
tes la hora del envío, en segundos Unix.v1esHMAC-SHA256(secretKey, "<t>.<raw body>")en hexadecimal. La clave es tu clave secreta completa.
Para verificar la firma, calcula el mismo HMAC sobre el cuerpo sin procesar, exactamente como llegó, compara en tiempo constante y rechaza las solicitudes con más de 5 minutos de antigüedad. Si el cuerpo se ha deserializado y convertido de nuevo a JSON, no coincidirá con la firma.
Verificación con un SDK
verifyWebhook hace todo lo anterior y devuelve el evento deserializado. Si falla, lanza un error con uno de estos códigos: malformed_header, timestamp_out_of_range, invalid_signature o invalid_payload. Responde a estos errores con HTTP 400. La instalación de los SDK se explica en Identificación de usuarios.
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 tolerancia predeterminada es de 5 minutos (300 segundos). Puedes cambiarla en cualquier SDK, por ejemplo con kavchat.verifyWebhook(body, header, { toleranceSeconds: 300 }) en Node.js.
Sin 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'));
}Envío y reintentos
- Un envío se considera correcto si tu servidor responde con un código de estado
2xxen un plazo de 8 segundos. - Si el envío falla, se reintenta después de 5 segundos, 30 segundos y 2 minutos: cuatro intentos en total.
- El usuario no nota nada: el ticket se guarda en la bandeja de tickets del panel de control en cualquier caso y siempre puedes consultarlo a través de la API de servidor.
- Si tu servidor recibió la solicitud, pero tardó más de 8 segundos en responder, se volverá a enviar. Usa el
iddel ticket para filtrar duplicados.