CRM de WhatsApp con IA y traspaso a humano con Claude Code

Cómo armé un CRM de WhatsApp open source con Twilio, Supabase y Claude Code, donde un agente de IA contesta y cualquiera del equipo puede tomar el chat.

Publicado27 de octubre de 2026
Lectura10 min de lectura
CRM de WhatsApp con IA y traspaso a humano con Claude Code | Cómo armé un CRM de WhatsApp open source con Twilio, Supabase y Claude Code, donde un agente de IA contesta y cualquiera del equipo puede tomar el chat.

Que un agente de IA conteste en WhatsApp es la parte fácil. La difícil es el momento en que una persona tiene que meterse en la conversación, y la mayoría de los desarrollos que veo no lo diseñan.

 

Armé un CRM de WhatsApp open source sobre la API oficial de WhatsApp a través de Twilio, con Supabase como base de datos y Claude como agente. Los clientes le escriben a un número de WhatsApp y el agente contesta solo. Cualquiera del equipo puede tomar un chat desde la bandeja, mandar un par de mensajes y devolverlo. Claude Code instala todo con un solo comando. Este post cubre las decisiones detrás, sobre todo el traspaso a humano, porque es ahí donde estos sistemas fallan frente al cliente.

 

Qué hace el CRM

 

El panel tiene una bandeja que agrupa las conversaciones de todos los números conectados. Podés filtrar por las conversaciones asignadas a vos o por las que no tienen asignación. Hay además una pestaña aparte para las conversaciones donde el agente detectó que el cliente quiere hablar con una persona. Pueden entrar varios operadores. Que un humano esté atendiendo una conversación es un campo distinto de que esa conversación esté asignada a ese humano.

 

Cada cliente tiene una ficha con nombre, empresa y email, un historial de tickets (uno por cada vez que se disparó un traspaso) y un resumen con IA. Ese resumen lee el historial de la conversación y se actualiza a medida que llegan mensajes nuevos.

 

También hay una base de conocimiento donde subís documentos: horarios, ubicación, políticas de la empresa, información de productos o servicios. Los documentos se vectorizan y el agente recupera solo lo que la pregunta necesita. Si alguien pregunta por los horarios, la lista de precios no se carga en el contexto.

 

El backend expone endpoints que el panel de operadores usa con Supabase realtime, así que la bandeja se actualiza en vivo sin recargar la página.

 

El flujo de un mensaje

 

@router.post("/webhook/whatsapp")
async def whatsapp_webhook(request: Request, background: BackgroundTasks) -> Response:
    app_state = request.app.state
    form = dict(await request.form())

    if app_state.settings.twilio_validate_signature:
        signature = request.headers.get("X-Twilio-Signature", "")
        if not app_state.whatsapp.validate_signature(
            app_state.settings.webhook_url, form, signature
        ):
            log.warning("invalid_signature", url=app_state.settings.webhook_url)
            return Response(status_code=403, content="invalid signature")

    phone = strip_whatsapp(str(form.get("From", "")))
    body = str(form.get("Body", "") or "")
    sid = str(form.get("MessageSid", "") or "") or None
    num_media = int(form.get("NumMedia", 0) or 0)

    if not phone:
        return Response(content=EMPTY_TWIML, media_type="application/xml")

    log.info("inbound", phone=phone, body=body, sid=sid)

    # Twilio reintenta ante cualquier no-2xx. 200 primero, trabajo después.
    background.add_task(_process, request.app, phone, body, sid, num_media)
    return Response(content=EMPTY_TWIML, media_type="application/xml")

 

Cuando un cliente escribe, Twilio manda el payload al webhook y el servidor devuelve 200 de inmediato. El procesamiento pasa después de esa confirmación. Twilio solo necesita saber que el request llegó. Hacerlo esperar una llamada al LLM es la forma de terminar con timeouts y reintentos.

 

Después viene la primera lectura. Cada conversación tiene un estado con tres valores posibles: bot, necesita humano o humano. Si el estado es humano, el mensaje se guarda y no pasa nada más. El agente nunca lo ve. Todo mensaje entrante se guarda en la tabla de mensajes, sin importar el estado.

 

Si el estado es bot, el servidor arma el contexto del agente:

 

  • Los últimos 10 mensajes de la conversación.
  • Las notas que dejó el operador si tomó el chat y lo devolvió.
  • Cuatro skills: gestión de clientes, tickets, base de conocimiento y traspaso a humano.

 

La skill de traspaso es la que le permite al agente marcar que una conversación necesita a una persona. Cuando pasa eso, el agente deja de contestar y la conversación aparece en la pestaña "necesita humano" para que alguien la tome.

 

La segunda lectura

 

if await _still_bot(db, phone, result.reply_text):
    await _reply(state, phone, result.reply_text, client_id=client_id)


async def _still_bot(db: Any, phone: str, text: str) -> bool:
    """Segunda lectura del estado, justo antes de mandar.

    La primera fue antes de llamar a Claude, y el agente tarda segundos. Si en
    ese medio el operador tomó el chat, la respuesta del bot no sale: queda en
    los logs por si el operador la quiere ver.

    ponytail: entre esta lectura y el envío quedan milisegundos de ventana (antes
    eran segundos). Cerrarla del todo pide un lock por conversación en la base.
    """
    try:
        conversation = await db.get_conversation(phone)
    except Exception as exc:  # noqa: BLE001
        log.warning("conversation_recheck_failed", error=str(exc))
        return True
    if (conversation or {}).get("status") == "human":
        log.info("reply_dropped", phone=phone, reason="chat_tomado", text=text[:200])
        return False
    return True

 

Antes de mandar la respuesta por Twilio, el servidor vuelve a leer el estado. Es la línea que más importa de todo el desarrollo.

 

Armar el contexto y generar una respuesta lleva unos segundos. En esa ventana, un operador puede abrir la conversación, hacer clic en "tomar chat" y escribir su propio mensaje. Sin una segunda lectura, el agente termina su trabajo y manda la respuesta igual, y al cliente le contestan dos voces distintas a la vez. Con la segunda lectura, si el estado pasó a humano mientras el agente trabajaba, la respuesta se descarta.

 

Es un problema de estado. Una sugerencia común es pedirle al modelo en el system prompt que se quede callado cuando entra una persona. Eso pone la decisión dentro de un componente que no puede ver en tiempo real lo que hace el operador. En este diseño el estado vive en la base de datos, un botón del panel lo cambia y el agente solo lo lee.

 

Tomar el chat y devolverlo

 

Tomar un chat está pensado para intervenciones cortas: confirmar un descuento, contestar algo delicado, mandar uno o dos mensajes puntuales. Mientras el estado es humano, podés escribir desde el panel o desde el mismo WhatsApp, y el agente queda afuera de la conversación.

 

Cuando el operador le devuelve el chat a la IA, puede dejar notas. Esas notas entran al contexto del agente junto con los últimos 10 mensajes, así que el agente retoma la conversación sabiendo qué dijo la persona y por qué. En la demo tomo el chat, mando un mensaje y lo devuelvo con una nota que dice que era una prueba. El siguiente mensaje del cliente lo vuelve a contestar el agente.

 

Setup con Claude Code

 

El repositorio es público. Podés clonarlo vos o pedirle a Claude Code que lo clone en una carpeta con el nombre que quieras. Desde ahí, un comando de setup corre la instalación de punta a punta:

 

  • Revisa las dependencias y hace el build del proyecto.
  • Se conecta a Supabase con el MCP de Supabase. Te autenticás en el navegador y elegís la organización. Claude Code crea un proyecto nuevo en la región que elijas (yo usé São Paulo, la más cercana a Buenos Aires) y corre el script SQL inicial.
  • Carga las credenciales de Twilio y el número del sender de WhatsApp.
  • Crea el primer usuario admin del panel con una contraseña generada.
  • Detecta el dominio de ngrok y verifica que las skills y el agente estén corriendo.

 

Hay dos momentos de ese proceso que conviene conocer de antemano. El script SQL inicial tiene sentencias como drop y truncate, así que Claude Code pide permiso antes de correr un comando potencialmente destructivo. En un proyecto nuevo solo se asegura de que no existan tablas con esos nombres, y terminás con siete tablas. El segundo: el MCP de Supabase no devuelve la secret key del proyecto, por seguridad. La copiás vos desde el dashboard de Supabase y la pegás en el archivo .env, junto con el account SID de Twilio, el auth token y el número de WhatsApp.

 

La parte del número (conectar un número de Twilio a Meta Business como sender de WhatsApp y cargar saldo en Twilio) es un proceso aparte que cubrí por separado.

 

Trade-offs

 

El túnel local. Para desarrollo local uso ngrok con un dominio reservado. Twilio necesita una URL pública a la que reenviar los mensajes. Con un dominio fijo, no tenés que actualizar el webhook en la consola cada vez que se reinicia el túnel. Es un setup de desarrollo: si tu computadora se suspende, tus clientes dejan de recibir respuestas.

 

Correr Claude en local. En modo local el agente corre Claude desde la línea de comandos en vez de pasar por la API, así que el testing no suma costos de API. Esa elección es para construir y testear. Un sistema que atiende clientes reales pasa por la API, y ese costo por conversación tiene que estar en tu precio.

 

El recargo de Twilio. Twilio suma USD 0.005 por cada mensaje enviado, encima de lo que cobra Meta. Ir directo a la Cloud API de Meta te ahorra ese recargo y a cambio te da más trabajo de configuración. Qué lado conviene depende de tu volumen y de cuánto uses las herramientas de Twilio.

 

Una ventana de 10 mensajes. El agente ve los últimos 10 mensajes más las notas del operador. Para la mayoría de las conversaciones de atención al cliente alcanza, y mantiene predecible el consumo de tokens. En una conversación larga, todo lo que se dijo antes de esa ventana desaparece para el agente. Por eso el historial más viejo tiene que vivir en el resumen del cliente.

 

Conocimiento que queda viejo. La base de conocimiento contesta con lo que dicen los documentos. Si cambian los horarios o los precios y nadie actualiza el documento, el agente repite la información vieja con total seguridad. Alguien del equipo tiene que hacerse cargo de esos documentos.

 

Dónde extenderlo

 

La ficha del cliente es el lugar natural para conectar otros sistemas: mandar clientes a HubSpot o a otro CRM, o enviar las conversaciones a un almacén para analítica. Como Claude Code armó el proyecto, también es la herramienta que uso para sumarle features.

 

El agente es la parte que la gente muestra en las demos. Lo que decide si le podés confiar tus clientes es un campo de estado y una segunda lectura antes de cada respuesta.

 

-Gonza

¿Querés un agente de IA así en tu WhatsApp? Mirá mis servicios de agentes de IA para WhatsApp.

Posts relacionados