CRPRO HubEntrar no painel

Como criar um agente de IA no WhatsApp pela API oficial

Um agente de IA no WhatsApp oficial é um laço curto: a mensagem do cliente chega por webhook, um modelo de linguagem escreve a resposta e a API envia. O difícil não é a chamada ao modelo — é o que fica em volta dela: responder dentro do tempo do webhook, lembrar a conversa, saber a hora de chamar uma pessoa e não brigar com quem já está atendendo pelo celular. Este guia mostra um agente completo e o que costuma quebrar em produção.

O que a Meta permite

Bot que atende os clientes da própria empresa — tirar dúvida, acompanhar pedido, agendar, qualificar e passar para a equipe — é o uso para o qual a plataforma existe. O que os termos da Meta para a WhatsApp Business Platform restringem, na seção de provedores de IA, é usar a plataforma para oferecer ou vender a própria tecnologia de IA como produto principal — o assistente de uso geral, do tipo “converse com um modelo pelo WhatsApp”. Se o seu agente responde sobre o seu negócio, ele está do lado permitido.

A arquitetura

  • Webhook. O CRPRO Hub entrega message.received. O seu endpoint verifica a assinatura, deduplica pelo id do evento, coloca numa fila e responde 2xx — em até 10 segundos. Um modelo pode levar mais que isso para responder; por isso a chamada a ele não fica no caminho do webhook. Veja receber mensagens.
  • Worker. Tira o evento da fila, monta o histórico daquela conversa, chama o modelo e envia a resposta pela API do Hub.
  • Estado. Duas coisas por conversa: o histórico e se uma pessoa já assumiu. Num banco, não em memória, para sobreviver a um deploy.

O código

O exemplo usa a API da Anthropic; qualquer modelo com API de chat entra no mesmo lugar. Ele é a função do worker — recebe o evento já verificado e deduplicado:

Preparo
npm install @anthropic-ai/sdk

// agente.js
import Anthropic from '@anthropic-ai/sdk'

const claude = new Anthropic() // le ANTHROPIC_API_KEY do ambiente
agente.js
const MODELO = 'claude-sonnet-5-5'
const SISTEMA = `Você é o atendente virtual da Loja Exemplo no WhatsApp.
Responda em português, em mensagens curtas, e use só as informações abaixo.
Se não souber a resposta, se o cliente pedir uma pessoa, ou se o assunto for
reclamação, cancelamento ou pagamento, responda exatamente [[HUMANO]].

Horário de atendimento: segunda a sexta, das 9h às 18h.
Prazo de entrega: até 3 dias úteis.`

const historicos = new Map() // canal:contato -> mensagens; use um banco
const comHumano = new Set() // conversas que uma pessoa assumiu
const respostas = new Map() // message_id recebido -> texto ja gerado

async function aoReceber(evento) {
  // Em coexistencia, o que a equipe digita no celular chega como
  // message.echo: alguem assumiu, e o bot sai da conversa.
  if (evento.event === 'message.echo') {
    comHumano.add(`${evento.channel_id}:${evento.data.to}`)
    return
  }
  if (evento.event !== 'message.received') return
  const conversa = `${evento.channel_id}:${evento.data.from}`
  if (comHumano.has(conversa)) return

  if (!evento.data.text) {
    return enviar(evento, 'Por enquanto eu só leio texto. Pode escrever a sua dúvida?')
  }

  const historico = [...(historicos.get(conversa) ?? []), { role: 'user', content: evento.data.text }]
  // Reprocessar o mesmo evento reaproveita o texto ja gerado: o corpo do envio
  // fica identico e a API devolve o envio original. Chamar o modelo de novo
  // geraria outro texto, e a mesma Idempotency-Key com outro corpo da 409.
  let texto = respostas.get(evento.data.message_id)
  if (texto === undefined) {
    const resposta = await claude.messages.create({
      model: MODELO,
      max_tokens: 500,
      system: SISTEMA,
      messages: aparar(historico),
    })
    texto = resposta.content
      .filter((bloco) => bloco.type === 'text')
      .map((bloco) => bloco.text)
      .join('\n')
      .trim()
    respostas.set(evento.data.message_id, texto)
  }

  if (!texto || texto.includes('[[HUMANO]]')) {
    comHumano.add(conversa)
    await avisarEquipe(evento) // e-mail, Slack, CRM: o que a sua equipe usa
    return enviar(evento, 'Vou chamar uma pessoa da equipe para continuar com você.')
  }
  const envio = await enviar(evento, texto)
  // So entra no historico o que o cliente de fato recebeu.
  if (envio.ok) historicos.set(conversa, aparar([...historico, { role: 'assistant', content: texto }]))
  return envio
}

// As ultimas 20 mensagens, sempre comecando por uma do usuario.
function aparar(historico) {
  const recente = historico.slice(-20)
  while (recente.length && recente[0].role !== 'user') recente.shift()
  return recente
}

function enviar(evento, texto) {
  return fetch(`https://crprohub.com/api/v1/channels/${evento.channel_id}/messages`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.CRPRO_API_KEY}`,
      'Content-Type': 'application/json',
      // Uma resposta por mensagem recebida, mesmo se o evento for reprocessado.
      'Idempotency-Key': `ia-${evento.data.message_id}`,
    },
    body: JSON.stringify({ to: evento.data.from, type: 'text', text: { body: texto.slice(0, 4096) } }),
  })
}

Quatro decisões nesse código são de propósito:

  • O prompt de sistema limita o assunto. O agente responde com as informações que você deu e, fora delas, chama uma pessoa. Um agente que inventa prazo de entrega gera mais atendimento do que economiza.
  • O histórico é curto. As últimas 20 mensagens, sempre começando por uma do cliente. Conversa longa custa mais a cada resposta e quase nunca precisa do começo.
  • A chave de idempotência sai da mensagem recebida. Se o mesmo evento for processado duas vezes, o worker reaproveita o texto que já gerou, o corpo do envio sai idêntico e a API devolve o envio original em vez de mandar outra resposta. Chamar o modelo de novo geraria outro texto, e a mesma chave com outro corpo volta 409 IDEMPOTENCY_KEY_REUSED. E só entra no histórico o que o cliente de fato recebeu.
  • Mídia recebe um aviso, não um palpite. Áudio e imagem chegam sem texto; responder sem entender o conteúdo é pior do que pedir que a pessoa escreva.

O código é executado nos testes do CRPRO Hub com o modelo simulado: a passagem para humano, o eco, o histórico e o corpo de envio são conferidos contra a validação real da API.

Passagem para humano

Todo agente precisa de uma saída. No exemplo, o modelo responde um marcador quando não sabe ou quando o assunto pede uma pessoa; o worker então avisa a equipe, manda uma frase ao cliente e deixa de responder aquela conversa.

Em coexistência, há um sinal ainda melhor: quando alguém da equipe responde pelo celular, o Hub entrega message.echo. O worker trata esse evento como “uma pessoa assumiu” e sai da conversa sozinho — o bot não atropela o atendente. Para isso, o endpoint precisa assinar também message.echo. Devolver a conversa ao bot é decisão sua: por tempo sem mensagens, por um comando da equipe ou quando o atendimento é encerrado no seu sistema.

O que costuma quebrar

  • Chamar o modelo dentro do webhook. Passou de 10 segundos, a entrega volta, o modelo é chamado de novo e o endpoint caminha para a pausa. Fila primeiro, sempre.
  • Responder ao próprio envio. Endpoint que assina todos os eventos recebe message.sent e message.status das respostas do bot. Filtre por message.received.
  • Mensagens em rajada. O cliente manda três mensagens seguidas e o worker responde três vezes, fora de ordem. Processe uma conversa por vez, ou espere alguns segundos e junte as mensagens antes de chamar o modelo.
  • Resposta atrasada. Resposta logo depois da mensagem do cliente está dentro da janela de 24 horas. Um follow-up do bot no dia seguinte não está, e volta com o erro 131047: fora da janela, só template.
  • Bot conversando com bot. Dois sistemas automáticos respondendo um ao outro esbarram no limite de mensagens para o mesmo contato (erro 131056). Ponha um teto de respostas por conversa por minuto.

Quanto custa

São duas contas. A do modelo, por volume de texto processado — o histórico curto e o prompt objetivo são o que a mantém baixa. E a da Meta: resposta dentro da janela de atendimento é mensagem de serviço, que passou a ser cobrada em outubro de 2026 depois de uma franquia mensal por número. Os números e a calculadora estão em preços.

Sem programar, o mesmo resultado — atendimento com IA no WhatsApp oficial, funil e passagem para a equipe — está pronto no CRPRO CRM, o produto de atendimento da mesma empresa. Este guia é para quem quer o agente dentro do próprio sistema.