Skip to content

Chatbot basico (echo)

Receita minima — todo ola que chega volta como “voce disse: ola”. Pode evoluir trocando a logica de resposta por chamada a LLM, lookup em base, etc.

Pre-requisitos

  • App criada com webhook_url apontando pro seu servidor
  • Chip vinculado com can_receive=true E can_send=true
  • webhook_secret salvo em variavel de ambiente

Codigo (Node + Express)

import 'dotenv/config'
import express from 'express'
import crypto from 'node:crypto'
const HUB = process.env.WPPHUB_BASE_URL // https://hub.gustavomaritan.com
const APP_KEY = process.env.WPPHUB_APP_KEY // ak_live_...
const SECRET = process.env.WPPHUB_WEBHOOK_SECRET // whsec_...
const PORT = process.env.PORT || 3001
const app = express()
// CRITICO: precisa do body bruto pra validar HMAC.
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
// 1. Valida HMAC
const sig = req.header('x-hub-signature') || ''
const expected =
'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex')
if (
sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
) {
return res.status(401).send('invalid signature')
}
const payload = JSON.parse(req.body.toString())
// 2. So responde a `message.received` direction=in (mensagens recebidas).
if (payload.event !== 'message.received' || payload.data.direction !== 'in') {
return res.sendStatus(200) // ignora outros eventos
}
// 3. Ignora se nao for texto (pra simplificar)
if (payload.data.type !== 'text') {
return res.sendStatus(200)
}
// 4. Responde imediatamente 2xx pra hub nao retentar
res.sendStatus(200)
// 5. Dispara resposta async
const m = payload.data
const reply = `voce disse: ${m.content.text}`
try {
await sendMessage({
chipId: m.chip_id,
to: m.sender.phone_number || m.from, // fallback pra JID se phone vazio
text: reply,
idempotencyKey: `echo-${m.message_id}`, // dedup
})
} catch (e) {
console.error('failed to reply', e.message)
}
})
async function sendMessage({ chipId, to, text, idempotencyKey }) {
const res = await fetch(`${HUB}/v1/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${APP_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
chip_id: chipId,
to,
type: 'text',
content: { text },
idempotency_key: idempotencyKey,
}),
})
if (!res.ok) {
const problem = await res.json().catch(() => ({}))
throw new Error(`${res.status} ${problem.code || ''}: ${problem.detail || ''}`)
}
return res.json()
}
app.listen(PORT, () => console.log(`echo bot on :${PORT}`))

package.json

{
"type": "module",
"dependencies": {
"dotenv": "^16",
"express": "^4"
}
}

.env

WPPHUB_BASE_URL=https://hub.gustavomaritan.com
WPPHUB_APP_KEY=ak_live_...
WPPHUB_WEBHOOK_SECRET=whsec_...
PORT=3001

Como rodar

Terminal window
npm install
node server.js

Expor via ngrok http 3001 em DEV, configurar a URL no campo webhook_url da app.

Variacoes

Responder so a comandos especificos

const text = m.content.text.toLowerCase().trim()
if (text === '/hora') {
await sendMessage({ ..., text: `agora: ${new Date().toLocaleString('pt-BR')}` })
} else if (text.startsWith('/cep ')) {
const cep = text.slice(5)
// chama ViaCEP, responde endereco
}

Ignorar mensagens de grupo

if (m.chat.is_group) return res.sendStatus(200)

Filtrar por business contact

if (!m.sender.is_business) return res.sendStatus(200) // so responde a contas business

LLM como cerebro

const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: 'Atendente da loja X. Responda curto.' },
{ role: 'user', content: m.content.text },
],
})
const reply = response.choices[0].message.content
await sendMessage({ ..., text: reply })

Cuidados

  1. Rate limit do chip: se 100 pessoas mandam ao mesmo tempo, sua respostas vao acumular na fila do hub (rate limit + jitter). Backoff natural — nao precisa rate limit do seu lado.
  2. Loops infinitos: se voce responde a TODA mensagem recebida, e um contato comeca a responder, vai loopar. Mantenha logica defensiva (ignorar from_me, ignorar mensagens recentes do proprio bot).
  3. Idempotency: sempre mande idempotency_key. Se webhook chegar duplicado (retry hub), voce nao reenvia.