Intercom Webhooks — Recepcion y Verificacion
Skill de referencia con codigo listo para usar (Express y FastAPI) para recibir, verificar y gestionar webhooks de Intercom via firma HMAC-SHA1. Cubre eventos de conversaciones, contactos y tickets.
Descarga abierta · sin registro · para Node.js, Python, Express
// Generated with: intercom-webhooks skill // https://github.com/hookdeck/webhook-skills // Cliente: Cultiva Desk — SaaS B2B atención al cliente con IA
'use strict';
const express = require('express'); const crypto = require('crypto'); const { Pool } = require('pg');
// ─── Config ────────────────────────────────────────────────────────────────── const PORT = process.env.PORT || 3000; const CLIENT_SECRET = process.env.INTERCOM_CLIENT_SECRET; const DATABASE_URL = process.env.DATABASE_URL;
const app = express(); const pool = new Pool({ connectionString: DATABASE_URL });
// ─── HMAC-SHA1 Verification ─────────────────────────────────────────────────── /**
- Verifica la firma X-Hub-Signature de Intercom.
- Intercom firma el raw body con HMAC-SHA1 usando el client_secret de la app.
- Cabecera recibida: "sha1=<40-hex-chars>"
- @param {Buffer} rawBody - Body sin parsear (express.raw)
- @param {string} signatureHeader - Valor de X-Hub-Signature
- @param {string} clientSecret - INTERCOM_CLIENT_SECRET de tu app
- @returns {boolean} */ function verifyIntercomWebhook(rawBody, signatureHeader, clientSecret) { if (!signatureHeader || !clientSecret) return false;
const [algorithm, signature] = signatureHeader.split('='); if (algorithm !== 'sha1' || !signature) return false;
const expected = crypto .createHmac('sha1', clientSecret) .update(rawBody) .digest('hex');
// timingSafeEqual previene timing attacks en la comparación try { return crypto.timingSafeEqual( Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'), ); } catch { // Longitudes distintas → firma inválida return false; } }
// ─── Audit Log ─────────────────────────────────────────────────────────────── /**
- Persiste cada notificación en webhook_logs para auditoría e idempotencia.
- Esquema:
- CREATE TABLE webhook_logs (
id BIGSERIAL PRIMARY KEY,notification_id TEXT NOT NULL UNIQUE, -- idempotency keytopic TEXT NOT NULL,status TEXT NOT NULL, -- 'processed' | 'duplicate' | 'error'payload JSONB,received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()- );
*/
async function logWebhook(notificationId, topic, status, payload) {
try {
await pool.query(
INSERT INTO webhook_logs (notification_id, topic, status, payload) VALUES ($1, $2, $3, $4) ON CONFLICT (notification_id) DO NOTHING, [notificationId, topic, status, JSON.stringify(payload)], ); } catch (err) { console.error('[webhook_logs] Error escribiendo log:', err.message); } }
// ─── Idempotencia ───────────────────────────────────────────────────────────── /**
- Devuelve true si notification_id ya fue procesado (evita duplicados en reintentos).
*/
async function isDuplicate(notificationId) {
const { rows } = await pool.query(
SELECT 1 FROM webhook_logs WHERE notification_id = $1 AND status = 'processed' LIMIT 1, [notificationId], ); return rows.length > 0; }
// ─── Handlers por topic ──────────────────────────────────────────────────────
async function handlePing(notification) { console.log('[ping] Handshake recibido — webhook registrado correctamente.'); }
async function handleConversationUserCreated(notification) {
const conv = notification.data.item;
console.log([conversation.user.created] Nueva conversación id=${conv.id});
// TODO: crear ticket en CRM interno, notificar al equipo via Slack, etc.
}
async function handleConversationUserReplied(notification) {
const conv = notification.data.item;
console.log([conversation.user.replied] Usuario respondió en conversación id=${conv.id});
// TODO: marcar conversación como "pendiente de revisión" en CRM.
}
async function handleConversationAdminAssigned(notification) {
const conv = notification.data.item;
const admin = conv.assignee;
console.log([conversation.admin.assigned] Conversación ${conv.id} asignada a admin ${admin?.id});
// Actualizar CRM interno: asignar agente
await pool.query(
UPDATE conversations SET assigned_agent_id = $1, updated_at = NOW() WHERE intercom_id = $2,
[admin?.id, conv.id],
);
}
async function handleContactUserCreated(notification) {
const contact = notification.data.item;
console.log([contact.user.created] Nuevo usuario id=${contact.id} email=${contact.email});
// TODO: sincronizar con CRM, disparar secuencia de onboarding.
}
async function handleContactLeadCreated(notification) {
const lead = notification.data.item;
console.log([contact.lead.created] Nuevo lead id=${lead.id} email=${lead.email});
// TODO: añadir a pipeline de ventas.
}
async function handleTicketCreated(notification) {
const ticket = notification.data.item;
console.log([ticket.created] Nuevo ticket id=${ticket.id} título="${ticket.ticket_attributes?.title}");
// TODO: abrir ticket en sistema interno, asignar SLA.
}
async function handleTicketStateUpdated(notification) {
const ticket = notification.data.item;
const newState = ticket.ticket_state?.state;
console.log([ticket.state.updated] Ticket ${ticket.id} → estado "${newState}");
await pool.query(
UPDATE tickets SET state = $1, updated_at = NOW() WHERE intercom_id = $2,
[newState, ticket.id],
);
}
// ─── Router de topics ───────────────────────────────────────────────────────── const TOPIC_HANDLERS = { 'ping': handlePing, 'conversation.user.created': handleConversationUserCreated, 'conversation.user.replied': handleConversationUserReplied, 'conversation.admin.assigned': handleConversationAdminAssigned, 'contact.user.created': handleContactUserCreated, 'contact.lead.created': handleContactLeadCreated, 'ticket.created': handleTicketCreated, 'ticket.state.updated': handleTicketStateUpdated, };
// ─── Endpoint principal ─────────────────────────────────────────────────────── // CRITICO: express.raw() — Intercom firma el raw body, no el JSON parseado. app.post( '/webhooks/intercom', express.raw({ type: 'application/json' }), async (req, res) => { // 1. Verificar firma ANTES de hacer nada más const signature = req.headers['x-hub-signature']; if (!verifyIntercomWebhook(req.body, signature, CLIENT_SECRET)) { console.error('[webhook] Verificación de firma FALLIDA'); return res.status(401).json({ error: 'Invalid signature' }); }
// 2. Parsear payload tras verificación
let notification;
try {
notification = JSON.parse(req.body.toString('utf8'));
} catch (err) {
console.error('[webhook] JSON inválido:', err.message);
return res.status(400).json({ error: 'Invalid JSON' });
}
const { id: notificationId, topic } = notification;
// 3. Responder 200 OK inmediatamente (< 200 ms — Intercom reintenta si tarda más)
res.status(200).json({ received: true });
// 4. Procesamiento asíncrono tras responder
setImmediate(async () => {
// Idempotencia: evitar reprocesamiento en reintentos de Intercom
if (await isDuplicate(notificationId)) {
console.log(`[webhook] Duplicado ignorado: notification_id=${notificationId}`);
await logWebhook(notificationId, topic, 'duplicate', notification);
return;
}
const handler = TOPIC_HANDLERS[topic];
if (!handler) {
console.warn(`[webhook] Topic no manejado: "${topic}"`);
await logWebhook(notificationId, topic, 'unhandled', notification);
return;
}
try {
await handler(notification);
await logWebhook(notificationId, topic, 'processed', notification);
console.log(`[webhook] OK — topic="${topic}" id="${notificationId}"`);
} catch (err) {
console.error(`[webhook] Error procesando topic="${topic}":`, err.message);
await logWebhook(notificationId, topic, 'error', { error: err.message, notification });
}
});
}, );
// ─── Health check ───────────────────────────────────────────────────────────── app.get('/health', (_req, res) => res.json({ status: 'ok', service: 'cultiva-desk-webhooks' }));
// ─── Arrancar servidor ────────────────────────────────────────────────────────
app.listen(PORT, () => {
console.log(Cultiva Desk — Intercom webhook handler escuchando en :${PORT});
console.log(Endpoint: POST http://localhost:${PORT}/webhooks/intercom);
console.log('Desarrollo local: npx hookdeck-cli listen', PORT, 'intercom --path /webhooks/intercom');
});
module.exports = app; // para tests
// qué_hace
Proporciona codigo de referencia para recibir y verificar webhooks de Intercom con HMAC-SHA1 en Express y FastAPI.
// cómo_lo_hace
Verifica la cabecera X-Hub-Signature con el client_secret de la app y enruta los eventos por topic (conversacion, contacto, ticket).
// ejemplo_de_uso
Úsala cuando integras Intercom en tu backend y necesitas verificar que los eventos que recibes son auténticos antes de procesar la lógica. Ej.: el handler de Express verifica la firma HMAC-SHA1 antes de procesar el evento de cierre de conversación.
// plataformas
// opiniones_de_la_comunidad
Opiniones
Cargando opiniones…
// pase_cultiva_ia
Llévate todo el arsenal con el Pase
Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.
Pago único · IVA incluido · pago seguro con Stripe.
Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.