Bot simples
Um bot de respostas automáticas com webhook assinado, cotação e resposta grátis dentro da janela de 24 horas.
Este bot responde perguntas frequentes ("horário", "endereço", "pix") no WhatsApp. Ele:
- recebe o webhook
MessageUpsertede confere a assinatura; - responde só mensagens recebidas de clientes (
automation_eligible); - cota e envia com
SendConversationMessage, que sai de graça se o cliente escreveu nas últimas 24 horas; - usa
idempotency_keyderivada da mensagem recebida: se o webhook chegar duas vezes, a resposta sai uma vez só.
Para fluxos com menus, perguntas e transferência para humanos, use os Fluxos do painel (sem código). Este exemplo é para quem quer a lógica no próprio sistema.
Pré-requisitos
- Chave com
events:read,product:inboxemessages:send. - Webhook cadastrado para
MessageUpsertedapontando parahttps://seu-servidor/zynzap(veja Webhooks). - Variáveis de ambiente
ZYNZAP_API_KEYeZYNZAP_WEBHOOK_SECRET.
Código
// bot.mjs — Node.js 18+ (sem dependências). Rode: node bot.mjs
import crypto from "node:crypto";
import http from "node:http";
const API = process.env.ZYNZAP_API_BASE_URL ?? "https://api.zynzap.com.br/v1";
const ANSWERS = [
[/hor[aá]rio/i, "Atendemos de segunda a sexta, das 9h às 18h."],
[/endere[cç]o/i, "Estamos na Rua Exemplo, 123, Centro."],
[/pix/i, "Nossa chave Pix é o CNPJ 00.000.000/0001-00."],
];
function verify(rawBody, headers) {
const ts = headers["x-zynzap-timestamp"] ?? "";
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", process.env.ZYNZAP_WEBHOOK_SECRET)
.update(`${ts}.${rawBody}`)
.digest("hex");
return (headers["x-zynzap-signature"] ?? "").split(",").some((part) => {
const [scheme, value = ""] = part.trim().split("=");
return scheme === "v1" && value.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected));
});
}
async function call(path, body) {
const response = await fetch(`${API}${path}`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ZYNZAP_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await response.json();
if (!response.ok) throw new Error(`${data.error.code} ${data.error.reason ?? ""}: ${data.error.message}`);
return data;
}
async function handle(event) {
if (event.type !== "MessageUpserted") return;
const { session_id, conversation_id, message, automation_eligible } = event.data.MessageUpserted;
if (!automation_eligible || message.direction !== "inbound" || message.message_type !== "text") return;
const answer = ANSWERS.find(([pattern]) => pattern.test(message.body ?? ""))?.[1];
if (!answer) return;
const request = {
conversation_id,
session_id,
message_type: "text",
text: answer,
idempotency_key: `bot-${message.id}`,
};
const quote = await call("/billing/quotes", { method: "SendConversationMessage", request });
await call("/rpc/SendConversationMessage", { request, billing: { quoteId: quote.quoteId } });
}
http
.createServer((req, res) => {
let rawBody = "";
req.setEncoding("utf8");
req.on("data", (chunk) => (rawBody += chunk));
req.on("end", () => {
if (!verify(rawBody, req.headers)) return res.writeHead(401).end();
res.writeHead(204).end(); // responde rápido; o trabalho continua abaixo
handle(JSON.parse(rawBody)).catch((error) => console.error(error.message));
});
})
.listen(8080, () => console.log("Bot ouvindo em :8080/zynzap"));Testar
- Rode o bot e exponha a porta 8080 com um túnel HTTPS (
cloudflared tunnel --url http://localhost:8080). - Cadastre o endereço do túnel como webhook com o evento
MessageUpsertede use Enviar teste para conferir a assinatura (o bot responde204). - De outro celular, mande "qual o horário?" para o seu número.
Ir além
- Grave os
event.idprocessados (Redis, banco) para ignorar repetições além doidempotency_key. - Transfira para um humano com
AssignConversationouTransferConversationquando o bot não souber responder. - Respeite pedidos de descadastro com
SetContactCommStatus.