← Volver al catálogo
Atención al clienteReferenciaIntermedioGratis

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.

Descargar skill (.zip)

Descarga abierta · sin registro · para Node.js, Python, Express

// resultado_de_ejemplo

// 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 key
    
  • topic           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

Node.jsPythonExpressFastAPIIntercom
Categoría
Atención al cliente
Tipo
Referencia
Nivel
Intermedio
Licencia
MIT
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// 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 €.