// webhook-server.ts
import express from 'express';
import crypto from 'crypto';
const app = express();
const SECRET = process.env.CORDIALY_WEBHOOK_SECRET!;
// ⚠️ IMPORTANTE: usar raw body para validar a assinatura
app.use('/webhook/cordialy', express.raw({ type: 'application/json' }));
function validarAssinatura(payload: Buffer, signature: string): boolean {
const esperado = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(payload)
.digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(esperado));
} catch {
return false;
}
}
app.post('/webhook/cordialy', async (req, res) => {
const signature = req.headers['x-cordialy-signature'] as string;
if (!signature || !validarAssinatura(req.body, signature)) {
return res.status(401).json({ error: 'Assinatura inválida' });
}
// Responde imediatamente — processa em background
res.status(200).json({ received: true });
const evento = JSON.parse(req.body.toString());
await processarEvento(evento);
});
async function processarEvento(evento: any) {
console.log(`[webhook] ${evento.event} — lead: ${evento.data?.lead_id}`);
switch (evento.event) {
case 'lead.created':
await onLeadCriado(evento.data);
break;
case 'lead.status_changed':
await onStatusAlterado(evento.data);
break;
case 'message.received':
await onMensagemRecebida(evento.data);
break;
case 'session.ended':
await onSessaoEncerrada(evento.data);
break;
default:
console.log(`[webhook] evento não tratado: ${evento.event}`);
}
}
async function onLeadCriado(data: any) {
// Sincroniza com seu CRM
await crm.createContact({
phone: data.customer_phone,
name: data.name,
externalId: data.lead_id,
});
}
async function onStatusAlterado(data: any) {
if (data.new_status === 'converted') {
await crm.markAsWon(data.lead_id);
await notificar(`Lead ${data.lead_id} convertido!`);
}
}
async function onMensagemRecebida(data: any) {
// Notifica o time de vendas
await slack.send(`#vendas`, `Nova mensagem do lead ${data.lead_id}: "${data.content}"`);
}
async function onSessaoEncerrada(data: any) {
await crm.updateDeal(data.lead_id, {
status: data.status,
duration: calcularDuracao(data.started_at, data.ended_at),
});
}
app.listen(3000, () => console.log('Webhook server rodando na porta 3000'));
Use
express.raw() no Node.js e $request->getContent() no PHP para obter o body bruto. Se usar express.json() antes, o body já foi parseado e a assinatura não bate.