Capítulo 4 de 13
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
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.
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.
{
"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?" }
}]
}
}]
}]
}Copie, salve como src/index.js e publique. É o webhook completo, com assinatura conferida.
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;
}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.
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 deployNo portal de desenvolvedores, dentro do produto WhatsApp, procure a área de configuração do webhook.
O endereço termina em /webhook, e o token de verificação é a mesma senha que você salvou em VERIFY_TOKEN.
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.
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 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.
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.
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.
Não. O endereço precisa ser https com certificado válido, e por isso um túnel local só serve para testar.
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