CRPRO HubEntrar no painel

Como ligar o Chatwoot ao WhatsApp oficial

O Chatwoot é a caixa de entrada onde a equipe atende; o CRPRO Hub é a conexão oficial com o WhatsApp. Uma ponte liga os dois: o que o cliente escreve no WhatsApp vira mensagem na conversa do Chatwoot, e o que o agente responde no Chatwoot sai pelo número oficial. Este guia mostra a ponte, com a verificação de assinatura dos dois lados e o filtro que evita a mensagem dar voltas.

Por que uma caixa do tipo API

O canal de WhatsApp nativo do Chatwoot pede as credenciais da Meta direto — o id do número, o id da conta e um token, ou um aplicativo seu na Meta para o cadastro embutido. Com o CRPRO Hub, quem tem a conexão com a Meta é o Hub, e o número pode continuar no celular por coexistência. Do lado do Chatwoot, basta uma caixa de entrada do tipo API, feita justamente para canais que chegam por integração.

Preparar o Chatwoot

  • Em Configurações > Caixas de entrada > Adicionar, escolha API, dê um nome e preencha a URL de callback com o endereço da ponte. É para essa URL que o Chatwoot manda as mensagens dos agentes.
  • Adicione os agentes que vão atender essa caixa.
  • Nas configurações da caixa, copie o inbox_identifier — ele identifica a caixa na Client API, que a ponte usa para registrar as mensagens dos clientes — e o segredo da callback, que assina cada chamada do Chatwoot.
  • No CRPRO Hub, crie um endpoint de webhook para a ponte assinando message.received — veja configurar webhook.

Os dois sentidos

  • Cliente → Chatwoot. O Hub entrega message.received. Na primeira mensagem de um número, a ponte cria o contato e a conversa pela Client API e guarda o source_id e o id da conversa; nas seguintes, só acrescenta a mensagem. Mensagem criada pela Client API entra como sendo do contato.
  • Agente → WhatsApp. O Chatwoot chama a URL de callback com message_created. A ponte acha o número pelo source_id da conversa e envia pela API do Hub.

Evitar o laço

A callback do Chatwoot dispara para toda mensagem criada na conversa — inclusive a do cliente que a própria ponte acabou de registrar. Sem filtro, a ponte devolve ao cliente o que ele mesmo escreveu. Só a resposta pública de um agente segue para o WhatsApp:

Filtro
// So a resposta de um agente vai para o WhatsApp. A callback do Chatwoot
// dispara tambem para a mensagem do cliente que a propria ponte criou
// (incoming) e para nota privada: encaminhar essas fecha um laco.
function deveEncaminhar(evento) {
  return (
    evento.event === 'message_created' &&
    evento.message_type === 'outgoing' &&
    evento.private !== true &&
    Boolean(evento.content)
  )
}

Do lado do Hub, vale o mesmo: só message.received vira mensagem nova no Chatwoot. Assine também message.status se for atualizar o status de entrega (mais abaixo), mas nunca transforme message.sent ou message.status em mensagem na conversa. Em coexistência, o que a equipe responde pelo celular chega como message.echo; espelhar isso no Chatwoot como saída faz a callback disparar e a ponte reenviar, então deixe o eco de fora ou marque e ignore essas mensagens.

Verificar o Chatwoot

A URL de callback é pública, então a ponte confere quem está chamando. As versões atuais do Chatwoot assinam cada entrega com o segredo da caixa: HMAC SHA-256 de timestamp.corpo, no header X-Chatwoot-Signature, com o timestamp em X-Chatwoot-Timestamp. Como na verificação do Hub, leia o corpo cru antes de qualquer parse. Instância antiga que não mande esses headers precisa ser atualizada antes de ir para produção.

Verificar a callback
const { createHmac, timingSafeEqual } = require('node:crypto')

// O Chatwoot assina "timestamp.corpo cru" com o segredo da caixa de entrada.
// Rejeitar timestamp velho (5 minutos) evita reaproveitamento de entrega.
function chatwootValido(headers, rawBody, segredo, agora = Date.now()) {
  const timestamp = headers['x-chatwoot-timestamp'] ?? ''
  const recebida = (headers['x-chatwoot-signature'] ?? '').replace('sha256=', '')
  if (!/^[0-9a-f]{64}$/.test(recebida)) return false
  if (Math.abs(agora / 1000 - Number(timestamp)) > 300) return false
  const esperada = createHmac('sha256', segredo).update(`${timestamp}.${rawBody}`).digest('hex')
  return timingSafeEqual(Buffer.from(recebida, 'hex'), Buffer.from(esperada, 'hex'))
}

A ponte

As duas funções abaixo pressupõem que a assinatura de cada lado já foi verificada — a do Hub está em configurar webhook — e que a entrega do Hub já foi deduplicada pelo id do evento. O Idempotency-Key do envio sai do id da mensagem no Chatwoot: se a mesma callback chegar duas vezes, a API devolve o resultado original em vez de mandar de novo.

ponte.js
const CHATWOOT = 'https://app.chatwoot.com' // ou a URL da sua instancia
const INBOX = process.env.CHATWOOT_INBOX_IDENTIFIER // Configuracoes da caixa API
const CANAL = process.env.CRPRO_CANAL // o canal (numero) ligado a esta caixa
const contatos = new Map() // telefone -> { sourceId, conversaId }; use um banco
const telefones = new Map() // sourceId -> telefone
const enviadas = new Map() // wamid -> id da mensagem no Chatwoot, para o status

// 1. Mensagem do cliente: do Hub para o Chatwoot, pela Client API.
async function aoReceberDoHub(evento) {
  // Um endpoint do Hub pode receber todos os canais da organizacao; esta
  // caixa so atende um numero, e e por ele que as respostas saem.
  if (evento.event !== 'message.received' || evento.channel_id !== CANAL) return
  const telefone = evento.data.from
  let contato = contatos.get(telefone)
  if (!contato) {
    const criado = await chatwoot(`/contacts`, {
      identifier: telefone,
      name: evento.data.contact?.name || telefone,
      phone_number: `+${telefone}`,
    })
    const conversa = await chatwoot(`/contacts/${criado.source_id}/conversations`, {})
    contato = { sourceId: criado.source_id, conversaId: conversa.id }
    contatos.set(telefone, contato)
    telefones.set(criado.source_id, telefone)
  }
  await chatwoot(`/contacts/${contato.sourceId}/conversations/${contato.conversaId}/messages`, {
    content: evento.data.text || '[mensagem sem texto]',
  })
}

// 2. Resposta do agente: do Chatwoot para o Hub, pela callback da caixa API.
async function aoReceberDoChatwoot(evento) {
  if (!deveEncaminhar(evento)) return
  const telefone = telefones.get(evento.conversation?.contact_inbox?.source_id)
  if (!telefone) return
  const resposta = await fetch(`https://crprohub.com/api/v1/channels/${CANAL}/messages`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.CRPRO_API_KEY}`,
      'Content-Type': 'application/json',
      // Estavel por mensagem do Chatwoot: a callback repetida nao duplica.
      'Idempotency-Key': `chatwoot-${evento.account?.id}-${evento.id}`,
    },
    body: JSON.stringify({ to: telefone, type: 'text', text: { body: evento.content.slice(0, 4096) } }),
  })
  // Guarde o wamid para atualizar o status no Chatwoot quando chegar o
  // message.status. Fora da janela de 24 horas a Meta recusa texto livre, e o
  // Chatwoot nao sabe disso: um 502 aqui tambem precisa virar aviso ao agente.
  if (resposta.ok) {
    const corpo = await resposta.json()
    enviadas.set(corpo.data.message_id, evento.id)
  }
  return resposta.status
}

async function chatwoot(caminho, corpo) {
  const resposta = await fetch(`${CHATWOOT}/public/api/v1/inboxes/${INBOX}${caminho}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(corpo),
  })
  return resposta.json()
}
Os mapas em memória são para leitura. Em produção, guarde o par telefone ↔ source_id num banco: perder esse mapa faz a ponte criar um contato novo para cada cliente. Estes trechos são executados nos testes do CRPRO Hub, com chamadas simuladas, e o envio que eles produzem passa na validação da API de envio.

Janela de 24 horas e status de entrega

A caixa do tipo API não sabe da regra da janela: o Chatwoot deixa o agente responder a qualquer momento. Mas o WhatsApp só aceita texto livre até 24 horas depois da última mensagem do cliente; depois disso, a Meta recusa com o erro 131047 e só sai template. Avise o agente quando isso acontecer:

  • ao enviar, guarde o message_id que o Hub devolve junto com a conversa e a mensagem do Chatwoot;
  • quando chegar message.status para esse id, atualize a mensagem no Chatwoot pela API de aplicação, que aceita os estados enviado, entregue, lido e falhou — com o motivo da falha — em caixas do tipo API;
  • se o próprio envio voltar 502 META_SEND_FAILED, marque a mensagem como falha na hora.

No n8n

O n8n não tem nó oficial do Chatwoot, só nós da comunidade. A ponte cabe em dois fluxos com nós genéricos: um Webhook para o Hub, com o Code de verificação do guia de n8n e HTTP Requests para a Client API do Chatwoot; e outro Webhook para a callback do Chatwoot, com um Code para a assinatura dele (o trecho acima, adaptado como no guia), o filtro e um HTTP Request para o envio no Hub. O par telefone ↔ source_id cabe numa tabela do n8n.