{"id":540,"date":"2026-10-28T16:07:09","date_gmt":"2026-10-28T16:07:09","guid":{"rendered":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/"},"modified":"2026-10-28T16:07:11","modified_gmt":"2026-10-28T16:07:11","slug":"bot-whatsapp-nodejs-api-oficial","status":"publish","type":"post","link":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/","title":{"rendered":"Bot de WhatsApp com Node.js na API oficial (Express, do zero)"},"content":{"rendered":"\n<p><em>Atualizado em 28\/10\/2026<\/em><\/p>\n\n\n\n<p>Para criar um bot de WhatsApp com Node.js na API oficial, voc\u00ea precisa de um servidor Express com HTTPS e duas rotas: um <code>GET<\/code> que devolve o desafio de verifica\u00e7\u00e3o da Meta e um <code>POST<\/code> que recebe as mensagens, valida a assinatura <code>X-Hub-Signature-256<\/code> sobre o corpo bruto (com <code>express.raw<\/code>) e responde pelo endpoint <code>\/messages<\/code> da Cloud API usando o <code>fetch<\/code> nativo. Sem SDK e sem depend\u00eancias al\u00e9m do Express, tudo cabe em um arquivo de cerca de 150 linhas.<\/p>\n\n\n\n<p>O bot final responde &#8220;oi&#8221; ou &#8220;menu&#8221; com tr\u00eas bot\u00f5es, trata o toque em cada bot\u00e3o, marca a mensagem como lida, ignora eventos repetidos e roda em produ\u00e7\u00e3o com Docker e HTTPS autom\u00e1tico. O c\u00f3digo foi testado com Express 5.3 e Node.js 26, e funciona direto na Meta ou pela Vectalk, mudando s\u00f3 duas vari\u00e1veis de ambiente. Se voc\u00ea prefere Python, o mesmo bot existe em <a href=\"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-python-api-oficial\/\">vers\u00e3o FastAPI<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Como o bot funciona<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>O cliente manda uma mensagem para o seu n\u00famero.<\/li>\n\n\n<li>A Meta envia um <code>POST<\/code> com o evento para a URL do seu webhook.<\/li>\n\n\n<li>O servidor valida a assinatura, responde <code>200<\/code> na hora e processa o evento depois.<\/li>\n\n\n<li>O bot chama <code>POST \/&lt;PHONE_NUMBER_ID&gt;\/messages<\/code> para responder.<\/li>\n\n<\/ol>\n\n\n\n<p>Como a resposta sai logo depois da mensagem do cliente, ela est\u00e1 dentro da <a href=\"https:\/\/vectalk.com.br\/blog\/janela-24-horas-whatsapp\/\">janela de 24 horas<\/a> e pode ser texto livre ou mensagem interativa, sem template. Para a vis\u00e3o geral de como acessar a API, veja o guia da <a href=\"https:\/\/vectalk.com.br\/blog\/api-oficial-do-whatsapp\/\">API oficial do WhatsApp<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">O que voc\u00ea precisa antes<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Vari\u00e1vel<\/th><th>O que \u00e9<\/th><th>Onde encontrar<\/th><\/tr><\/thead><tbody><tr><td><code>PHONE_NUMBER_ID<\/code><\/td><td>ID do n\u00famero na Cloud API<\/td><td>Painel de Apps, na se\u00e7\u00e3o do WhatsApp<\/td><\/tr><tr><td><code>ACCESS_TOKEN<\/code><\/td><td>Token com permiss\u00e3o <code>whatsapp_business_messaging<\/code><\/td><td>Token de usu\u00e1rio do sistema<\/td><\/tr><tr><td><code>APP_SECRET<\/code><\/td><td>Chave secreta do app, usada na assinatura<\/td><td>Configura\u00e7\u00f5es do app &gt; B\u00e1sico<\/td><\/tr><tr><td><code>VERIFY_TOKEN<\/code><\/td><td>Texto qualquer escolhido por voc\u00ea<\/td><td>Voc\u00ea define e repete no painel<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Use um <a href=\"https:\/\/vectalk.com.br\/blog\/token-permanente-api-whatsapp\/\">token permanente de usu\u00e1rio do sistema<\/a>, porque o token tempor\u00e1rio do painel expira. O n\u00famero tamb\u00e9m precisa estar registrado na Cloud API, com o PIN de duas etapas definido.<\/p>\n\n\n\n<p>No ambiente, voc\u00ea precisa de Node.js 18 ou mais recente, que \u00e9 a vers\u00e3o m\u00ednima exigida pelo Express 5 e j\u00e1 traz o <code>fetch<\/code> global, sem precisar de axios.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Prepare o projeto<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">mkdir bot-whatsapp &amp;&amp; cd bot-whatsapp\nnpm init -y\nnpm pkg set type=module\nnpm install express@5<\/code><\/pre>\n\n\n\n<p>O <code>type=module<\/code> permite usar <code>import<\/code>. Crie o arquivo <code>server.js<\/code>. Os blocos de c\u00f3digo das se\u00e7\u00f5es 1 a 5 formam o arquivo completo: cole cada um em sequ\u00eancia.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">import crypto from 'node:crypto';\nimport express from 'express';\n\nconst {\n  VERIFY_TOKEN,\n  ACCESS_TOKEN,\n  APP_SECRET = '',\n  WEBHOOK_KEY = '',\n  PHONE_NUMBER_ID = '',\n  PORT = 3000,\n} = process.env;\nconst API_URL =\n  process.env.API_URL ?? `https:\/\/graph.facebook.com\/v26.0\/${PHONE_NUMBER_ID}\/messages`;\n\nconst app = express();\nconst processed = new Set(); \/\/ em produ\u00e7\u00e3o, use Redis ou o banco<\/code><\/pre>\n\n\n\n<p><code>API_URL<\/code> usa a Graph API da Meta como padr\u00e3o. Para usar a Vectalk, voc\u00ea troca essa vari\u00e1vel e o token, como mostra a se\u00e7\u00e3o sobre a Vectalk.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Verifica\u00e7\u00e3o do webhook (GET)<\/h2>\n\n\n\n<p>Quando voc\u00ea salva a URL de callback no painel, a Meta manda um <code>GET<\/code> com <code>hub.mode=subscribe<\/code>, <code>hub.verify_token<\/code> e <code>hub.challenge<\/code>. Se o token bater, responda <code>200<\/code> com o valor de <code>hub.challenge<\/code>. Qualquer outra resposta deixa o endpoint n\u00e3o verificado, e a Meta n\u00e3o envia eventos.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">app.get('\/webhook', (req, res) =&gt; {\n  const mode = req.query['hub.mode'];\n  const token = req.query['hub.verify_token'];\n  if (mode === 'subscribe' &amp;&amp; token === VERIFY_TOKEN) {\n    return res.status(200).send(req.query['hub.challenge']);\n  }\n  res.sendStatus(403);\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">3. Validar a assinatura com express.raw<\/h2>\n\n\n\n<p>Todo <code>POST<\/code> da Meta traz <code>X-Hub-Signature-256: sha256=&lt;hash&gt;<\/code>, um HMAC-SHA256 do corpo calculado com a chave secreta do app. O hash precisa ser calculado sobre os bytes exatos que chegaram. Se o <code>express.json()<\/code> j\u00e1 transformou o corpo em objeto, <code>JSON.stringify<\/code> n\u00e3o devolve os mesmos bytes, e a assinatura nunca bate.<\/p>\n\n\n\n<p>A solu\u00e7\u00e3o \u00e9 usar <code>express.raw<\/code> s\u00f3 na rota do webhook. Segundo a documenta\u00e7\u00e3o do Express, esse middleware coloca o corpo em <code>req.body<\/code> como <code>Buffer<\/code>, mas apenas quando o <code>Content-Type<\/code> combina com a op\u00e7\u00e3o <code>type<\/code>, cujo padr\u00e3o \u00e9 <code>application\/octet-stream<\/code>. Como a Meta envia <code>application\/json<\/code>, \u00e9 preciso informar <code>type: 'application\/json'<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">function signatureOk(rawBody, header = '') {\n  if (!APP_SECRET) return true;\n  const expected =\n    'sha256=' + crypto.createHmac('sha256', APP_SECRET).update(rawBody).digest('hex');\n  const a = Buffer.from(expected);\n  const b = Buffer.from(header);\n  return a.length === b.length &amp;&amp; crypto.timingSafeEqual(a, b);\n}<\/code><\/pre>\n\n\n\n<p><code>crypto.timingSafeEqual<\/code> compara em tempo constante, mas lan\u00e7a erro se os buffers tiverem tamanhos diferentes. Por isso o c\u00f3digo checa o tamanho antes.<\/p>\n\n\n\n<p>Tr\u00eas erros comuns nessa etapa:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>app.use(express.json())<\/code> global antes da rota.<\/strong> O corpo j\u00e1 chega convertido e o <code>Buffer<\/code> some. Registre o <code>express.json()<\/code> depois da rota do webhook ou s\u00f3 nas rotas que precisam dele.<\/li>\n\n\n<li><strong><code>type<\/code> errado no <code>express.raw<\/code>.<\/strong> Com o padr\u00e3o, <code>req.body<\/code> fica <code>undefined<\/code> para requisi\u00e7\u00f5es JSON.<\/li>\n\n\n<li><strong>Chave errada.<\/strong> A assinatura usa a chave secreta do app, n\u00e3o o token de acesso nem o <code>VERIFY_TOKEN<\/code>.<\/li>\n\n<\/ul>\n\n\n\n<p>O artigo sobre <a href=\"https:\/\/vectalk.com.br\/blog\/assinatura-webhook-whatsapp\/\">validar a assinatura do webhook<\/a> aprofunda o tema.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Receber o evento e responder r\u00e1pido<\/h2>\n\n\n\n<p>A Meta espera <code>200<\/code> r\u00e1pido. Se a entrega falha, ela tenta de novo com frequ\u00eancia cada vez menor por at\u00e9 7 dias, ent\u00e3o o mesmo evento pode chegar mais de uma vez. O c\u00f3digo responde antes de processar e ignora IDs de mensagem j\u00e1 vistos.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">app.post('\/webhook', express.raw({ type: 'application\/json' }), (req, res) =&gt; {\n  if (WEBHOOK_KEY &amp;&amp; req.query.key !== WEBHOOK_KEY) return res.sendStatus(403);\n  if (!Buffer.isBuffer(req.body)) return res.sendStatus(400);\n  if (!signatureOk(req.body, req.get('x-hub-signature-256'))) return res.sendStatus(401);\n\n  const payload = JSON.parse(req.body.toString('utf8'));\n  res.sendStatus(200); \/\/ responde j\u00e1 e processa depois\n  processPayload(payload).catch((err) =&gt; console.error('falha no processamento', err));\n});\n\nasync function processPayload(payload) {\n  for (const entry of payload.entry ?? []) {\n    for (const change of entry.changes ?? []) {\n      for (const msg of change.value?.messages ?? []) {\n        if (processed.has(msg.id)) continue; \/\/ a Meta pode reenviar o mesmo evento\n        processed.add(msg.id);\n        try {\n          await handle(msg);\n        } catch (err) {\n          console.error(`falha ao responder ${msg.id}`, err.message);\n        }\n      }\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p>O <code>res.sendStatus(200)<\/code> sai antes de <code>processPayload<\/code>, que roda sem bloquear a resposta. Eventos de status (<code>sent<\/code>, <code>delivered<\/code>, <code>read<\/code>, <code>failed<\/code>) chegam no mesmo webhook, em <code>value.statuses<\/code>, e este bot os ignora. Para trat\u00e1-los, veja <a href=\"https:\/\/vectalk.com.br\/blog\/status-mensagem-webhook-whatsapp\/\">status de mensagem via webhook<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Responder texto e bot\u00f5es interativos<\/h2>\n\n\n\n<p>A l\u00f3gica fica em <code>handle<\/code>. Texto &#8220;oi&#8221; ou &#8220;menu&#8221; abre o menu; o toque em um bot\u00e3o chega como <code>type: \"interactive\"<\/code> com <code>interactive.type: \"button_reply\"<\/code> e o <code>id<\/code> definido no envio.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">const ANSWERS = {\n  horario: 'Atendemos de segunda a sexta, das 9h \u00e0s 18h.',\n  precos: 'Veja a tabela completa em https:\/\/example.com\/precos',\n  humano: 'Certo! Um atendente vai continuar a conversa por aqui.',\n};\n\nasync function handle(msg) {\n  const dest = recipientOf(msg);\n  await markRead(msg.id);\n\n  if (msg.type === 'text') {\n    const text = msg.text.body.trim().toLowerCase();\n    if (['oi', 'ol\u00e1', 'ola', 'menu'].includes(text)) return sendMenu(dest);\n    return sendText(dest, 'N\u00e3o entendi. Digite *menu* para ver as op\u00e7\u00f5es.');\n  }\n  if (msg.type === 'interactive' &amp;&amp; msg.interactive.type === 'button_reply') {\n    const choice = msg.interactive.button_reply.id;\n    return sendText(dest, ANSWERS[choice] ?? 'Op\u00e7\u00e3o inv\u00e1lida.');\n  }\n  return sendText(dest, 'Por enquanto eu entendo s\u00f3 texto e bot\u00f5es.');\n}\n\nfunction recipientOf(msg) {\n  \/\/ Usu\u00e1rios com nome de usu\u00e1rio podem chegar sem \"from\", s\u00f3 com o BSUID\n  return msg.from ? { to: msg.from } : { recipient: msg.from_user_id };\n}<\/code><\/pre>\n\n\n\n<p>A fun\u00e7\u00e3o <code>recipientOf<\/code> prepara o bot para os nomes de usu\u00e1rio do WhatsApp. Segundo a documenta\u00e7\u00e3o da Meta, quando o cliente adota um nome de usu\u00e1rio, o campo <code>from<\/code> pode faltar no webhook, mas o <code>from_user_id<\/code> (BSUID) vem sempre, e desde julho de 2026 \u00e9 poss\u00edvel enviar mensagens para ele no campo <code>recipient<\/code>. Detalhes em <a href=\"https:\/\/vectalk.com.br\/blog\/usernames-whatsapp-bsuid\/\">usernames e BSUID<\/a>.<\/p>\n\n\n\n<p>E as fun\u00e7\u00f5es que chamam a API com o <code>fetch<\/code> nativo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">async function callApi(payload) {\n  const r = await fetch(API_URL, {\n    method: 'POST',\n    headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, 'Content-Type': 'application\/json' },\n    body: JSON.stringify({ messaging_product: 'whatsapp', ...payload }),\n  });\n  const body = await r.json();\n  if (!r.ok) throw new Error(`API ${r.status}: ${JSON.stringify(body)}`);\n  return body;\n}\n\nasync function markRead(messageId) {\n  try {\n    await callApi({ status: 'read', message_id: messageId, typing_indicator: { type: 'text' } });\n  } catch {\n    \/\/ n\u00e3o impede a resposta\n  }\n}\n\nfunction sendText(dest, text) {\n  return callApi({ recipient_type: 'individual', ...dest, type: 'text', text: { body: text } });\n}\n\nfunction sendMenu(dest) {\n  const buttons = [\n    ['horario', 'Hor\u00e1rio'],\n    ['precos', 'Pre\u00e7os'],\n    ['humano', 'Falar com atendente'],\n  ];\n  return callApi({\n    recipient_type: 'individual',\n    ...dest,\n    type: 'interactive',\n    interactive: {\n      type: 'button',\n      body: { text: 'Ol\u00e1! Como posso ajudar?' },\n      action: {\n        buttons: buttons.map(([id, title]) =&gt; ({ type: 'reply', reply: { id, title } })),\n      },\n    },\n  });\n}\n\napp.listen(PORT, () =&gt; console.log(`Bot ouvindo na porta ${PORT}`));<\/code><\/pre>\n\n\n\n<p><code>markRead<\/code> marca a mensagem como lida e mostra &#8220;digitando&#8230;&#8221; para o cliente; a Meta remove o indicador quando voc\u00ea responde ou depois de 25 segundos. Um detalhe do <code>fetch<\/code>: ele n\u00e3o lan\u00e7a erro em respostas 4xx e 5xx, por isso o c\u00f3digo confere <code>r.ok<\/code> e lan\u00e7a o erro com o corpo da Meta, que traz o c\u00f3digo para diagn\u00f3stico.<\/p>\n\n\n\n<p>Os limites dos bot\u00f5es de resposta, segundo a documenta\u00e7\u00e3o da Meta:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Campo<\/th><th>Limite<\/th><\/tr><\/thead><tbody><tr><td>Bot\u00f5es por mensagem<\/td><td>at\u00e9 3<\/td><\/tr><tr><td><code>title<\/code> do bot\u00e3o<\/td><td>20 caracteres<\/td><\/tr><tr><td><code>id<\/code> do bot\u00e3o<\/td><td>256 caracteres<\/td><\/tr><tr><td><code>body.text<\/code><\/td><td>1.024 caracteres<\/td><\/tr><tr><td><code>footer.text<\/code><\/td><td>60 caracteres<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>O toque no bot\u00e3o chega assim no webhook (trecho de <code>messages[0]<\/code>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-json\">{\n  \"from\": \"5511999999999\",\n  \"id\": \"wamid.HBgL...\",\n  \"type\": \"interactive\",\n  \"interactive\": {\n    \"type\": \"button_reply\",\n    \"button_reply\": { \"id\": \"precos\", \"title\": \"Pre\u00e7os\" }\n  }\n}<\/code><\/pre>\n\n\n\n<p>Listas com at\u00e9 10 op\u00e7\u00f5es e outros formatos est\u00e3o em <a href=\"https:\/\/vectalk.com.br\/blog\/mensagens-interativas-whatsapp-api\/\">mensagens interativas<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Testar localmente<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">export VERIFY_TOKEN=\"um-texto-qualquer\"\nexport ACCESS_TOKEN=\"EAAG...\"\nexport APP_SECRET=\"sua-chave-secreta\"\nexport PHONE_NUMBER_ID=\"123456789012345\"\nnode server.js<\/code><\/pre>\n\n\n\n<p>Em outro terminal, teste a verifica\u00e7\u00e3o e um evento assinado sem depender da Meta:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">curl \"http:\/\/127.0.0.1:3000\/webhook?hub.mode=subscribe&amp;hub.verify_token=um-texto-qualquer&amp;hub.challenge=42\"\n\nBODY='{\"entry\":[{\"changes\":[{\"value\":{\"messages\":[{\"from\":\"5511999999999\",\"id\":\"wamid.teste1\",\"type\":\"text\",\"text\":{\"body\":\"menu\"}}]}}]}]}'\nSIG=$(printf '%s' \"$BODY\" | openssl dgst -sha256 -hmac \"$APP_SECRET\" | awk '{print $NF}')\ncurl -i -X POST http:\/\/127.0.0.1:3000\/webhook \\\n  -H \"Content-Type: application\/json\" \\\n  -H \"X-Hub-Signature-256: sha256=$SIG\" \\\n  --data-binary \"$BODY\"<\/code><\/pre>\n\n\n\n<p>O primeiro comando devolve <code>42<\/code>. O segundo devolve <code>200 OK<\/code> e faz o bot enviar o menu para o n\u00famero em <code>from<\/code> (use o seu n\u00famero de teste). Troque um caractere do <code>BODY<\/code> sem recalcular a assinatura e a resposta passa a ser <code>401<\/code>.<\/p>\n\n\n\n<p>Para eventos reais, a Meta exige URL p\u00fablica com certificado TLS v\u00e1lido. Em desenvolvimento, use um t\u00fanel, como mostra o guia <a href=\"https:\/\/vectalk.com.br\/blog\/testar-webhook-whatsapp-localmente\/\">testar o webhook localmente com ngrok<\/a>. Depois v\u00e1 em Painel de Apps &gt; WhatsApp &gt; Configura\u00e7\u00e3o, preencha a URL de callback (<code>https:\/\/...\/webhook<\/code>) e o token de verifica\u00e7\u00e3o, salve e assine o campo <code>messages<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Deploy simples com Docker e HTTPS<\/h2>\n\n\n\n<p>O <code>npm install<\/code> j\u00e1 criou o <code>package-lock.json<\/code>. Crie o <code>Dockerfile<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">FROM node:22-slim\nWORKDIR \/app\nCOPY package.json package-lock.json .\/\nRUN npm ci --omit=dev\nCOPY server.js .\nENV PORT=3000\nCMD [\"node\", \"server.js\"]<\/code><\/pre>\n\n\n\n<p>Suba o container no servidor:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">docker build -t bot-whatsapp .\ndocker run -d --restart unless-stopped -p 127.0.0.1:3000:3000 \\\n  -e VERIFY_TOKEN -e ACCESS_TOKEN -e APP_SECRET -e PHONE_NUMBER_ID \\\n  bot-whatsapp<\/code><\/pre>\n\n\n\n<p>Para o HTTPS, o Caddy obt\u00e9m e renova o certificado sozinho quando o DNS do dom\u00ednio aponta para o servidor e as portas 80 e 443 est\u00e3o abertas. O <code>Caddyfile<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">bot.suaempresa.com.br\nreverse_proxy 127.0.0.1:3000<\/code><\/pre>\n\n\n\n<p>Atualize a URL de callback no painel para <code>https:\/\/bot.suaempresa.com.br\/webhook<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Checklist antes de colocar em produ\u00e7\u00e3o<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Deduplica\u00e7\u00e3o persistente.<\/strong> O <code>Set<\/code> em mem\u00f3ria some a cada rein\u00edcio e n\u00e3o \u00e9 compartilhado entre inst\u00e2ncias. Guarde os IDs no Redis ou no banco, com expira\u00e7\u00e3o.<\/li>\n\n\n<li><strong>Fila.<\/strong> Se a resposta depende de IA ou de sistemas lentos, mova o processamento para uma fila (BullMQ, RabbitMQ, SQS) com worker separado. Um processo que cai no meio do <code>processPayload<\/code> perde o evento, porque o <code>200<\/code> j\u00e1 foi enviado.<\/li>\n\n\n<li><strong>Uma inst\u00e2ncia por enquanto.<\/strong> Com estado em mem\u00f3ria, n\u00e3o rode em cluster. Ao escalar, mova o estado para fora do processo.<\/li>\n\n\n<li><strong>Erros da API.<\/strong> O c\u00f3digo registra o corpo de erro da Meta; a tabela de <a href=\"https:\/\/vectalk.com.br\/blog\/erros-api-whatsapp\/\">erros da API do WhatsApp<\/a> ajuda a interpretar.<\/li>\n\n\n<li><strong>Segredos fora do c\u00f3digo.<\/strong> Token e chave secreta s\u00f3 em vari\u00e1veis de ambiente ou cofre de segredos.<\/li>\n\n\n<li><strong>M\u00eddia.<\/strong> Imagens, \u00e1udios e documentos chegam no webhook s\u00f3 com um ID; o download \u00e9 uma chamada separada \u00e0 API de m\u00eddia.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Como fazer com a Vectalk<\/h2>\n\n\n\n<p>A Vectalk \u00e9 um proxy da WhatsApp Cloud API oficial: os payloads de envio e os eventos de webhook s\u00e3o os mesmos da Meta, ent\u00e3o o <code>server.js<\/code> funciona sem mudar a l\u00f3gica. Mudam tr\u00eas coisas:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>URL e token.<\/strong> Defina <code>API_URL=https:\/\/apis.vectalk.com.br\/api\/messages<\/code> e use em <code>ACCESS_TOKEN<\/code> um token de inst\u00e2ncia da Vectalk. Com ele, o <code>phone_number_id<\/code> \u00e9 resolvido automaticamente.<\/li>\n\n\n<li><strong>Cadastro do webhook por API.<\/strong> Registre a URL com <code>POST https:\/\/apis.vectalk.com.br\/api\/webhooks<\/code>, com <code>instance_id<\/code>, <code>url<\/code> e <code>name<\/code>. Voc\u00ea pode ter v\u00e1rias URLs por inst\u00e2ncia.<\/li>\n\n\n<li><strong>Prote\u00e7\u00e3o do endpoint.<\/strong> A <a href=\"https:\/\/docs.vectalk.com.br\/concepts\/webhooks\">documenta\u00e7\u00e3o de webhooks da Vectalk<\/a> n\u00e3o descreve um cabe\u00e7alho de assinatura. Deixe <code>APP_SECRET<\/code> vazio, defina <code>WEBHOOK_KEY<\/code> com um valor aleat\u00f3rio longo e cadastre a URL como <code>https:\/\/bot.suaempresa.com.br\/webhook?key=SEU_VALOR<\/code>.<\/li>\n\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">const r = await fetch('https:\/\/apis.vectalk.com.br\/api\/webhooks', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.VECTALK_TOKEN}`,\n    'Content-Type': 'application\/json',\n  },\n  body: JSON.stringify({\n    instance_id: 'SUA_INSTANCIA',\n    url: 'https:\/\/bot.suaempresa.com.br\/webhook?key=SEU_VALOR',\n    name: 'Bot Node.js',\n  }),\n});\nconsole.log(await r.json());<\/code><\/pre>\n\n\n\n<p>A Vectalk espera <code>200<\/code> em menos de 20 segundos e reenvia o evento se n\u00e3o receber, o que a deduplica\u00e7\u00e3o j\u00e1 cobre. Crie sua conta em <a href=\"https:\/\/apps.vectalk.com.br\/register\">apps.vectalk.com.br\/register<\/a> e veja os outros endpoints na <a href=\"https:\/\/docs.vectalk.com.br\">documenta\u00e7\u00e3o<\/a>. Para colocar IA nas respostas, o tutorial <a href=\"https:\/\/vectalk.com.br\/blog\/chatgpt-api-oficial-whatsapp\/\">ChatGPT na API oficial do WhatsApp<\/a> parte de uma base parecida.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Perguntas frequentes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Preciso de uma biblioteca de WhatsApp para Node.js?<\/h3>\n\n\n\n<p>N\u00e3o. A Cloud API \u00e9 HTTP com JSON: Express para receber e <code>fetch<\/code> para enviar bastam. Bibliotecas que automatizam o WhatsApp Web, como Baileys, s\u00e3o outra coisa: n\u00e3o usam a API oficial da Meta.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Por que a assinatura nunca bate?<\/h3>\n\n\n\n<p>Quase sempre \u00e9 o corpo: algum middleware j\u00e1 converteu o JSON antes do <code>express.raw<\/code>, ou o <code>type<\/code> n\u00e3o \u00e9 <code>application\/json<\/code>. Confira tamb\u00e9m se a chave \u00e9 a chave secreta do app.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">O bot pode iniciar a conversa?<\/h3>\n\n\n\n<p>Texto livre e bot\u00f5es s\u00f3 funcionam dentro da janela de 24 horas aberta pela \u00faltima mensagem do cliente. Fora dela, \u00e9 preciso enviar um template aprovado.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Quanto custa manter um bot respondendo?<\/h3>\n\n\n\n<p>Respostas dentro da janela de atendimento s\u00e3o mensagens de servi\u00e7o. Desde 1\u00ba de outubro de 2026, cada n\u00famero tem franquia de 1.000 mensagens de servi\u00e7o por m\u00eas; a partir da 1.001\u00aa, a tarifa para destinat\u00e1rios no Brasil \u00e9 de R$ 0,0350 por mensagem.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Posso usar TypeScript ou Fastify?<\/h3>\n\n\n\n<p>Pode. A sequ\u00eancia n\u00e3o muda: responder ao <code>GET<\/code> de verifica\u00e7\u00e3o, validar o HMAC sobre o corpo bruto, devolver <code>200<\/code> r\u00e1pido e chamar o endpoint de mensagens. No Fastify, garanta acesso ao corpo bruto antes que o JSON seja convertido.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>Fontes oficiais consultadas em 28\/10\/2026:<\/strong> <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/create-webhook-endpoint\" target=\"_blank\" rel=\"noreferrer noopener\">Create a webhook endpoint (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/overview\" target=\"_blank\" rel=\"noreferrer noopener\">Webhooks overview (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/messages\/interactive-reply-buttons-messages\" target=\"_blank\" rel=\"noreferrer noopener\">Interactive reply buttons messages (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/reference\/messages\/interactive\" target=\"_blank\" rel=\"noreferrer noopener\">Interactive messages webhook (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/typing-indicators\" target=\"_blank\" rel=\"noreferrer noopener\">Typing indicators (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/business-scoped-user-ids\" target=\"_blank\" rel=\"noreferrer noopener\">Business-scoped user IDs (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/pricing\" target=\"_blank\" rel=\"noreferrer noopener\">Pre\u00e7os (Meta)<\/a>, <a href=\"https:\/\/expressjs.com\/en\/5x\/api\/express\/\" target=\"_blank\" rel=\"noreferrer noopener\">Express 5: express.raw()<\/a>, <a href=\"https:\/\/expressjs.com\/en\/5x\/api.html\" target=\"_blank\" rel=\"noreferrer noopener\">Express 5.x API<\/a>, <a href=\"https:\/\/caddyserver.com\/docs\/quick-starts\/reverse-proxy\" target=\"_blank\" rel=\"noreferrer noopener\">Caddy: reverse proxy quick-start<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/concepts\/webhooks\">Vectalk: webhooks<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/authentication\">Vectalk: autentica\u00e7\u00e3o<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/api-reference\/messages\/send-buttons\">Vectalk: bot\u00f5es de resposta<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Tutorial completo para criar um bot de WhatsApp em Node.js com Express na API oficial: webhook, assinatura com express.raw, bot\u00f5es e deploy com HTTPS.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[8],"tags":[],"class_list":["post-540","post","type-post","status-publish","format-standard","hentry","category-uncategorized-pt"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Bot de WhatsApp com Node.js e Express na API | Vectalk<\/title>\n<meta name=\"description\" content=\"Tutorial WhatsApp API Node JS com Express: verifica\u00e7\u00e3o do webhook, assinatura com express.raw, fetch nativo, bot\u00f5es interativos e deploy com HTTPS.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Bot de WhatsApp com Node.js e Express na API | Vectalk\" \/>\n<meta property=\"og:description\" content=\"Tutorial WhatsApp API Node JS com Express: verifica\u00e7\u00e3o do webhook, assinatura com express.raw, fetch nativo, bot\u00f5es interativos e deploy com HTTPS.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/\" \/>\n<meta property=\"og:site_name\" content=\"Blog Vectalk\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-28T16:07:09+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-28T16:07:11+00:00\" \/>\n<meta name=\"author\" content=\"wanderleyfcunha\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"wanderleyfcunha\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"13 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/bot-whatsapp-nodejs-api-oficial\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/bot-whatsapp-nodejs-api-oficial\\\/\"},\"author\":{\"name\":\"wanderleyfcunha\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\"},\"headline\":\"Bot de WhatsApp com Node.js na API oficial (Express, do zero)\",\"datePublished\":\"2026-10-28T16:07:09+00:00\",\"dateModified\":\"2026-10-28T16:07:11+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/bot-whatsapp-nodejs-api-oficial\\\/\"},\"wordCount\":1728,\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/bot-whatsapp-nodejs-api-oficial\\\/\",\"url\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/bot-whatsapp-nodejs-api-oficial\\\/\",\"name\":\"Bot de WhatsApp com Node.js e Express na API | Vectalk\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#website\"},\"datePublished\":\"2026-10-28T16:07:09+00:00\",\"dateModified\":\"2026-10-28T16:07:11+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\"},\"description\":\"Tutorial WhatsApp API Node JS com Express: verifica\u00e7\u00e3o do webhook, assinatura com express.raw, fetch nativo, bot\u00f5es interativos e deploy com HTTPS.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/bot-whatsapp-nodejs-api-oficial\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/bot-whatsapp-nodejs-api-oficial\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/bot-whatsapp-nodejs-api-oficial\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Bot de WhatsApp com Node.js na API oficial (Express, do zero)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/\",\"name\":\"Blog Vectalk\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\",\"name\":\"wanderleyfcunha\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"caption\":\"wanderleyfcunha\"},\"sameAs\":[\"https:\\\/\\\/vectalk.com.br\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Bot de WhatsApp com Node.js e Express na API | Vectalk","description":"Tutorial WhatsApp API Node JS com Express: verifica\u00e7\u00e3o do webhook, assinatura com express.raw, fetch nativo, bot\u00f5es interativos e deploy com HTTPS.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/","og_locale":"en_US","og_type":"article","og_title":"Bot de WhatsApp com Node.js e Express na API | Vectalk","og_description":"Tutorial WhatsApp API Node JS com Express: verifica\u00e7\u00e3o do webhook, assinatura com express.raw, fetch nativo, bot\u00f5es interativos e deploy com HTTPS.","og_url":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/","og_site_name":"Blog Vectalk","article_published_time":"2026-10-28T16:07:09+00:00","article_modified_time":"2026-10-28T16:07:11+00:00","author":"wanderleyfcunha","twitter_card":"summary_large_image","twitter_misc":{"Written by":"wanderleyfcunha","Est. reading time":"13 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/#article","isPartOf":{"@id":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/"},"author":{"name":"wanderleyfcunha","@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b"},"headline":"Bot de WhatsApp com Node.js na API oficial (Express, do zero)","datePublished":"2026-10-28T16:07:09+00:00","dateModified":"2026-10-28T16:07:11+00:00","mainEntityOfPage":{"@id":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/"},"wordCount":1728,"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/","url":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/","name":"Bot de WhatsApp com Node.js e Express na API | Vectalk","isPartOf":{"@id":"https:\/\/vectalk.com.br\/blog\/#website"},"datePublished":"2026-10-28T16:07:09+00:00","dateModified":"2026-10-28T16:07:11+00:00","author":{"@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b"},"description":"Tutorial WhatsApp API Node JS com Express: verifica\u00e7\u00e3o do webhook, assinatura com express.raw, fetch nativo, bot\u00f5es interativos e deploy com HTTPS.","breadcrumb":{"@id":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-nodejs-api-oficial\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/vectalk.com.br\/blog\/"},{"@type":"ListItem","position":2,"name":"Bot de WhatsApp com Node.js na API oficial (Express, do zero)"}]},{"@type":"WebSite","@id":"https:\/\/vectalk.com.br\/blog\/#website","url":"https:\/\/vectalk.com.br\/blog\/","name":"Blog Vectalk","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/vectalk.com.br\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b","name":"wanderleyfcunha","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","caption":"wanderleyfcunha"},"sameAs":["https:\/\/vectalk.com.br\/blog"]}]}},"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/540","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/comments?post=540"}],"version-history":[{"count":1,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/540\/revisions"}],"predecessor-version":[{"id":543,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/540\/revisions\/543"}],"wp:attachment":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/media?parent=540"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/categories?post=540"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/tags?post=540"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}