
Typebot + WhatsApp: ligando seu fluxo a uma API self-hosted
Como conectar um Typebot self-hosted ao WhatsApp sem passar pela Cloud API da Meta — conector em Node, a Chat API do Typebot, sessões mapeadas por telefone, botões e listas, e os erros que todo mundo comete na primeira tentativa.
Typebot + WhatsApp: ligando seu fluxo a uma API self-hosted
O Typebot é, de longe, o construtor visual de fluxos mais querido por quem trabalha com conversação no Brasil: open source, self-hostável, e com um editor que não trata você como criança. O problema aparece na hora de plugar no WhatsApp.
A integração nativa do Typebot com WhatsApp usa a Cloud API da Meta. Isso significa conta no Meta Business, número verificado, templates aprovados antes de você poder iniciar conversa, e cobrança por conversa. Para muitos casos — bot de atendimento, qualificação de lead, FAQ, agendamento — é cerimônia demais para o que você está tentando fazer.
Este guia monta o outro caminho: Typebot self-hosted conversando com um gateway de WhatsApp self-hosted, os dois na sua infraestrutura, sem intermediário e sem custo por mensagem. Vamos usar a WaSphere como gateway, mas a arquitetura serve para qualquer API de WhatsApp que tenha webhooks.
A arquitetura
O Typebot expõe uma Chat API: você abre uma sessão de conversa por HTTP, manda o que o usuário digitou, e ele devolve as próximas mensagens do fluxo. Ou seja, o Typebot não precisa saber o que é WhatsApp — ele só precisa de alguém que traduza.
WhatsApp ──▶ WaSphere ──webhook assinado──▶ [ conector ] ──▶ Typebot (Chat API)
WhatsApp ◀── WaSphere ◀────── REST ─────── [ conector ] ◀── mensagens do fluxo
O conector é um serviço HTTP pequeno. Ele faz três coisas: valida a assinatura do webhook, guarda o mapa telefone -> sessão do Typebot, e converte os blocos do Typebot em tipos de mensagem do WhatsApp.
O que você vai precisar
- Typebot self-hosted rodando (a stack oficial em Docker), com um fluxo publicado. Anote o ID público do typebot — é o que aparece na URL de publicação.
- WaSphere rodando com um número conectado. O Quick Start leva uns dez minutos: clonar, definir os segredos,
docker compose up -d, ler o QR code. - Uma chave de API da WaSphere com o escopo
messages:send. Ver Chaves de API. - Um lugar para hospedar o conector — pode ser o mesmo servidor.
Passo 1 — Registrar o webhook na WaSphere
O conector precisa receber as mensagens que chegam no WhatsApp:
curl -X POST "https://api.seudominio.com/workspaces/{workspaceId}/webhooks" \
-H "Authorization: Bearer wsk_sua_chave" \
-H "Content-Type: application/json" \
-d '{
"name": "Typebot",
"url": "https://conector.seudominio.com/wasphere/incoming",
"events": ["message.received"],
"retryMax": 3,
"isActive": true
}'
Assine só message.received. A WaSphere emite bem mais que isso — recibos, mudanças de sessão, atualizações de grupo, eventos de bulk — e cada evento extra é uma execução inútil no seu conector.
Cada entrega chega assim:
POST /wasphere/incoming HTTP/1.1
X-WaSphere-Event: message.received
X-WaSphere-Signature: v1,sha256=<hmac-hex>
X-WaSphere-Timestamp: 1748168400
Content-Type: application/json
{
"event": "message.received",
"sessionId": "atendimento",
"timestamp": "2026-07-19T10:14:02.000Z",
"deliveryId": "abc123",
"data": {
"messageId": "3EB0C767D097B7C7A5C1",
"from": "5511999999999@s.whatsapp.net",
"fromName": "Ana Souza",
"type": "text",
"isGroup": false,
"content": { "text": "oi, quero saber os preços" }
}
}
A assinatura é HMAC-SHA256 sobre a string {timestamp}.{rawBody}, com o WEBHOOK_SIGNING_SECRET global do WA Server, formatada como v1,sha256={hex}. Valide antes de tocar no corpo — sem isso, qualquer pessoa que descubra a URL do conector consegue iniciar conversas falsas no seu fluxo. Exemplos completos em Node, Python e PHP estão em Webhooks.
Passo 2 — Falar com a Chat API do Typebot
A Chat API tem dois endpoints que importam: um para começar a conversa e um para continuar.
Começar:
curl -X POST "https://typebot.seudominio.com/api/v1/typebots/meu-bot/startChat" \
-H "Content-Type: application/json" \
-d '{
"prefilledVariables": {
"telefone": "5511999999999",
"nome": "Ana Souza"
}
}'
A resposta traz três coisas que você usa: sessionId (guarde), messages (o que mandar para o WhatsApp agora) e input (o que o fluxo está esperando em seguida).
Continuar, com o que a pessoa respondeu:
curl -X POST "https://typebot.seudominio.com/api/v1/sessions/$SESSION_ID/continueChat" \
-H "Content-Type: application/json" \
-d '{ "message": "quero o plano anual" }'
Confira a versão da API na documentação do seu Typebot antes de subir para produção — o caminho /api/v1/... mudou de forma entre versões maiores, e vale conferir contra a instância que você está rodando.
O prefilledVariables é a peça que a maioria esquece. Se você injetar o telefone como variável logo no startChat, o fluxo inteiro passa a ter acesso a ele — o que permite gravar lead no CRM, buscar pedido, ou mandar mensagem depois, tudo de dentro dos blocos de HTTP request do Typebot.
Passo 3 — O conector
Cem linhas, aproximadamente. A parte interessante é o mapa de sessões e a tradução dos blocos.
const TYPEBOT = 'https://typebot.seudominio.com';
const BOT_ID = process.env.TYPEBOT_PUBLIC_ID;
const WA = `https://api.seudominio.com/workspaces/${process.env.WORKSPACE_ID}`;
const SESSION = process.env.WA_SESSION_ID;
const sessions = new Map(); // telefone -> typebot sessionId (troque por Redis)
app.post('/wasphere/incoming', verificarAssinatura, async (req, res) => {
// responda já — o processamento vai por fora
res.sendStatus(200);
const { data } = req.body;
if (data.isGroup || data.type !== 'text') return;
const telefone = data.from.split('@')[0];
const texto = data.content?.text;
if (!texto) return;
let sessionId = sessions.get(telefone);
let resposta;
if (!sessionId) {
resposta = await postJson(`${TYPEBOT}/api/v1/typebots/${BOT_ID}/startChat`, {
prefilledVariables: { telefone, nome: data.fromName },
});
sessions.set(telefone, resposta.sessionId);
} else {
resposta = await postJson(
`${TYPEBOT}/api/v1/sessions/${sessionId}/continueChat`,
{ message: texto },
);
}
for (const msg of resposta.messages ?? []) {
await enviarParaWhatsApp(telefone, msg);
}
// fluxo terminou — limpe para a próxima conversa começar do início
if (!resposta.input) sessions.delete(telefone);
});
Repare no res.sendStatus(200) antes de qualquer trabalho. A WaSphere considera a entrega falhada se você demorar mais de 10 segundos para responder, e aí ela retenta — com 1s, 5s e 30s de intervalo. Se o seu conector processa de forma síncrona e o Typebot demora, você recebe a mesma mensagem quatro vezes e o fluxo avança quatro passos de uma vez. Responda primeiro, processe depois.
Passo 4 — Traduzir os blocos do Typebot
O Typebot devolve mensagens tipadas. Texto é trivial; o que dá trabalho são os blocos de escolha, porque no editor eles são botões e no WhatsApp precisam virar um tipo de mensagem específico.
async function enviarParaWhatsApp(telefone, msg) {
if (msg.type === 'text') {
return waPost('text', { to: telefone, text: extrairTexto(msg) });
}
if (msg.type === 'image') {
return waPost('image', { to: telefone, url: msg.content.url });
}
if (msg.type === 'video') {
return waPost('video', { to: telefone, url: msg.content.url });
}
if (msg.type === 'audio') {
return waPost('audio', { to: telefone, url: msg.content.url, isVoiceNote: false });
}
}
function waPost(tipo, corpo) {
return fetch(`${WA}/proxy/api/sessions/${SESSION}/messages/${tipo}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WASPHERE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(corpo),
});
}
O padrão da URL é sempre .../sessions/{sessionId}/messages/{tipo}, e a WaSphere aceita 14 tipos — text, image, video, audio, document, gif, sticker, location, contact, poll, buttons, list, view-once, reaction. Todos documentados em Enviar mensagens.
Para um bloco de escolha com até três opções, use botões:
{
"to": "5511999999999",
"text": "Qual plano te interessa?",
"footer": "Responda tocando em uma opção",
"buttons": [
{ "id": "mensal", "text": "Mensal" },
{ "id": "anual", "text": "Anual" }
]
}
Com mais de três opções, o tipo é list — que abre uma lista com seções e linhas selecionáveis. Nos dois casos a escolha volta como um message.received normal, trazendo o id do botão ou da linha. É esse id que você manda de volta no continueChat, e não o texto visível: o texto muda quando alguém edita o fluxo, o id não.
O telefone vai sempre em formato internacional, só dígitos, sem + e sem espaços. 5511999999999. Misturar formatos é a causa número um de "o bot criou duas conversas para a mesma pessoa".
O que dá errado na primeira vez
O fluxo trava no meio. Sessão do Typebot expirada e o continueChat retornando erro. Trate a falha recriando a sessão com startChat em vez de deixar a pessoa falando sozinha.
A mesma mensagem processada duas vezes. Toda entrega traz um deliveryId. Guarde os recentes e descarte repetidos — a entrega é pelo menos uma vez, por design.
429 no envio. A API limita 60 requisições por minuto por chave, e responde com o cabeçalho Retry-After em segundos. Um fluxo que dispara cinco mensagens seguidas por usuário chega lá mais rápido do que parece.
As mensagens chegam devagar. Isso é de propósito. Toda sessão nova tem uma pausa aleatória de 4 a 12 segundos antes de cada envio, porque cadência perfeita é assinatura de robô e é assim que número toma bloqueio. Dá para baixar em Controles anti-ban, mas pense duas vezes: mais rápido é sempre mais arriscado.
500 no envio. A sessão caiu. Assine session.disconnected e reconecte pelo dashboard.
Uma nota honesta sobre risco
Essa arquitetura roda sobre o protocolo web não oficial do WhatsApp. Ela é gratuita e não tem janela de 24 horas — você conversa quando quiser, sem template aprovado. Em troca, o número pode ser bloqueado, e nenhum gateway (este incluído) pode prometer o contrário.
O que de fato reduz risco não é configuração: é só falar com quem pediu para receber, aquecer número novo devagar, e dar um jeito fácil de sair. Se o seu caso é disparo em volume para quem não pediu, nenhum ajuste de delay salva a conta — esse trabalho pertence à Cloud API da Meta com templates aprovados, que a WaSphere também suporta, por sessão. Você pode rodar o número do bot no Baileys e o número de notificação na Meta, no mesmo workspace.
Próximos passos
- Enviar mensagens — os 14 tipos, com exemplos em cURL e Node
- Webhooks — payloads, verificação de assinatura, política de retentativa
- Controles anti-ban — o que dá e o que não dá para controlar
- Chatwoot + WhatsApp self-hosted — se você precisa de atendimento humano junto do bot
- WPPConnect vs WaSphere — comparando gateways open source
Todo o stack é MIT e sobe em Docker. Leia o código antes de confiar nele com o telefone dos seus clientes — esse é o argumento inteiro do open source.