ZynzapAPI

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:

  1. recebe o webhook MessageUpserted e confere a assinatura;
  2. responde só mensagens recebidas de clientes (automation_eligible);
  3. cota e envia com SendConversationMessage, que sai de graça se o cliente escreveu nas últimas 24 horas;
  4. usa idempotency_key derivada 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:inbox e messages:send.
  • Webhook cadastrado para MessageUpserted apontando para https://seu-servidor/zynzap (veja Webhooks).
  • Variáveis de ambiente ZYNZAP_API_KEY e ZYNZAP_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

  1. Rode o bot e exponha a porta 8080 com um túnel HTTPS (cloudflared tunnel --url http://localhost:8080).
  2. Cadastre o endereço do túnel como webhook com o evento MessageUpserted e use Enviar teste para conferir a assinatura (o bot responde 204).
  3. De outro celular, mande "qual o horário?" para o seu número.

Ir além

  • Grave os event.id processados (Redis, banco) para ignorar repetições além do idempotency_key.
  • Transfira para um humano com AssignConversation ou TransferConversation quando o bot não souber responder.
  • Respeite pedidos de descadastro com SetContactCommStatus.