Capítulo 4 de 13

Receber a mensagem: o webhook, com o código pronto

O endereço que escuta o WhatsApp, a conferência de assinatura e o arquivo inteiro para copiar e publicar.

Manual gratuito de Certu · Atualizado em 30 de agosto de 2026 · ver o manual inteiro

Como receber mensagens do WhatsApp no meu servidor?

Resposta curta

Você publica um endereço https que responde a dois tipos de chamada: uma verificação por GET, em que a Meta devolve um desafio que você repete, e as mensagens por POST, com o conteúdo em JSON. O endereço precisa conferir a assinatura do cabeçalho e responder 200 rápido.

O que ter aberto antes de começar

O que a Meta manda para você?

Esta é a forma real de uma mensagem de texto recebida.

O que interessa está fundo: o telefone de quem escreveu em entry, changes, value, messages, from, e o texto em messages, text, body.

Nem todo aviso é mensagem. A Meta manda no mesmo endereço as confirmações de entrega e de leitura, que vêm em statuses no lugar de messages. Se o seu código não separar os dois, o atendente responde a própria confirmação de entrega.

exemplo do que chega no POSTjson
{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "102290129340398",
    "changes": [{
      "field": "messages",
      "value": {
        "messaging_product": "whatsapp",
        "metadata": {
          "display_phone_number": "5548999999999",
          "phone_number_id": "106540352242922"
        },
        "contacts": [{
          "profile": { "name": "Ana" },
          "wa_id": "5548988887777"
        }],
        "messages": [{
          "from": "5548988887777",
          "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgAS",
          "timestamp": "1749416383",
          "type": "text",
          "text": { "body": "voces abrem sabado?" }
        }]
      }
    }]
  }]
}

O arquivo inteiro

Copie, salve como src/index.js e publique. É o webhook completo, com assinatura conferida.

src/index.jsjavascript
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname !== '/webhook') {
      return new Response('no ar', { status: 200 });
    }

    // 1. Verificacao. A Meta chama uma vez, quando voce cadastra o endereco.
    if (request.method === 'GET') {
      const modo = url.searchParams.get('hub.mode');
      const token = url.searchParams.get('hub.verify_token');
      const desafio = url.searchParams.get('hub.challenge');
      if (modo === 'subscribe' && token === env.VERIFY_TOKEN) {
        return new Response(desafio, { status: 200 });
      }
      return new Response('token errado', { status: 403 });
    }

    // 2. Mensagens.
    if (request.method === 'POST') {
      const corpo = await request.text();

      const assinatura = request.headers.get('x-hub-signature-256');
      if (!(await assinaturaConfere(corpo, assinatura, env.APP_SECRET))) {
        return new Response('assinatura invalida', { status: 401 });
      }

      const dados = JSON.parse(corpo);
      const valor = dados?.entry?.[0]?.changes?.[0]?.value;

      // Confirmacao de entrega e de leitura chegam aqui tambem. Ignore.
      const mensagem = valor?.messages?.[0];
      if (!mensagem) {
        return new Response('ok', { status: 200 });
      }

      const de = mensagem.from;
      const nome = valor?.contacts?.[0]?.profile?.name ?? '';
      const texto = mensagem.type === 'text' ? mensagem.text.body : '';

      console.log('mensagem de', nome, de, texto);

      // No capitulo 5 a resposta entra aqui.

      return new Response('ok', { status: 200 });
    }

    return new Response('metodo nao permitido', { status: 405 });
  },
};

// A Meta assina o corpo com o segredo do aplicativo. Sem esta conferencia,
// qualquer pessoa que descubra o seu endereco manda mensagem falsa pro seu
// atendente e voce paga a resposta.
async function assinaturaConfere(corpo, cabecalho, segredo) {
  if (!cabecalho || !segredo) return false;

  const chave = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(segredo),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const assinado = await crypto.subtle.sign('HMAC', chave, new TextEncoder().encode(corpo));
  const hex = [...new Uint8Array(assinado)]
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
  const esperado = 'sha256=' + hex;

  // Comparacao de tempo constante: comparar com === vaza, pelo tempo de
  // resposta, quantos caracteres bateram.
  if (esperado.length !== cabecalho.length) return false;
  let diferenca = 0;
  for (let i = 0; i < esperado.length; i += 1) {
    diferenca |= esperado.charCodeAt(i) ^ cabecalho.charCodeAt(i);
  }
  return diferenca === 0;
}

Como publicar?

Quatro comandos no terminal, e o endereço está no ar.

VERIFY_TOKEN é uma senha inventada por você, qualquer texto, e você vai repetir a mesma na tela da Meta. APP_SECRET é o segredo do aplicativo, que fica em Configurações, Básico, no portal de desenvolvedores.

O comando de publicação imprime o endereço do seu worker. O endereço do webhook é ele mais /webhook no fim.

terminalbash
npm create cloudflare@latest atendente -- --type=hello-world
cd atendente
# cole o arquivo acima em src/index.js
npx wrangler secret put VERIFY_TOKEN
npx wrangler secret put APP_SECRET
npx wrangler deploy

Cadastrar o endereço na Meta

  1. Passo 1

    Abra a configuração do WhatsApp no aplicativo

    No portal de desenvolvedores, dentro do produto WhatsApp, procure a área de configuração do webhook.

  2. Passo 2

    Cole o endereço e o token

    O endereço termina em /webhook, e o token de verificação é a mesma senha que você salvou em VERIFY_TOKEN.

  3. Passo 3

    Assine o campo messages

    Depois de verificar, marque o campo messages na lista de assinaturas. Sem essa marcação a Meta valida o endereço e nunca manda mensagem nenhuma.

  4. Passo 4

    Mande uma mensagem para o número

    Escreva do seu celular para o número cadastrado e veja o texto aparecer no registro, com npx wrangler tail.

Onde as pessoas se queimam

Se o seu endereço demorar a responder, a Meta reenvia a mesma mensagem. Responda 200 primeiro e faça o trabalho pesado depois, senão o cliente recebe a mesma resposta duas ou três vezes. Guardar os identificadores já respondidos, o campo id da mensagem, resolve de vez.

O que o plano gratuito aguenta?

O plano gratuito de Cloudflare Workers dá 100 mil requisições por dia e 10 milissegundos de processamento por requisição. O tempo esperando a resposta da Meta e do modelo de IA não conta como processamento, então um atendente cabe folgado.

Se você guardar o histórico das conversas no armazenamento de chave e valor, o plano gratuito permite 100 mil leituras e mil escritas por dia. É o limite que aperta primeiro, e o capítulo 7 mostra como não estourar.

Perguntas frequentes

Preciso mesmo conferir a assinatura?

Precisa. O endereço é público. Sem conferir, qualquer um que descubra a URL manda mensagem falsa, o seu atendente responde e você paga a chamada do modelo de IA.

Posso usar Node no meu servidor em vez de Workers?

Pode. As regras são as mesmas: responder o desafio no GET, ler o JSON no POST, conferir o cabeçalho x-hub-signature-256 sobre o corpo bruto e devolver 200 rápido. Só muda a forma de subir o servidor.

A Meta aceita endereço sem https?

Não. O endereço precisa ser https com certificado válido, e por isso um túnel local só serve para testar.

Por que a mesma mensagem chega duas vezes?

Porque o seu endereço demorou ou devolveu erro, e a Meta reenvia. Guarde o campo id de cada mensagem já tratada e ignore repetições.

E onde entra Certu

Em Certu esta parte é o que o cliente nunca vê: o endereço que escuta, a assinatura conferida, a repetição descartada e a fila que segura pico de mensagem. É trabalho de uma vez só para montar e de todo mês para manter, e é por manter que se paga um serviço em vez de um script.

Ver o que Certu faz e quanto custa

Continuar o manual