Menu de guias

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

  1. Crie no seu servidor um endpoint HTTPS que aceite POST com um corpo JSON.
  2. No painel de controle, acesse Configurações e insira a URL em Webhook.
  3. 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.

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 é 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.

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

A 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

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

Envio e novas tentativas

  • O envio é bem-sucedido quando seu servidor responde com um status 2xx em 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 id do chamado para filtrar duplicatas.