Biblioteca · Atención al cliente y agentes de voz

Chat con IA en tu sitio web: Crisp + Claude, un widget inteligente

Creador55 minActualizado: octubre de 2026
61 de 105 en la biblioteca

Tiempo: ~20 min de teoría + 35 min de práctica


Lo esencial

Un visitante entró a tu sitio a las 23:47 de un sábado. Vio el botón "Escríbenos" y mandó una pregunta. Lo normal: silencio hasta el lunes en la mañana. Para entonces ya le compró a la competencia.

Un chat con IA responde en un segundo. A cualquier hora. En muchos idiomas. Y si no puede ayudar, lo dice con honestidad y anota los datos para que un operador llame en la mañana.

🎨 Imagínalo así: una tienda en un centro comercial con un asesor de ventas. No está dormitando en una esquina: recibe a cada persona que entra y le pregunta de inmediato "¿En qué te puedo ayudar?". Eso es justo lo que hace un widget de chat con IA: recibe al visitante en el momento en que está listo para conversar.


Conceptos clave

  • Crisp.chat + Claude: un webhook intercepta el mensaje → Claude responde → la respuesta llega al chat
  • Intercom Fin: un agente de IA integrado (más sencillo, más caro)
  • Chatwoot: alternativa de código abierto, self-hosted + Claude
  • Escalamiento inteligente: Claude resuelve → si no puede → lo toma una persona
  • RAG sobre la documentación: Claude sabe todo sobre tu producto
  • Análisis de preguntas: Claude encuentra patrones → tú mejoras las preguntas frecuentes (FAQ)

Teoría

Por qué el chat de siempre no funciona

Un widget de chat en línea normal funciona mientras hay un operador. Si no hay operador → "Estamos desconectados, déjanos tu email". En ese punto, parte de los visitantes se va.

Tres dolores:

  1. Horario: fuera del horario de atención, el cliente se va
  2. Velocidad: aun en línea, un operador tarda de 2 a 5 minutos en responder (está ocupado con otra cosa)
  3. Escala: un operador solo puede llevar unos cuantos chats a la vez

La IA resuelve los tres: responde al instante, 24/7, a mil clientes en paralelo.

Crisp.chat + Claude: integración por webhook

Crisp es una herramienta de chat europea (muchos la eligen por su cumplimiento del GDPR; suele ser más sencilla y barata que Intercom). El plan gratis te permite poner el widget en tu sitio, pero el acceso a la API y a los webhooks depende del plan: revisa las condiciones en el sitio de Crisp.

Cómo funciona:

Código
El cliente escribe en el chat → webhook de Crisp → tu servidor → Claude API → respuesta → Crisp API → el cliente la ve
javascript
// server.js — servidor de Node.js para procesar el webhook de Crisp
const express = require('express');
const Anthropic = require('@anthropic-ai/sdk');
const { Crisp } = require('crisp-api');

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

const claude = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY
});

const crisp = new Crisp();
crisp.authenticateTier(
  'plugin',
  process.env.CRISP_API_IDENTIFIER,
  process.env.CRISP_API_KEY
);
// Revisa los nombres de los métodos de la librería crisp-api en su README: cambian entre versiones

// Base de conocimiento sobre el producto
const KNOWLEDGE_BASE = `
Eres el asistente de la empresa [Nombre de la empresa].

PRODUCTO:
- Qué es: [descripción del producto]
- Precio: [precios]
- Cómo empezar: [pasos]

PREGUNTAS FRECUENTES:
- Pregunta 1: Respuesta 1
- Pregunta 2: Respuesta 2

QUÉ NO HACER:
- No inventar funciones que no existen
- No dar descuentos por tu cuenta
- No prometer plazos sin confirmarlos

CUÁNDO PASAR A UN OPERADOR:
- Queja sobre el producto
- Pregunta sobre un reembolso
- Un problema técnico que no puedes resolver
`;

// Almacén del historial de conversaciones (en producción: Redis o una base de datos)
const conversationHistory = new Map();

// Webhook de Crisp
app.post('/crisp-webhook', async (req, res) => {
  const event = req.body;

  // Procesamos solo los mensajes nuevos de los clientes
  if (event.event !== 'message:send') {
    return res.json({ status: 'ignored' });
  }

  const sessionId = event.data?.session_id;
  const message = event.data?.content;
  const websiteId = event.data?.website_id;
  const from = event.data?.from;  // 'user' u 'operator'

  // Ignoramos los mensajes de los operadores (para no entrar en un ciclo)
  if (from === 'operator') {
    return res.json({ status: 'ignored' });
  }

  console.log(`Mensaje nuevo [${sessionId}]: ${message}`);

  try {
    // Obtenemos o creamos el historial de la conversación
    if (!conversationHistory.has(sessionId)) {
      conversationHistory.set(sessionId, []);
    }
    const history = conversationHistory.get(sessionId);

    // Agregamos el mensaje del usuario al historial
    history.push({
      role: 'user',
      content: message
    });

    // Mantenemos el historial de un tamaño razonable (los últimos 10 mensajes)
    const recentHistory = history.slice(-10);

    // Enviamos a Claude
    const response = await claude.messages.create({
      model: 'claude-sonnet-5-5',
      max_tokens: 500,
      system: KNOWLEDGE_BASE,
      messages: recentHistory
    });

    const aiResponse = response.content.map((b) => (b.type === "text" ? b.text : "")).join("");

    // Guardamos la respuesta de la IA en el historial
    history.push({
      role: 'assistant',
      content: aiResponse
    });

    // Determinamos si hace falta un operador
    const needsHuman = detectEscalationNeeded(aiResponse, message);

    // Enviamos la respuesta a Crisp
    await crisp.website.sendMessageInConversation(websiteId, sessionId, {
      type: 'text',
      from: 'operator',
      origin: 'chat',
      content: aiResponse
    });

    // Si hace falta un operador, avisamos
    if (needsHuman) {
      await crisp.website.sendMessageInConversation(websiteId, sessionId, {
        type: 'text',
        from: 'operator',
        origin: 'chat',
        content: '⏳ Te paso con un especialista. Un momento, por favor.'
      });

      // Cambiamos el estado de la conversación a "requiere atención" (state: pending, esperando operador)
      await crisp.website.changeConversationState(
        websiteId,
        sessionId,
        { state: 'pending' }
      );

      console.log(`[${sessionId}] Transferido a un operador`);
    }

    res.json({ status: 'success', escalated: needsHuman });

  } catch (error) {
    console.error('Error:', error);

    // Si hay un error, llamamos a un operador de inmediato
    await crisp.website.sendMessageInConversation(websiteId, sessionId, {
      type: 'text',
      from: 'operator',
      origin: 'chat',
      content: 'Disculpa, en este momento no puedo ayudarte. ¡Un especialista se pondrá en contacto contigo pronto!'
    });

    res.status(500).json({ status: 'error' });
  }
});

function detectEscalationNeeded(aiResponse, userMessage) {
  const escalationPhrases = [
    'te paso con', 'te comunico con', 'un especialista te ayudará',
    'comunícate con', 'no puedo ayudarte con eso', 'requiere atención manual'
  ];

  const userEscalationPhrases = [
    'una persona real', 'operador', 'agente', 'no quiero hablar con un robot',
    'llamen a una persona', 'quiero hablar con una persona'
  ];

  const inAiResponse = escalationPhrases.some(p =>
    aiResponse.toLowerCase().includes(p)
  );

  const inUserMessage = userEscalationPhrases.some(p =>
    userMessage.toLowerCase().includes(p)
  );

  return inAiResponse || inUserMessage;
}

app.listen(3000, () => {
  console.log('Servidor iniciado en el puerto 3000');
});
bash
# dependencias de package.json
npm init -y
npm install express @anthropic-ai/sdk crisp-api dotenv

# .env
ANTHROPIC_API_KEY=sk-ant-...
CRISP_API_IDENTIFIER=your_crisp_id
CRISP_API_KEY=your_crisp_key

# Iniciar
node server.js

# Hacerlo accesible desde internet (para el webhook)
npx ngrok http 3000
# Copia una URL tipo https://abc123.ngrok.io
# En Crisp: sección de webhooks (los nombres del menú cambian) → agrega la dirección https://abc123.ngrok.io/crisp-webhook

🎨 Imagínalo así: un webhook es como un timbre. Crisp toca el timbre cuando el cliente escribe. Tu servidor abre la puerta, piensa qué contestar (con Claude) y le regresa la respuesta a Crisp. Todo en 1 o 2 segundos.

Intercom Fin: un agente de IA integrado

Intercom es una opción más cara (planes de pago, y el agente de IA Fin se cobra aparte; precios en el sitio de Intercom), pero trae integrado el agente de IA Fin. No necesitas escribir código: conectas tu documentación y Fin empieza a responder.

Parámetro Crisp + Claude Intercom Fin
Precio Plan de Crisp + pago de la Claude API Plan de Intercom + pago de Fin (precios en los sitios)
Configuración Requiere código Sin código
Flexibilidad Total Limitada
Calidad de las respuestas Muy alta Alta
Analítica La tuya Integrada
Mejor para Desarrolladores Equipos no técnicos

Chatwoot: código abierto + self-hosted

Chatwoot es una alternativa gratuita (de código abierto) a Intercom que alojas tú mismo. Control total sobre los datos, sin pago de licencia; el servidor y el mantenimiento corren por tu cuenta.

python
# Integración Chatwoot + Claude con Python
import anthropic
import requests
import os

claude_client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

CHATWOOT_URL = os.environ["CHATWOOT_URL"]  # https://your-chatwoot.com
CHATWOOT_TOKEN = os.environ["CHATWOOT_TOKEN"]  # User Access Token
CHATWOOT_ACCOUNT_ID = os.environ["CHATWOOT_ACCOUNT_ID"]

def send_message_to_chatwoot(
    conversation_id: int,
    message: str,
    message_type: str = "outgoing"
) -> dict:
    """Envía un mensaje a una conversación de Chatwoot"""

    response = requests.post(
        f"{CHATWOOT_URL}/api/v1/accounts/{CHATWOOT_ACCOUNT_ID}/conversations/{conversation_id}/messages",
        headers={
            "api_access_token": CHATWOOT_TOKEN,
            "Content-Type": "application/json"
        },
        json={
            "content": message,
            "message_type": message_type,
            "private": False  # Visible para el cliente
        }
    )
    return response.json()


def handle_chatwoot_webhook(webhook_data: dict) -> dict:
    """Procesa un webhook entrante de Chatwoot"""

    event_type = webhook_data.get("event")

    # Solo los mensajes entrantes de los clientes
    if event_type != "message_created":
        return {"status": "ignored"}

    message = webhook_data.get("content", "")
    conversation_id = webhook_data.get("conversation", {}).get("id")
    message_type = webhook_data.get("message_type")

    # Ignoramos los salientes (de operadores o de la IA)
    if message_type != "incoming":
        return {"status": "ignored"}

    # Claude responde
    response = claude_client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=400,
        system="Eres el asistente de la empresa. Responde sobre el producto. Si no sabes algo, dilo con honestidad.",
        messages=[{"role": "user", "content": message}]
    )

    ai_answer = "".join(b.text for b in response.content if b.type == "text")

    # Enviamos la respuesta a Chatwoot
    send_message_to_chatwoot(conversation_id, ai_answer)

    return {"status": "success"}

Escalamiento inteligente: cuándo pasarle la conversación a una persona

El punto clave de cualquier chat con IA es saber cuándo detenerse. Que la IA intente responder todo suele ser peor que pasar rápido la conversación a un operador.

python
def should_escalate_to_human(
    user_message: str,
    ai_response: str,
    conversation_turns: int
) -> tuple[bool, str]:
    """
    Determina si hace falta pasar a un operador.
    Devuelve (hay_que_transferir, motivo)
    """

    # Solicitudes explícitas de una persona real
    human_requests = [
        "quiero hablar con una persona", "llamen a un operador",
        "un asesor de verdad", "no quiero hablar con un robot", "¿eres un bot?",
        "comunícame con alguien"
    ]
    if any(phrase in user_message.lower() for phrase in human_requests):
        return True, "el cliente pidió un operador humano"

    # Mensajes con carga emocional
    anger_signals = [
        "esto es un abuso", "devuélvanme mi dinero", "los voy a demandar",
        "esto es un fraude", "pésimo", "exijo"
    ]
    if any(signal in user_message.lower() for signal in anger_signals):
        return True, "el cliente expresa un fuerte descontento"

    # La IA reconoce que no puede ayudar
    ai_cant_help = [
        "no puedo ayudarte", "no está en mis manos",
        "te paso con un especialista", "consúltalo con un operador"
    ]
    if any(phrase in ai_response.lower() for phrase in ai_cant_help):
        return True, "la IA no pudo responder la pregunta"

    # Conversación demasiado larga sin solución (5 o más turnos)
    if conversation_turns >= 5:
        claude_check = claude_client.messages.create(
            model="claude-haiku-4-5",  # Modelo rápido y barato para la verificación
            max_tokens=50,
            messages=[{
                "role": "user",
                "content": f"¿Se resolvió el problema del cliente? Responde solo: sí/no. Mensaje: {user_message}"
            }]
        )
        if "no" in "".join(b.text for b in claude_check.content if b.type == "text").lower():
            return True, "el problema no se resolvió en 5 mensajes"

    return False, ""

RAG: Claude sabe todo sobre tu producto

Sin una base de conocimiento, Claude inventará datos sobre tu producto. Con una base, responde con precisión.

python
# Un sistema RAG sencillo para el widget de chat
# En producción: Pinecone / Chroma / Qdrant en lugar de una lista de cadenas

class SimpleRAG:
    def __init__(self, documents: list[dict]):
        """
        documents: [{"title": "...", "content": "...", "category": "..."}]
        """
        self.documents = documents
        self.client = anthropic.Anthropic()

    def find_relevant(self, query: str, top_k: int = 3) -> list[dict]:
        """Encuentra los documentos relevantes (de forma simplificada, con Claude)"""

        # En un sistema real: búsqueda vectorial
        # Aquí: Claude elige de una lista pequeña

        titles = [f"{i}. {d['title']}" for i, d in enumerate(self.documents)]
        titles_text = "\n".join(titles)

        response = self.client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=100,
            messages=[{
                "role": "user",
                "content": f"""Pregunta: "{query}"

¿Qué documentos son los más relevantes? Indica solo los números separados por comas (por ejemplo: 0,2):
{titles_text}"""
            }]
        )

        try:
            indices = [int(i.strip()) for i in "".join(b.text for b in response.content if b.type == "text").split(",")]
            return [self.documents[i] for i in indices[:top_k] if i < len(self.documents)]
        except Exception:
            return self.documents[:top_k]  # Fallback

    def answer_with_context(self, question: str) -> str:
        """Responde la pregunta con el contexto de la base de conocimiento"""

        relevant_docs = self.find_relevant(question)
        context = "\n\n".join([
            f"### {d['title']}\n{d['content']}"
            for d in relevant_docs
        ])

        response = self.client.messages.create(
            model="claude-sonnet-5-5",
            max_tokens=300,
            messages=[{
                "role": "user",
                "content": f"""Usa SOLO la información de la base de conocimiento.
Si la respuesta no está en la base, di con honestidad "no lo sé" y sugiere escribir a soporte.

BASE DE CONOCIMIENTO:
{context}

PREGUNTA: {question}"""
            }]
        )

        return "".join(b.text for b in response.content if b.type == "text")


# Ejemplo de configuración (producto ficticio)
kb = SimpleRAG([
    {
        "title": "Planes",
        "category": "pricing",
        "content": "Plan Básico: $15/mes (hasta 5 usuarios). Estándar: $45/mes (hasta 20 usuarios). Business: $120/mes (ilimitado). 20% de descuento con pago anual."
    },
    {
        "title": "Integraciones",
        "category": "technical",
        "content": "Funciona con: HubSpot, Shopify, Google Sheets. La API está disponible en todos los planes. Webhooks a partir del plan Estándar."
    },
    {
        "title": "Seguridad",
        "category": "technical",
        "content": "Los datos se guardan en servidores en Estados Unidos. Cifrado AES-256. SOC2 Type II. Copias de seguridad cada 6 horas."
    }
])

# Prueba
questions = [
    "¿Cuánto cuesta el plan ilimitado?",
    "¿Hay integración con HubSpot?",
    "¿Puedo trabajar con ustedes desde Europa?"
]

for q in questions:
    answer = kb.answer_with_context(q)
    print(f"Q: {q}")
    print(f"A: {answer}\n")

Analítica: qué es lo que más preguntan

python
def analyze_chat_questions(chat_logs: list[str]) -> dict:
    """Analiza las preguntas de los chats y encuentra patrones"""

    client = anthropic.Anthropic()

    combined = "\n---\n".join(chat_logs[:50])

    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=800,
        messages=[{
            "role": "user",
            "content": f"""Analiza las preguntas de los clientes en el widget de chat.

LOGS DE LOS CHATS:
{combined}

Necesito:
1. **Los 10 temas** que más preguntan (con el % aproximado del total de preguntas)
2. **Preguntas sin respuesta**: dónde la IA dijo "no lo sé" (qué agregar a las preguntas frecuentes)
3. **Preguntas que pasaron a un operador**: qué es demasiado complejo para la IA
4. **Patrones de horario**: cuándo escriben más
5. **Recomendaciones**: qué cambiar en el sitio para que la gente encuentre las respuestas por su cuenta

Estos datos son para el equipo de producto: sé concreto."""
        }]
    )

    return "".join(b.text for b in response.content if b.type == "text")

Práctica

Tarea: configurar el webhook Crisp + Claude en 35 minutos

Paso 1: Registro en Crisp (5 min)

bash
# 1. Ve a crisp.chat
# 2. Crea una cuenta (gratis)
# 3. Agrega tu sitio: Settings → Websites → Add
# 4. Copia el código del widget y pégalo en tu sitio antes de </body>

# Para probar sin un sitio real, Crisp tiene un chat de prueba

Paso 2: Obtener las credenciales de la API (5 min)

bash
# En el Crisp Dashboard:
# Profile → API Access Tokens → Create new token
# Guarda:
# - Identifier (como un usuario)
# - Key (como una contraseña)
# - Website ID (en la URL: app.crisp.chat/website/WEBSITE_ID/)

Paso 3: Levantar el servidor local (15 min)

bash
mkdir crisp-claude-bot && cd crisp-claude-bot
npm init -y
npm install express @anthropic-ai/sdk dotenv ngrok

# Creamos .env
cat > .env << EOF
ANTHROPIC_API_KEY=sk-ant-your-key
CRISP_API_IDENTIFIER=your-identifier
CRISP_API_KEY=your-key
CRISP_WEBSITE_ID=your-website-id
PORT=3000
EOF
javascript
// bot.js — versión mínima que funciona
require('dotenv').config();
const express = require('express');
const Anthropic = require('@anthropic-ai/sdk');

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

const claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// El webhook más simple, sin historial (para probar)
app.post('/webhook', async (req, res) => {
  const { event, data } = req.body;

  if (event !== 'message:send' || data?.from !== 'user') {
    return res.json({ ok: true });
  }

  const question = data?.content;
  const sessionId = data?.session_id;
  const websiteId = data?.website_id;

  try {
    const response = await claude.messages.create({
      model: 'claude-sonnet-5-5',
      max_tokens: 250,
      system: 'Eres el asistente de un sitio web. Responde breve y al punto. Si no sabes algo, dilo con honestidad.',
      messages: [{ role: 'user', content: question }]
    });

    const answer = response.content.map((b) => (b.type === "text" ? b.text : "")).join("");

    // Enviamos la respuesta con la REST API de Crisp
    await fetch(
      `https://api.crisp.chat/v1/website/${websiteId}/conversation/${sessionId}/message`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Basic ${Buffer.from(
            `${process.env.CRISP_API_IDENTIFIER}:${process.env.CRISP_API_KEY}`
          ).toString('base64')}`,
          'Content-Type': 'application/json',
          'X-Crisp-Tier': 'plugin'
        },
        body: JSON.stringify({
          type: 'text',
          content: answer,
          from: 'operator',
          origin: 'chat'
        })
      }
    );

    console.log(`[${sessionId}] Q: ${question.substring(0,50)}... → respondido`);
    res.json({ ok: true });

  } catch (err) {
    console.error(err);
    res.status(500).json({ error: err.message });
  }
});

app.listen(process.env.PORT, () =>
  console.log(`Bot iniciado en el puerto ${process.env.PORT}`)
);
bash
# Iniciamos el servidor
node bot.js

# En otra terminal, abrimos el túnel
npx ngrok http 3000
# Copiamos la URL: https://abc123.ngrok.io

Paso 4: Configurar el webhook en Crisp (5 min)

Código
En el Crisp Dashboard:
Sección de webhooks en la configuración (los nombres del menú de Crisp cambian)
→ Endpoint URL: https://abc123.ngrok.io/webhook
→ Events: message:send ✅
→ Save

Paso 5: Probar (5 min)

Escribe esto en el chat
Abre el chat de prueba de Crisp (botón "Preview" en el Dashboard)
Escribe cualquier pregunta
Mira la respuesta de la IA
Revisa los logs en la terminal

Herramientas y recursos

  • Crisp.chat: crisp.chat (tiene plan gratis; el acceso a la API y a los webhooks depende del plan, revísalo en el sitio)
  • Chatwoot: chatwoot.com (código abierto, self-hosted gratis)
  • Intercom: intercom.com (de pago, con el agente de IA Fin integrado; precios en el sitio)
  • Tawk.to: tawk.to (chat gratuito; revisa en el sitio las funciones de IA y sus condiciones)
  • ngrok: ngrok.com (túnel para probar webhooks en tu computadora)
  • Anthropic API: los nombres de modelos del código son a octubre de 2026; precios y versiones vigentes: Lo vigente

Conclusiones clave

El chat de tu sitio no es "un canal de soporte más". Es un punto de decisión. El cliente escribe en el chat justo cuando está listo para comprar o listo para irse. Una IA que responde en un segundo a las 23:47 te da la oportunidad de retener a ese visitante que se iba.

El escalamiento importa más que las respuestas. El mejor chat con IA es el que entiende rápido cuándo hace falta una persona y transfiere sin demora. Un cliente que recibió "no lo sé, te paso con un especialista" queda conforme. Un cliente al que la IA le inventó una función inexistente se pierde para siempre.

La analítica es un bonus escondido. Cientos de conversaciones = cientos de comentarios. Claude analiza los patrones en minutos y te muestra qué mejorar en el producto, el sitio y las preguntas frecuentes. Es una buena fuente adicional junto a la investigación de usuarios de siempre.


Siguiente lección

→ Payments & Billing: Stripe MCP, suscripciones, webhooks

La marca se guarda solo en este navegador y no se envía a ningún sitio. Mi progreso