Typebot no WhatsApp pela API oficial, com o ZapClaw

Em resumo: o ZapClaw entrega cada mensagem do WhatsApp a um pequeno script intermediário (o relay), que roda o seu Typebot publicado pela Chat API do Typebot, e as respostas do bot voltam pelo ZapClaw. As mensagens passam pela API Cloud oficial da Meta, e o número continua funcionando no app WhatsApp Business do celular. Você mantém o bot que já construiu e não precisa de app próprio na Meta.

Conecte o seu número em minutos e coloque nele o bot que você já construiu, hospedado pelo Typebot ou self-hosted.

Plano Business: R$ 99 por mês, com números ilimitados

Comece grátis Ver o relay

O que você precisa para conectar

Não precisa de CNPJ nem de empresa verificada para conectar. A lista completa.

Por que o ZapClaw para um Typebot no WhatsApp

Por que existe um relay no meio

O Typebot não consegue receber o webhook do ZapClaw do jeito que ele chega. O bloco Webhook só retoma uma conversa que já está pausada nele, e a Chat API espera uma chamada por mensagem, com uma sessão guardada para cada pessoa.

Então o relay cuida disso do jeito que o guia do Typebot para apps de mensagem (em inglês) descreve: abre uma sessão na primeira mensagem do cliente, continua a mesma sessão nas seguintes e devolve os balões de texto do bot pelo ZapClaw. São menos de 100 linhas de Node.js, sem nada para instalar.

Configure em quatro passos

  1. No Typebot, publique o seu bot e copie o ID público dele (onde encontrar). Mantenha o fluxo centrado em texto: balões de texto, campos de texto e botões.
  2. No ZapClaw, conecte o seu número, crie uma chave de API e copie a Chave de assinatura na página Webhook.
  3. Rode o relay abaixo com esses três valores num servidor com endereço HTTPS público e cole esse endereço em Webhook no ZapClaw. O ZapClaw só entrega em endereço HTTPS público.
  4. Clique em Enviar evento de teste no ZapClaw: o relay responde 200 e não inicia nada. Depois mande uma mensagem de WhatsApp para o seu número de outro celular: o bot responde.

O relay, pronto para rodar

Salve como relay.mjs e inicie com node relay.mjs no Node.js 18 ou mais recente. Ele lê as configurações do ambiente: ZAPCLAW_API_KEY, ZAPCLAW_SECRET (a chave de assinatura), TYPEBOT_PUBLIC_ID, TYPEBOT_API para um Typebot self-hosted, e PORT, a porta em que o relay escuta (8787 por padrão), para a qual o seu proxy HTTPS encaminha.

// relay.mjs: ZapClaw webhook in, Typebot Chat API, replies out through ZapClaw.
// Node.js 18 or newer, nothing to install. Start it with: node relay.mjs
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

const ZAPCLAW_API_KEY = process.env.ZAPCLAW_API_KEY; // zc_live_...
const ZAPCLAW_SECRET = process.env.ZAPCLAW_SECRET; // the webhook signing secret
const TYPEBOT_PUBLIC_ID = process.env.TYPEBOT_PUBLIC_ID; // your published bot
const TYPEBOT_API = process.env.TYPEBOT_API || 'https://typebot.co/api';

const sessions = new Map(); // customer: { id, choices }. Use your database in production.
const queues = new Map(); // one message at a time per customer, in order

const isSigned = (raw, header = '') => {
  const hmac = createHmac('sha256', ZAPCLAW_SECRET).update(raw).digest('hex');
  const expected = Buffer.from(`sha256=${hmac}`);
  const given = Buffer.from(String(header));
  return given.length === expected.length && timingSafeEqual(given, expected);
};

const typebot = async (path, body) => {
  const res = await fetch(`${TYPEBOT_API}/v1/${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...body, textBubbleContentFormat: 'markdown' }),
  });
  return res.ok ? res.json() : null;
};

const sendText = (from, to, text) =>
  fetch('https://api.zapclaw.app/api/v1/send/text', {
    method: 'POST',
    headers: { 'X-API-Key': ZAPCLAW_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ from, to, text }),
  });

const answer = async ({ data, account }) => {
  const customer = data.user_id || data.from;
  const from = account.display_phone_number || account.phone_number_id;
  const to = data.from || data.user_id;
  const session = sessions.get(customer);
  let reply = null;
  if (session) {
    // Typebot takes a button by its exact label, so accept its number or any letter case.
    const typed = data.text.body.trim();
    const choice = session.choices.find(
      (label, i) => typed === String(i + 1) || typed.toLowerCase() === label.toLowerCase()
    );
    reply = await typebot(`sessions/${session.id}/continueChat`, {
      message: { type: 'text', text: choice || typed },
    });
  }
  if (!reply) {
    // First message, or a session that expired (continueChat answers 404): start the bot.
    reply = await typebot(`typebots/${TYPEBOT_PUBLIC_ID}/startChat`, {
      prefilledVariables: { name: data.contact_name, phone: to },
    });
    if (!reply) return;
  }
  const choices =
    reply.input?.type === 'choice input'
      ? reply.input.items.map((item) => item.content).filter(Boolean)
      : [];
  if (reply.input) sessions.set(customer, { id: reply.sessionId || session.id, choices });
  else sessions.delete(customer); // the flow ended: the next message starts it again
  const texts = (reply.messages || [])
    .filter((m) => m.type === 'text')
    // Typebot writes standard Markdown; WhatsApp reads *bold*, ~strike~ and bare links.
    .map((m) => m.content.markdown
      .replace(/\*\*(.+?)\*\*/g, (_, t) => `*${t}*`)
      .replace(/~~(.+?)~~/g, (_, t) => `~${t}~`)
      .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => `${label} (${url})`));
  if (choices.length) texts.push(choices.map((label, i) => `${i + 1}. ${label}`).join('\n'));
  for (const text of texts) if (text) await sendText(from, to, text);
};

createServer((req, res) => {
  const chunks = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    const raw = Buffer.concat(chunks);
    if (!isSigned(raw, req.headers['x-zapclaw-signature'])) return res.writeHead(401).end();
    res.writeHead(200).end(); // answer ZapClaw now: it waits 10 seconds, a bot can take longer
    let event;
    try {
      event = JSON.parse(raw.toString('utf8'));
    } catch {
      return;
    }
    if (event.event !== 'message.received' || event.data?.type !== 'text') return;
    const key = event.data.user_id || event.data.from;
    const next = (queues.get(key) || Promise.resolve())
      .then(() => answer(event))
      .catch((err) => console.error(err));
    queues.set(key, next);
  });
}).listen(process.env.PORT || 8787);

Num Typebot self-hosted, aponte TYPEBOT_API para o endereço do seu viewer, o app onde as pessoas respondem aos seus bots, seguido de /api.

O que ele faz: confere o cabeçalho X-ZapClaw-Signature, responde ao ZapClaw na hora (o ZapClaw espera 10 segundos, e um bot que chama uma IA pode demorar mais) e trata as mensagens de cada cliente uma de cada vez, em ordem. As opções de um bloco Buttons saem como uma lista numerada, e o cliente responde com o número ou com o texto da opção, em maiúsculas ou minúsculas. Quando o fluxo termina, a próxima mensagem começa tudo de novo.

Crie variáveis chamadas name e phone no seu bot e o relay preenche as duas na primeira mensagem, com o nome do contato no WhatsApp e o número dele. As sessões ficam na memória do relay, então reiniciá-lo recomeça todas as conversas: em produção, guarde as sessões no seu banco de dados, como o guia do Typebot recomenda.

O relay envia os balões de texto. Um balão de imagem traz o endereço dela em content.url, que você pode enviar com POST /api/v1/send/media.

Chamando o ZapClaw de dentro do bot

O bloco HTTP Request do Typebot também chama a API do ZapClaw, com um cabeçalho X-API-Key e um corpo personalizado montado com as suas variáveis: por exemplo, para avisar um vendedor no WhatsApp quando um lead chega ao fim do bot. A mesma chamada como comando curl:

curl -X POST https://api.zapclaw.app/api/v1/send/text \
  -H "X-API-Key: zc_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "from": "15551617409", "to": "15555550100", "text": "New lead from the bot" }'

from é o seu número conectado, que o webhook traz em account.display_phone_number, e to é a pessoa para quem você escreve. Uma mensagem enviada responde 201. Texto livre funciona dentro de 24 horas desde a última mensagem dessa pessoa para o seu número; fora dessa janela, a Meta só aceita um template aprovado, pelo POST /api/v1/send/template.

O que chega no relay

Cada mensagem chega como um evento message.received: o texto em data.text.body, o remetente em data.from (ou data.user_id para um contato que só mostra o nome de usuário), o nome do contato em data.contact_name e o seu número em account.display_phone_number.

{
  "event": "message.received",
  "sequence": 1042,
  "timestamp": "2026-10-15T14:00:00.000Z",
  "data": {
    "from": "15555550100",
    "user_id": "US.1A2B3C4D5E6F7G8H9I0J",
    "contact_name": "Maria Silva",
    "message_id": "wamid.HBg...",
    "type": "text",
    "timestamp": "1792072800",
    "text": { "body": "Hi" }
  },
  "account": {
    "phone_number_id": "964283400112599",
    "display_name": "My Business",
    "display_phone_number": "+1 555 161-7409"
  }
}

O relay só repassa mensagens de texto recebidas: o evento de teste do painel e qualquer outro evento recebem um 200 e nada mais. Todos os eventos e campos estão na documentação (em inglês).

Quanto custa

A Meta cobra as tarifas de mensagem do WhatsApp direto de você, sem margem do ZapClaw. Todos os planos e preços.

Para quem é

Equipes que montaram no Typebot fluxos de captação de leads, qualificação, perguntas frequentes ou agendamento e querem levar esses fluxos para o número de WhatsApp que os clientes já usam. Agências conectam os números dos clientes no plano Agency, cada cliente isolado dos outros. Não serve para disparo em massa.

Para ir além

Fontes

Documentação do Typebot, em inglês, conferida em 15 de outubro de 2026. Recursos mudam, então confira cada fonte antes de construir.

Coloque o seu Typebot no WhatsApp hoje

Comece no plano grátis: um número, sem cartão. Passe para o plano Business quando precisar de mais números ou de mais mensagens.

Comece grátis Ver a documentação do webhook (em inglês)