WWaSphere
GitHub
Typebot + WhatsApp: ligando seu fluxo a uma API self-hosted
WaSphere Teamtypebotchatbotintegracaoself-hostedwhatsapp-api

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

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.

Get Started

Ship a self-hosted WhatsApp API today

Clone the repo, set your secrets, and send your first message in minutes — Docker-based, MIT licensed, zero config.

Read the Quick Start