Webhook
Quando um usuário abre um chamado pelo chat, o Kavchat pode enviá-lo imediatamente ao seu sistema: uma plataforma de suporte, um CRM ou um bot no Slack. Cada envio é assinado com sua chave secreta.
O webhook está incluído no plano Premium.
Configuração
- Crie no seu servidor um endpoint HTTPS que aceite
POSTcom um corpo JSON. - No painel de controle, acesse Configurações e insira a URL em Webhook.
- Verifique a assinatura de cada requisição, conforme explicado abaixo, e responda com qualquer status
2xx.
A URL deve começar com https:// e apontar para um endereço público. Endereços de redes privadas são bloqueados.
Conteúdo da mensagem
O Kavchat envia dois tipos de evento: ticket.created para um chamado enviado pelo formulário de contato, e lead.created para os dados que um visitante deixou em uma etapa de lead do roteiro. O campo data contém o chamado ou o lead, na mesma estrutura retornada pela API do servidor, incluindo a transcrição da conversa.
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 é o campo sub do token de identidade quando o usuário foi identificado; caso contrário, é null. Os campos name, email, phone e os campos de assunto também podem ser null.
Leads e dados pessoais
Um roteiro pode fazer perguntas ao visitante (nome, telefone, e-mail, número ou texto livre), validar cada resposta e salvá-la em uma variável. Uma etapa de lead envia esses dados para a caixa de entrada de leads no painel de controle. No evento lead.created, o campo data.variables contém todos os valores coletados, organizados pelo nome da variável. Os valores mais comuns também aparecem nos campos data.name, data.email e data.phone. Os números de telefone chegam no formato internacional (E.164).
Essas informações são dados pessoais. O Kavchat armazena esses dados apenas no lead, no chamado ou na conversa com o atendente a que pertencem, nunca em relatórios ou logs. Os leads são excluídos ao final do período de retenção que você escolheu (30 dias no plano gratuito; no Premium, também é possível mantê-los sem excluir). Você pode excluir um lead individual a qualquer momento. Se você coleta dados pessoais, informe isso na sua política de privacidade. No texto de consentimento da etapa de lead, explique aos visitantes como você usará os dados.
Cabeçalho Kavchat-Signature
O cabeçalho tem este formato: t=<unix seconds>,v1=<hex>, em que:
té o horário do envio, em segundos Unix.v1éHMAC-SHA256(secretKey, "<t>.<raw body>")em hexadecimal. A chave usada é sua chave secreta completa.
Para verificar, calcule o mesmo HMAC sobre o corpo bruto, exatamente como foi recebido, compare em tempo constante e rejeite requisições com mais de 5 minutos. Um corpo que foi interpretado e convertido de volta para JSON não corresponderá à assinatura.
Verificação com SDK
A função verifyWebhook faz tudo isso e retorna o evento interpretado. Em caso de falha, lança um erro com um dos códigos: malformed_header, timestamp_out_of_range, invalid_signature ou invalid_payload. Responda a esses erros com HTTP 400. A instalação dos SDKs está explicada em Identificação de usuários.
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
endA tolerância padrão é de 5 minutos (300 segundos). Você pode alterá-la em qualquer SDK, por exemplo, com kavchat.verifyWebhook(body, header, { toleranceSeconds: 300 }) no Node.js.
Sem 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'));
}Envio e novas tentativas
- O envio é bem-sucedido quando seu servidor responde com um status
2xxem até 8 segundos. - Se o envio falhar, ele será repetido após 5 segundos, 30 segundos e 2 minutos: quatro tentativas no total.
- O usuário não percebe isso: o chamado é salvo na caixa de entrada de chamados do painel de controle de qualquer forma, e você sempre pode consultá-lo pela API do servidor.
- Se o seu servidor recebeu a requisição, mas levou mais de 8 segundos para responder, ela será enviada novamente. Use o
iddo chamado para filtrar duplicatas.