Pasar al contenido principal
osky
Memoria persistente en tu bot de WhatsApp: cómo preservar contexto entre canales con Sinch y Redis
Tema Sinch + Redis + WhatsApp context persistence

Por qué tu bot tiene amnesia y cómo curarla

Imagina que llevas tres años comprando en la misma tienda. Conoces al dueño, él sabe tu nombre, sabe que siempre pides lo mismo, sabe que la última vez tuviste un problema con un envío. Ahora imagina que cada vez que entras, el dueño te mira como si fuera la primera vez en su vida que te ve. Eso es exactamente lo que le pasa a tus clientes cuando tu bot de WhatsApp no preserva contexto entre sesiones y canales. No es un problema menor de UX: es una grieta en la confianza que erosiona retención. El blog de Sinch lo documentó con datos duros: los clientes que experimentan discontinuidad de contexto al cambiar de chatbot a agente humano, o de WhatsApp a email, reportan frustración activa y abandonan la conversación a tasas significativamente más altas. En 2026, con la infraestructura disponible, no hay excusa técnica para que esto siga pasando.

Diagrama de flujo de contexto entre canales de comunicación

Este tutorial te lleva paso a paso a implementar un sistema de memoria persistente para tu bot de WhatsApp usando Sinch Conversation API, Redis como almacén de contexto en caliente, y Node.js como capa de orquestación. Al final tendrás un bot que recuerda: canal de origen, historial reciente, datos de perfil del usuario y el punto exacto donde quedó la última conversación, sin importar cuántos días hayan pasado ni por cuál canal regrese el cliente.


Requisitos

  • Cuenta activa en Sinch con acceso a Conversation API (plan Developer o superior)
  • Número de WhatsApp Business verificado vinculado a Sinch
  • Node.js 20+ instalado
  • Redis 7+ (puedes usar Redis Cloud free tier para empezar)
  • ngrok o un servidor con IP pública para recibir webhooks en desarrollo
  • Variables de entorno configuradas (.env con tus credenciales)

Paso 1: Configura tu App en Sinch Conversation API

Entra al Sinch Dashboard, crea una nueva Conversation App y vincula tu canal de WhatsApp. Anota tu APP_ID y genera un ACCESS_TOKEN de servicio.

# Instala dependencias del proyecto
npm init -y
npm install express axios redis dotenv

Crea tu .env:

SINCH_APP_ID=tu_app_id_aqui
SINCH_ACCESS_TOKEN=tu_access_token_aqui
SINCH_PROJECT_ID=tu_project_id_aqui
REDIS_URL=redis://localhost:6379
PORT=3000

Paso 2: Inicializa el cliente de Redis con TTL inteligente

El contexto en caliente no necesita vivir para siempre. Define un TTL base de 30 días para usuarios activos y renuévalo en cada interacción. Si el usuario no vuelve en 30 días, el contexto expira limpiamente.

// redis.js
import { createClient } from 'redis';

const client = createClient({ url: process.env.REDIS_URL });
client.on('error', (err) => console.error('Redis error:', err));
await client.connect();

const CTX_TTL = 60 * 60 * 24 * 30; // 30 días en segundos

export async function getContext(contactId) {
  const raw = await client.get(`ctx:${contactId}`);
  return raw ? JSON.parse(raw) : null;
}

export async function setContext(contactId, context) {
  await client.set(
    `ctx:${contactId}`,
    JSON.stringify(context),
    { EX: CTX_TTL }
  );
}

export async function mergeContext(contactId, patch) {
  const existing = (await getContext(contactId)) || {};
  const updated = { ...existing, ...patch, updatedAt: Date.now() };
  await setContext(contactId, updated);
  return updated;
}

Paso 3: Construye el webhook receptor de Sinch

Sinch envía eventos a tu endpoint cuando llega un mensaje. Aquí capturas el mensaje, recuperas el contexto del usuario, lo enriqueces y ejecutas tu lógica de negocio.

// server.js
import express from 'express';
import { getContext, mergeContext } from './redis.js';
import { sendReply } from './sinch.js';

const app = express();
app.use(express.json());

app.post('/webhook', async (req, res) => {
  res.sendStatus(200); // Responde rápido a Sinch, procesa después

  const event = req.body;
  if (event.message_event?.message?.text) {
    const contactId = event.message_event.contact_id;
    const channel = event.message_event.channel_identity?.channel || 'WHATSAPP';
    const incomingText = event.message_event.message.text.text;

    // Recupera o inicializa contexto
    const ctx = await mergeContext(contactId, {
      lastChannel: channel,
      lastMessage: incomingText,
      lastSeen: Date.now(),
    });

    await handleMessage(contactId, incomingText, ctx, event.message_event.conversation_id);
  }
});

async function handleMessage(contactId, text, ctx, conversationId) {
  const isReturning = ctx.visitCount > 1;
  const greeting = isReturning
    ? `Bienvenido de vuelta. La última vez que hablamos fue el ${new Date(ctx.lastSeen - 1).toLocaleDateString('es-MX')}.`
    : 'Hola, es tu primera vez por aquí. Cuéntame en qué te ayudo.';

  // Actualiza contador de visitas
  await mergeContext(contactId, { visitCount: (ctx.visitCount || 0) + 1 });

  await sendReply(conversationId, greeting);
}

app.listen(process.env.PORT, () => {
  console.log(`Webhook activo en puerto ${process.env.PORT}`);
});

Paso 4: Capa de envío hacia Sinch

// sinch.js
import axios from 'axios';

const BASE = `https://us.conversation.api.sinch.com/v1/projects/${process.env.SINCH_PROJECT_ID}`;

export async function sendReply(conversationId, text) {
  await axios.post(
    `${BASE}/messages:send`,
    {
      app_id: process.env.SINCH_APP_ID,
      message: { text_message: { text } },
      conversation_id: conversationId,
    },
    {
      headers: {
        Authorization: `Bearer ${process.env.SINCH_ACCESS_TOKEN}`,
        'Content-Type': 'application/json',
      },
    }
  );
}

Paso 5: Handoff a agente humano con contexto intacto

El momento más crítico es cuando escalas a un agente humano. Aquí es donde el 80% de los bots falla: el agente recibe el ticket sin historial. Con tu Redis ya tienes todo listo para serializar el contexto y enviarlo como metadata al sistema de tickets (Zendesk, Freshdesk, lo que uses).

export async function escalateToAgent(contactId, conversationId) {
  const ctx = await getContext(contactId);

  // Ejemplo: crea ticket en Zendesk con contexto completo
  await axios.post('https://tusubdominio.zendesk.com/api/v2/tickets.json', {
    ticket: {
      subject: `Escalación WhatsApp - ${contactId}`,
      comment: {
        body: `Historial de contexto:\n${JSON.stringify(ctx, null, 2)}`,
      },
      tags: [ctx.lastChannel?.toLowerCase(), 'whatsapp-bot'],
      custom_fields: [
        { id: TU_CAMPO_CUSTOM_ID, value: ctx.lastMessage },
      ],
    },
  }, {
    auth: { username: 'tu@!zilch!email.com/token', password: 'TU_ZENDESK_TOKEN' },
  });
}

Advertencias de producción

No guardes PII crudo en Redis sin cifrar. Si el contexto incluye nombres, teléfonos o datos de pago, cifra el valor antes de hacer JSON.stringify o usa Redis con TLS y cifrado en reposo. En México aplica LFPDPPP; en otros mercados, GDPR o LGPD según corresponda.

El TTL es tu amigo, no tu enemigo. No te tiembles en ponerlo. Un contexto de 30 días está bien para e-commerce; para soporte técnico de software puede ser 90 días. Lo que no debes hacer es TTL infinito: terminas con millones de keys huérfanas que inflan tu factura de Redis Cloud.

Rate limiting en el webhook. Sinch puede reintentar eventos si no respondes en tiempo. Asegúrate de responder 200 inmediatamente (como en el ejemplo) y procesar de forma asíncrona. Si procesas antes de responder y tu lógica tarda más de 5 segundos, Sinch va a reenviar el evento y vas a procesar el mismo mensaje dos veces.

Prueba el escenario de canal cruzado de verdad. Manda un mensaje por WhatsApp, luego simula una llamada entrante o un email y verifica que el agente ve el mismo contexto. Si no lo pruebas end-to-end, no sabes si funciona.

Monitorea con alertas en Redis. Configura alertas en used_memory y en el número de keys activas. Un pico inesperado puede significar un bug que está creando keys duplicadas o que tu bot está siendo abusado.


Resultado final

Con esta arquitectura tu bot de WhatsApp deja de saludar a tus clientes como si fueran desconocidos. Sabe cuándo fue la última vez que hablaron, por qué canal llegaron, qué problema tenían, y puede pasarle esa información completa a un agente humano sin que el cliente tenga que repetir su historia. Eso no es un feature bonito: es el estándar mínimo de respeto que un cliente leal merece en 2026.