---
title: "Un chatbot de WhatsApp con IA para tu negocio · que responda, califique y agende sin romper las reglas de Meta"
description: "Un chatbot de WhatsApp con inteligencia artificial no es 'ChatGPT en tu número'. Meta lo prohíbe desde 2026 y lo aplica: un bot de propósito general es motivo de baja. Lo que sí permite, y funciona, es un asistente acotado al negocio que responde dudas, hace tres preguntas para calificar, ofrece hor"
url: https://www.davidiriza.com/lab/chatbot-whatsapp-con-ia-para-tu-negocio
author: David Iriza
level: Intermedio
category: Mensajería
tags: ["whatsapp", "chatbot", "ia", "zernio", "meta", "agendar", "leads", "api-oficial", "baneo"]
tools: ["WhatsApp Cloud API", "Zernio", "Claude", "Claude Code", "Next.js", "Vercel", "Google Calendar"]
published: 2026-08-29
updated: 2026-08-28
---
# Un chatbot de WhatsApp con IA para tu negocio: que responda, califique y agende sin romper las reglas de Meta

La mayoría de los negocios que 'ponen IA en WhatsApp' hacen una de dos cosas: conectan un asistente abierto que contesta cualquier cosa (y Meta los da de baja), o compran una herramienta de flujos con botones que nadie termina. El punto medio es un asistente con un solo trabajo: atender a quien escribe, entender qué necesita, hacer las preguntas que tu equipo haría y, si califica, darle un horario real. Aquí no hay magia: hay un webhook, un modelo con instrucciones muy estrictas, cuatro herramientas y las reglas de Meta respetadas al pie de la letra. Todo lo que sigue está probado en producción con un asistente que atiende leads, agenda en un calendario real y avisa a un humano cuando toca.

**Ficha:** Necesitas un número de WhatsApp Business · Herramientas: WhatsApp Cloud API, Zernio, Claude +4 · Te llevas: 1 plantilla y 8 comandos · Lectura: 48 min · Verificado 28-ago-2026

> Para agentes: esta guía describe pasos ejecutables. Sigue los bloques de comando y código en orden; pregunta al usuario solo lo que no puedas inferir (rutas, nombres de proyecto). Repositorio y contexto del sitio: https://www.davidiriza.com/llms.txt

## De un vistazo

1. Qué sí permite Meta y qué te tira el número
2. API oficial o librería que escanea un QR: por qué aquí no hay debate
3. La ventana de 24 horas y las plantillas
4. La arquitectura: webhook, cerebro, herramientas, respuesta
5. Conectar WhatsApp con Zernio, paso a paso
6. El prompt de sistema completo
7. Calificar con tres preguntas y agendar de verdad
8. La primera semana en modo borrador
9. Qué medir cada semana
10. Errores reales y cómo se evitan
11. Dejar que Claude Code lo construya: kits que sí existen y qué esperar
12. Sección técnica: el webhook en Next.js y el envío de plantilla

## 01 · Qué sí permite Meta y qué te tira el número

_primero la ley_

Antes de construir nada, hay una regla que decide el diseño completo. Los Términos de la Solución de WhatsApp Business (versión modificada el 6 de marzo de 2026) definen 'AI Providers' como proveedores de modelos de lenguaje, plataformas de IA generativa o asistentes de IA de propósito general, y les prohíben usar la plataforma cuando esa tecnología es 'la funcionalidad principal (y no incidental o accesoria)' de lo que ofrecen. Traducido: un número de WhatsApp cuya razón de ser es 'habla con una IA de lo que quieras' está fuera. Un número de tu negocio que usa IA para atender a tus clientes está dentro.

- **Permitido** — Un bot que responde preguntas sobre TU servicio, califica interesados, agenda citas, confirma pedidos, da seguimiento a una compra o resuelve soporte. La IA es el medio; el fin es tu operación.
- **Prohibido** — Un asistente que redacta correos, resume artículos, hace tareas escolares o platica de cualquier tema. Aunque lo firme tu marca. Meta lo considera un proveedor de IA usando su red para distribuir un chatbot.
- **La zona gris que hay que cerrar** — Tu bot de negocio con un modelo detrás PUEDE responder 'qué opinas de la inflación' si no lo restringes. Por eso el prompt de sistema de esta guía tiene una regla de rechazo: fuera de tema, responde con una frase y regresa al negocio.

Meta no aplica esto con un correo amable. Su página de cumplimiento describe una escalera: aviso con la política violada, después bloqueos de 1 o 3 días para mensajes de marketing, después 5, 7 o 30 días sin poder enviar nada, después bloqueo indefinido. Un número de negocio que se queda sin WhatsApp un mes es un negocio sin su canal principal.

> Regla práctica: si le quitas el modelo de lenguaje a tu bot y sigue teniendo sentido como negocio (preguntas, horarios, citas, confirmaciones), estás del lado correcto. Si sin el modelo no queda nada, es un asistente de propósito general.

## 02 · API oficial o librería que escanea un QR: por qué aquí no hay debate

_la decisión que no se deshace_

Hay dos formas de poner un programa a contestar tu WhatsApp. La primera es la API oficial de Meta (la Cloud API, sola o a través de un intermediario como Zernio): tu número queda registrado como cuenta de negocio, pagas las plantillas y Meta sabe que ahí hay un sistema. La segunda son librerías de código abierto que se hacen pasar por WhatsApp Web: escaneas un QR con tu teléfono, como cuando vinculas la computadora, y a partir de ahí un programa lee y manda mensajes en tu nombre. Las más usadas son whatsapp-web.js (levanta un Chrome invisible) y Baileys (habla el protocolo directamente); encima de ellas hay 'gateways' con API REST y panel, como OpenWA. Son gratis, se instalan en tu máquina, no piden verificar tu negocio ni aprobar plantillas, y por eso tientan. Las probé para poder decirte lo que sigue con conocimiento de causa.

WhatsApp lo dice en su propio centro de ayuda, en el artículo 'About unofficial apps': vincular tu cuenta a versiones no oficiales viola sus Términos de Servicio, y las consecuencias son que tu cuenta puede ser suspendida de forma temporal o permanente, o quedar restringida, incluida la capacidad de vincular dispositivos. En otro artículo, sobre mensajería automatizada o masiva no autorizada, Meta va más lejos: sus productos no están pensados para automatización fuera de sus herramientas de negocio, banea cuentas con clasificadores de aprendizaje automático y se reserva emprender acciones legales contra empresas que anuncian públicamente que usan WhatsApp de formas que violan los Términos. Y los Términos de WhatsApp Business prohíben desarrollar o usar aplicaciones que interactúen con sus servicios sin consentimiento escrito. No es una zona gris: es una prohibición explícita, con la sanción escrita.

|  | API oficial (Cloud API / Zernio) | Librería por QR (whatsapp-web.js, Baileys, OpenWA) |
| --- | --- | --- |
| Estatus ante Meta | Producto oficial; tu número es una cuenta de negocio reconocida | Viola los Términos; el propio README de OpenWA lo advierte |
| Riesgo del número | Escalera de avisos y bloqueos por política, con la política citada | Suspensión temporal o permanente sin aviso ni apelación útil |
| Costo | Plantillas por mensaje entregado; respuestas en ventana gratis | Cero en licencias; el costo es el número que pierdes |
| Escribir primero | Plantilla aprobada, a quien aceptó recibirte | Técnicamente posible; es justo lo que dispara el baneo |
| Verificación de negocio | Sí (cuenta días hábiles, no horas; se alarga si rechazan el primer intento) | No hay nada que verificar; tampoco nada que te respalde |
| Ventana de 24 h | Existe y la respetas por diseño | No aplica, y por eso el sistema de abuso te mira distinto |
| Infraestructura | Un webhook en tu hosting | Chrome invisible (300 a 500 MB de RAM por sesión) o un socket que se cae cuando WhatsApp cambia el protocolo |
| Cuándo sí | Cualquier negocio que viva de ese número | Proyecto personal, aprendizaje, herramienta interna con un número desechable |

Lo que verifiqué con OpenWA para no hablar de oídas: cloné el repo (versión 0.23.3, licencia MIT, NestJS sobre Node 22), instalé sus 1,065 dependencias, lo compilé y revisé su especificación de API. Sí trae lo que promete: crear sesión, arrancarla, pedir el QR, consultar estado y mandar texto, además de plantillas, encuestas y hasta envío masivo. También trae, en el primer bloque de su README, una advertencia que vale más que cualquier tutorial: 'siempre hay un riesgo distinto de cero de restricción o baneo', 'nunca conectes tu número personal o de negocio principal', 'usa un número que puedas permitirte perder', y para entornos donde el cumplimiento importa, 'trátalo como no aprobado y usa la Cloud API oficial'. Lo dicen los que lo mantienen. Lo dice Meta. Te lo digo yo.

- **Los 'trucos anti-baneo' son evasión, no cumplimiento** — Vas a encontrar recetas: esperar entre 5 y 15 segundos aleatorios antes de contestar, variar los saludos para que no parezcan plantilla, no responder de noche, 'calentar' el número tres días con mensajes a conocidos. Todas tienen el mismo objetivo: que el clasificador de Meta no te detecte. Eso no reduce la violación; reduce la probabilidad de que la vean hoy. Y el propio README de OpenWA reporta que el primer mensaje a un contacto nuevo a veces nunca llega aunque la API diga 'enviado': WhatsApp lo tira del lado del servidor.
- **El costo real no está en la factura** — Una librería por QR cuesta cero dólares y un número. Si ese número es el que está en tus tarjetas, tus anuncios y tus 5 mil contactos, el día que lo suspendan no hay a quién llamar: OpenWA dice literal que no tiene 'ninguna palanca' para levantar una restricción. La API oficial te cobra centavos por plantilla y a cambio te da algo que no se compra después: un número que Meta reconoce como negocio y una escalera de avisos antes del bloqueo.
- **Dónde sí tienen sentido** — Un bot para tu grupo de amigos, un recordatorio personal, un experimento de fin de semana para aprender cómo se ve un webhook. Con un número comprado para eso, que no te importe perder. Ahí una librería por QR es una herramienta legítima de aprendizaje y no le hace daño a nadie.
- **La señal de alarma** — Si un tutorial te dice 'usa un número nuevo por si acaso' está admitiendo el riesgo en la misma oración. Si además te recomienda un proxy residencial porque las IPs de centro de datos 'se marcan más', ya no estás construyendo un chatbot: estás construyendo un sistema para no ser detectado. Un negocio no debería tener que esconderse de su canal principal.

> Todo lo que sigue en esta guía asume la API oficial. No porque sea la única que funciona técnicamente, sino porque es la única con la que el número de tu negocio sigue siendo tuyo el mes que viene.

## 03 · La ventana de 24 horas y las plantillas

_la regla que rompe todo_

WhatsApp Business funciona con una ventana: cuando un cliente te escribe, se abre un periodo de 24 horas en el que puedes mandarle lo que quieras (texto libre, imágenes, botones) sin costo. Cada mensaje suyo la reinicia. Cuando pasan 24 horas sin que el cliente escriba, la ventana se cierra y solo puedes mandar una plantilla: un mensaje pre-aprobado por Meta, con variables, que se cobra por envío según su categoría.

| Situación | Qué puedes mandar | Costo |
| --- | --- | --- |
| El cliente escribió hace menos de 24 h | Texto libre, media, botones, y plantillas | Libre (las plantillas de utilidad también son gratis dentro de la ventana) |
| Pasaron más de 24 h sin respuesta | Solo plantilla aprobada | Se cobra por mensaje entregado según categoría y país |
| Llegó desde un anuncio de clic a WhatsApp | Todo, durante 72 h | Gratis (ventana de punto de entrada gratuito, si respondes en 24 h) |

Las plantillas tienen categoría: marketing (promociones, siempre se cobra), utilidad (confirmaciones, recordatorios, actualizaciones de una transacción), autenticación (códigos) y servicio (siempre gratis). Meta revisa cada plantilla; Zernio documenta que la aprobación puede tardar hasta 24 horas, y en mi experiencia una plantilla de marketing con botones puede quedarse 'pendiente' un día completo. Escribe las plantillas la primera semana del proyecto, no el día del lanzamiento.

Lo que nadie te dice hasta que te pasa: el error de ventana cerrada no se ve al enviar. La API acepta el mensaje, devuelve un identificador, y unos tres segundos después lo marca como fallido con el texto 'Message failed to send because more than 24 hours have passed since the customer last replied to this number'. Si tu sistema solo revisa que el envío 'salió bien', vas a creer que mandaste recordatorios que nadie recibió. Hay que leer el estado final del mensaje, no la respuesta inmediata.

> Consecuencia de diseño: el bot responde libre mientras la conversación está viva, y todo lo que salga por iniciativa tuya (confirmación de cita, recordatorio, reactivación) va por plantilla. Si tu proveedor lo soporta, Meta también acepta mensajes de utilidad sin plantilla previa ('Direct Send') en cuentas elegibles; lo probé y funciona para transaccionales, pero si lo usas para marketing te retiran el acceso.

## 04 · La arquitectura: webhook, cerebro, herramientas, respuesta

_cómo está armado_

Un bot de WhatsApp con IA tiene cuatro piezas, siempre las mismas. Entenderlas te sirve aunque no las programes tú: es lo que le vas a pedir a quien lo construya, y es lo que vas a revisar cuando algo falle.

1. Recibir: WhatsApp (a través de Zernio) avisa a tu sistema con un webhook, que es una dirección web tuya a la que le mandan cada mensaje nuevo en el instante en que llega. Tu sistema contesta 'recibido' de inmediato y procesa en segundo plano; si tarda en contestar, el proveedor reintenta y terminas respondiendo dos veces.
2. Pensar: el 'cerebro' junta el historial de esa persona, las reglas del negocio (el prompt de sistema) y el mensaje nuevo, y se lo pasa a un modelo de lenguaje. El modelo no responde directo: decide si necesita una herramienta.
3. Actuar: las herramientas son funciones concretas que el modelo puede pedir: ver_horarios (consulta el calendario real), agendar_cita (crea el evento), escalar_a_humano (marca la conversación y avisa al equipo), no_escribir_mas (da de baja). El sistema ejecuta la herramienta, le devuelve el resultado al modelo y este repite hasta tener una respuesta en texto.
4. Responder: el texto final sale por la API de WhatsApp. Si estamos dentro de la ventana de 24 h, como mensaje libre; si es un envío por iniciativa del negocio, como plantilla.

El bucle 'modelo pide herramienta, sistema la ejecuta, modelo sigue' es lo que reemplaza los diagramas de flujo de las herramientas sin código. Yo tuve el mismo bot en un editor visual de automatizaciones con 18 nodos y en código con un archivo de 150 líneas. La versión en código responde en 3 a 4 segundos contra 12, se prueba, se versiona y no se rompe cuando alguien mueve una cajita.

**Atajo: clonar el repo de esta guía (terminal)**

```bash
git clone https://github.com/davidiriza-lab/chatbot-whatsapp-zernio.git
```

> Repo público con licencia MIT: github.com/davidiriza-lab/chatbot-whatsapp-zernio. Trae los archivos de abajo listos para copiar a tu proyecto.

Así se ve el cerebro en código: las reglas duras van antes del modelo (la baja gana siempre; si la persona está con un humano, el bot calla), después el historial y el mensaje nuevo se mandan a la API del modelo con las cuatro herramientas declaradas, y el bucle repite mientras el modelo pida herramientas. Es una llamada HTTP directa, sin SDK, para que se vea todo lo que viaja. El prompt de sistema se lee de un archivo y se marca para caché: son 4 mil tokens que no cambian entre turnos.

**lib/cerebro.ts — reglas duras + bucle modelo/herramientas**

```typescript
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { agendarCita, escalarAHumano, noEscribirMas, verHorarios } from "@/lib/herramientas";
import type { Persona } from "@/lib/memoria";

/**
 * El cerebro: reglas duras primero, modelo después.
 * Llama a la API de Anthropic con fetch (sin SDK) y repite el bucle
 * "modelo pide herramienta, sistema la ejecuta, modelo sigue" hasta tener texto.
 */

const API = "https://api.anthropic.com/v1/messages";
const MODELO = process.env.MODELO_CONVERSACION ?? "claude-sonnet-5";
const MAX_VUELTAS = 6; // tope del bucle de herramientas por turno

const PALABRAS_BAJA = ["stop", "baja", "no me escribas", "no me escriban", "dejen de escribir"];

let promptCache: string | null = null;
function promptSistema(): string {
  if (promptCache) return promptCache;
  const leido = readFileSync(join(process.cwd(), "prompts", "sistema-bot.md"), "utf8");
  promptCache = leido;
  return leido;
}

// --- Tipos mínimos del wire de la API de Mensajes ---

interface BloqueTexto {
  type: "text";
  text: string;
}
interface BloqueUsoHerramienta {
  type: "tool_use";
  id: string;
  name: string;
  input: Record<string, unknown>;
}
interface BloqueResultado {
  type: "tool_result";
  tool_use_id: string;
  content: string;
}
type BloqueRespuesta = BloqueTexto | BloqueUsoHerramienta | { type: string };
type BloqueEntrada = BloqueTexto | BloqueUsoHerramienta | BloqueResultado;

interface MensajeApi {
  role: "user" | "assistant";
  content: string | BloqueEntrada[];
}

interface RespuestaApi {
  content: BloqueRespuesta[];
  stop_reason: "end_turn" | "tool_use" | "max_tokens" | "refusal" | string;
  error?: { message: string };
}

const HERRAMIENTAS = [
  {
    name: "ver_horarios",
    description: "Consulta el calendario real y devuelve hasta 3 horarios disponibles en hora local.",
    input_schema: {
      type: "object",
      properties: { preferencia: { type: "string", description: "Rango o día que prefiere la persona" } },
    },
  },
  {
    name: "agendar_cita",
    description: "Crea la cita en el calendario. Solo después de las tres preguntas y del 'sí' explícito.",
    input_schema: {
      type: "object",
      properties: {
        fecha_hora: { type: "string", description: "fecha_hora exacta devuelta por ver_horarios" },
        nombre: { type: "string" },
        telefono: { type: "string" },
        nota: { type: "string", description: "giro, objetivo y filtro en una línea" },
      },
      required: ["fecha_hora", "nombre", "telefono", "nota"],
    },
  },
  {
    name: "escalar_a_humano",
    description: "Marca la conversación para que una persona del equipo la tome.",
    input_schema: { type: "object", properties: { motivo: { type: "string" } }, required: ["motivo"] },
  },
  {
    name: "no_escribir_mas",
    description: "Da de baja a la persona. No se le vuelve a escribir.",
    input_schema: { type: "object", properties: {} },
  },
];

function ejecutarHerramienta(persona: Persona, nombre: string, input: Record<string, unknown>): string {
  const s = (k: string): string => (typeof input[k] === "string" ? (input[k] as string) : "");
  switch (nombre) {
    case "ver_horarios":
      return JSON.stringify(verHorarios(s("preferencia") || undefined));
    case "agendar_cita":
      return JSON.stringify(
        agendarCita(persona, {
          fecha_hora: s("fecha_hora"),
          nombre: s("nombre") || persona.nombre,
          telefono: s("telefono") || persona.telefono,
          nota: s("nota"),
        }),
      );
    case "escalar_a_humano":
      return JSON.stringify(escalarAHumano(persona, s("motivo") || "sin motivo"));
    case "no_escribir_mas":
      return JSON.stringify(noEscribirMas(persona));
    default:
      return JSON.stringify({ error: "herramienta desconocida: " + nombre });
  }
}

async function llamarModelo(mensajes: MensajeApi[]): Promise<RespuestaApi> {
  const r = await fetch(API, {
    method: "POST",
    headers: {
      "x-api-key": process.env.ANTHROPIC_API_KEY ?? "",
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model: MODELO,
      max_tokens: 1024, // respuestas de WhatsApp: cortas a propósito
      output_config: { effort: "low" }, // velocidad: el turno debe salir en 3 a 8 s
      system: [{ type: "text", text: promptSistema(), cache_control: { type: "ephemeral" } }],
      tools: HERRAMIENTAS,
      messages: mensajes,
    }),
  });
  const data = (await r.json()) as RespuestaApi;
  if (!r.ok) throw new Error("Anthropic " + r.status + ": " + (data.error?.message ?? "sin detalle"));
  return data;
}

/**
 * Decide qué contestar. Devuelve null cuando el bot no debe hablar
 * (persona en manos de un humano o dada de baja).
 */
export async function pensar(persona: Persona, textoNuevo: string): Promise<string | null> {
  // 1) Reglas duras antes del modelo: la baja gana siempre.
  const t = textoNuevo.trim().toLowerCase();
  if (PALABRAS_BAJA.some((p) => t === p || t.startsWith(p))) {
    noEscribirMas(persona);
    return "Listo, no te vuelvo a escribir. Gracias por tu tiempo.";
  }
  if (persona.etapa === "baja" || persona.etapa === "humano") return null;

  // 2) Historial + mensaje nuevo → modelo.
  const mensajes: MensajeApi[] = persona.historial.map((h) => ({ role: h.rol, content: h.texto }));
  mensajes.push({ role: "user", content: textoNuevo });

  for (let vuelta = 0; vuelta < MAX_VUELTAS; vuelta++) {
    const respuesta = await llamarModelo(mensajes);

    const usos = respuesta.content.filter((b): b is BloqueUsoHerramienta => b.type === "tool_use");
    const textos = respuesta.content.filter((b): b is BloqueTexto => b.type === "text");

    if (respuesta.stop_reason !== "tool_use" || usos.length === 0) {
      const texto = textos.map((b) => b.text).join("
").trim();
      return texto || "Dame un momento, te confirmo con una persona del equipo.";
    }

    // 3) Ejecutar TODAS las herramientas pedidas y devolver los resultados en un solo mensaje.
    mensajes.push({ role: "assistant", content: [...textos, ...usos] });
    mensajes.push({
      role: "user",
      content: usos.map((u) => ({
        type: "tool_result",
        tool_use_id: u.id,
        content: ejecutarHerramienta(persona, u.name, u.input),
      })),
    });
    if (usos.some((u) => u.name === "no_escribir_mas")) return "Listo, no te vuelvo a escribir.";
  }

  escalarAHumano(persona, "bucle de herramientas sin respuesta");
  return "Te paso con una persona del equipo para resolverlo bien.";
}
```

- **Un cerebro, varios canales** — El mismo cerebro puede atender WhatsApp, Instagram y Telegram. Cada canal tiene su webhook de entrada y su función de envío; la memoria, las reglas y las herramientas son las mismas. Así lo tengo: un asistente, tres puertas.
- **Memoria por persona** — Una fila por teléfono en tu base de datos con el historial, la etapa (nuevo, calificando, cita agendada, escalado, baja) y los datos que ya dio. El modelo recibe ese historial en cada turno; sin esto, cada mensaje es una conversación nueva.
- **Triaje antes de vender** — No todo el que escribe es un lead. Un conocido me mandó un emoji y el bot le lanzó el discurso de ventas completo. Ahora, antes de responder, una capa clasifica el mensaje en 'ventas', 'conocido' o 'silencio' usando el CRM y la memoria. Si es conocido, avisa al dueño y atiende sin discurso.
- **Agrupar ráfagas** — La gente escribe en tres mensajes seguidos. Un retraso de 6 segundos antes de procesar junta la ráfaga en un solo turno y evita tres respuestas encimadas. Y un tope de 40 turnos por persona corta al que se pone a platicar con el bot.

## 05 · Conectar WhatsApp con Zernio, paso a paso

_manos a la obra_

Zernio es un servicio que conecta tu número a la API oficial de WhatsApp (la 'Cloud API' de Meta) y te da una sola bandeja y una sola API para WhatsApp, Instagram, Messenger y Telegram. Cobra por cuenta conectada y no agrega margen sobre lo que Meta cobra por mensaje. Lo elegí porque quería un solo sistema de envío para todo, y porque su API expone lo que necesitas sin obligarte a usar su bandeja. Necesitas tres cosas antes de empezar: una cuenta de Meta Business (business.facebook.com), un método de pago registrado, y un número que NO esté dado de alta en el WhatsApp normal ni en WhatsApp Business de celular.

### Lo que ves en pantalla

1. En el panel de Zernio, menú lateral, entra a Connections. Busca la tarjeta de WhatsApp y haz clic en '+ Connect'.
2. Elige el tipo de número: 'Get a new number' (Zernio te lo da; de 3 a 21 dólares al mes según el país) o 'Use my own number'. Para un bot de negocio recomiendo número nuevo y dedicado: te ahorra el problema del número compartido de la sección de errores.
3. Selecciona el país. En países 'instantáneos' el número queda en unos 30 segundos; en países regulados te piden verificación de identidad (KYC) que tarda de 1 a 3 días hábiles. México cae en el segundo grupo: no lo dejes para el final.
4. Zernio verifica el número con Meta automáticamente. Cuando termine, verás el botón 'Continue to WhatsApp setup'.
5. Se abre la ventana de registro de Meta (Embedded Signup). Ahí eliges o creas tu cuenta de WhatsApp Business (WABA), confirmas el número verificado y aceptas los permisos: 'enviar y recibir mensajes' y 'administrar plantillas, números y ajustes'.
6. De regreso en Zernio, pestaña Settings: foto de perfil, nombre visible, descripción, dirección y correo. El nombre visible lo revisa Meta; si no coincide con tu negocio registrado, lo rechaza.
7. Pestaña Templates: aquí creas tus plantillas. Empieza con dos: la confirmación de cita (utilidad) y el recordatorio (utilidad). Cada una tarda hasta 24 h en aprobarse.
8. Settings, Webhooks: agrega la dirección de tu sistema (por ejemplo https://tu-app.vercel.app/api/whatsapp), marca el evento 'message.received' y define un secreto. Con ese secreto tu sistema comprueba que el aviso viene de Zernio y no de cualquiera.

Para probar sin número propio, Zernio tiene un 'sandbox': un número compartido suyo que te manda una plantilla de arranque; contestas cualquier cosa desde tu teléfono y la sesión queda activa 7 días. Los límites, según su documentación: 50 mensajes y 5 destinatarios distintos por cada 24 horas, un teléfono de prueba activo a la vez, y solo esa plantilla fija fuera de la ventana. Sirve para ver llegar los webhooks completos y construir el bot mientras esperas la verificación de tu número; no sirve para atender clientes.

Lo que cuesta, con fecha del 28 de agosto de 2026 y tomado de su página de precios: las primeras 2 cuentas conectadas son gratis sin tarjeta; de la 3 a la 10 cobran 6 dólares al mes cada una y baja con volumen. Un número nuevo va de 3 dólares al mes (Estados Unidos, Canadá, Reino Unido, Alemania, Portugal, entre otros) a 21 (Indonesia); México cuesta 6 dólares al mes y Colombia desde 15. El número se cobra al activarlo y el día 1 de cada mes.

Y una regla de Meta que no depende del intermediario: los límites de mensajería. Un número recién dado de alta puede iniciar conversaciones con 250 teléfonos distintos por 24 horas. Para subir a 2,000 necesitas verificar el negocio (o que el proveedor que te dio de alta lo haga, o mandar 2,000 plantillas entregadas de alta calidad en 30 días). De ahí escala solo a 10,000, 100,000 e ilimitado si mantienes calidad y usas al menos la mitad del límite en 7 días. Responder a quien te escribe no cuenta contra el límite; iniciar sí. Si vas a lanzar una campaña de plantillas, este número decide cuánta gente alcanzas el primer día.

> Si conectas directo a Meta sin intermediario, el primer token que ves en el panel de la app es temporal: dura 24 horas y en tu .env se ve idéntico al permanente. El síntoma es que todo funciona la primera tarde y a la mañana siguiente cada envío regresa 401 sin que hayas cambiado nada. Genera un token de usuario del sistema (System User) desde Business Manager antes de dar por conectado el bot.

> Trampa que me costó una tarde: si tu proveedor de hosting marca la clave de API como 'sensible', al bajar las variables de entorno a tu máquina llegan vacías. El servidor sí las tiene; tu computadora no. Si el bot funciona en producción y no en local, revisa eso antes de tocar código.

## 06 · El prompt de sistema completo

_el corazón del bot_

El prompt de sistema es el documento de instrucciones que el modelo lee antes de cada respuesta. Es donde vive tu negocio, tu tono y, sobre todo, tus prohibiciones. Este es el que uso, con lo específico cambiado por corchetes. Las reglas duras vienen de errores reales: precios inventados, promesas de reembolso, y el modelo diciendo 'sí' a un acompañante que no existía.

**chatbot · Prompt de sistema para un bot de negocio**

_Modelo de lenguaje (Claude, GPT o Gemini) detrás del webhook de WhatsApp_

```text
Eres el asistente de [NOMBRE DEL NEGOCIO] en WhatsApp. Atiendes a personas que escribieron por [SERVICIO PRINCIPAL]. Tu único trabajo es: responder dudas sobre el servicio, hacer tres preguntas para entender si les sirve, y si les sirve, ofrecer un horario y agendar.

## Tono
- Español de México, tú, cálido y directo. Mensajes cortos: máximo 3 líneas, una idea por mensaje.
- Sin emojis, sin signos de exclamación dobles, sin frases motivacionales.
- Nunca digas que eres una IA salvo que te lo pregunten directo; entonces di la verdad en una línea y sigue.

## Lo que sabes (única fuente de verdad)
[SERVICIO]: [descripción en 3 líneas]
Precio: [PRECIO EXACTO, con moneda]. Duración: [X]. Modalidad: [presencial / en línea].
Horario de atención: [días y horas, en hora de la CIUDAD DEL CLIENTE].
Qué incluye: [lista]. Qué NO incluye: [lista].
Preguntas frecuentes: [pregunta → respuesta, 5 a 8].

## Reglas duras (no se negocian)
1. NO inventes precios, descuentos, fechas ni políticas. Si no está arriba, no existe: di "eso te lo confirma una persona del equipo" y usa escalar_a_humano.
2. NO prometas reembolsos, cancelaciones ni cambios. Si lo preguntan, no discutas: escalar_a_humano con el motivo "pregunta por reembolso".
3. NO respondas temas fuera del negocio (noticias, tareas, redactar textos, opiniones). Responde en una línea: "De eso no te puedo ayudar, pero sí de [SERVICIO]" y regresa.
4. NO agendes sin haber hecho las tres preguntas de calificación y sin haber confirmado día y hora con la persona.
5. NO escribas el día de la semana a mano: usa la fecha que te devuelve ver_horarios tal cual.
6. Si la persona escribe "stop", "baja", "no me escribas" o similar: usa no_escribir_mas y despídete en una línea. No insistas.
7. Si la persona se molesta, pide hablar con alguien, o repite la misma pregunta dos veces: escalar_a_humano.

## Calificación (tres preguntas, de una en una, en orden)
1. "¿A qué te dedicas o qué negocio tienes?" → guarda como giro.
2. "¿Qué quieres resolver con [SERVICIO]?" → guarda como objetivo.
3. "[PREGUNTA DE FILTRO: presupuesto, tamaño de equipo, ciudad o urgencia]" → guarda como filtro.
Califica si: [CRITERIO CLARO, p. ej. tiene equipo de 3+ personas y quiere resolverlo este mes].
Si no califica: agradece, di qué sí le sirve ([recurso gratuito o alternativa]) y cierra. No ofrezcas horario.

## Agendar
- Cuando califique, llama ver_horarios y ofrece máximo 3 opciones, en hora local de [CIUDAD].
- Cuando elija, repite día, fecha y hora completos y pide un "sí".
- Con el "sí", llama agendar_cita. Confirma con el dato que devuelve la herramienta, no con lo que recuerdas.
- Si ninguna opción le sirve, pide dos rangos que le acomoden y vuelve a llamar ver_horarios con esa preferencia. Máximo dos rondas; después, escalar_a_humano.

## Herramientas disponibles
ver_horarios(preferencia?) · agendar_cita(fecha_hora, nombre, telefono, nota) · escalar_a_humano(motivo) · no_escribir_mas()

## Formato
Responde solo con el texto que se enviará por WhatsApp. Sin encabezados, sin listas con viñetas, sin markdown.
```

Tres decisiones que valen explicar. Primera: los precios van tal cual en el prompt, copiados de una sola fuente (tu página de ventas o tu hoja de precios), y cuando cambian se cambian ahí. El modelo no negocia. Segunda: el reembolso no se explica, se escala; cualquier frase del bot sobre reembolsos es una promesa que después alguien reclama. Tercera: 'confirma con el dato que devuelve la herramienta': los modelos son buenos redactando y malos recordando fechas. La herramienta es la que sabe.

> Este prompt pesa unos 4 mil tokens por turno. Con el modelo que uso para conversar, una conversación completa de calificación y cita cuesta centavos. Para resúmenes y clasificaciones uso el modelo más barato de la misma familia. En un operativo de varios miles de conversaciones el costo total de modelo quedó por debajo de 400 dólares en el peor escenario calculado.

## 07 · Calificar con tres preguntas y agendar de verdad

_el flujo que vende_

La primera versión de mi bot hacía siete preguntas. La tasa de abandono a la cuarta era brutal: la gente no llena formularios por WhatsApp, platica. Tres preguntas, una por mensaje, con un comentario corto entre cada una, es el límite. Y tienen que ser las tres que tu vendedor haría en los primeros dos minutos de una llamada, no las que quiere el CRM.

| Pregunta | Para qué sirve | Ejemplo de filtro |
| --- | --- | --- |
| ¿A qué te dedicas? | Contexto para personalizar todo lo que sigue y saber si es tu cliente | Giro atendido vs no atendido |
| ¿Qué quieres resolver? | Separa curiosos de gente con un problema; el bot cita esta respuesta al ofrecer horario | Problema concreto vs 'nomás información' |
| La pregunta de filtro | La única que descalifica: presupuesto, tamaño, ciudad o urgencia | '¿Para cuándo lo necesitas?' → este mes califica; 'algún día' no |

Agendar 'de verdad' significa que ver_horarios consulta un calendario real (Google Calendar, el calendario de tu CRM) y resta lo ocupado, con reglas: horario de atención, comida, espacio entre citas, tope por día. Mi regla es 10 a 17 h, comida de 14 a 15, 30 minutos de colchón y máximo cuatro citas al día. Y el dueño puede bloquear horarios desde el chat con su propio asistente. Si el bot ofrece un horario y luego no existe, perdiste al lead y la confianza.

### Después del 'sí'

1. agendar_cita crea el evento en el calendario (con liga de videollamada si es en línea) y guarda la cita en la fila de esa persona.
2. El sistema manda la plantilla de confirmación (categoría utilidad) con nombre, fecha, hora local y liga. Aunque la ventana esté abierta, usar plantilla aquí te asegura que el mismo mensaje funciona cuando la ventana esté cerrada.
3. Dos horas antes de la cita, un proceso programado manda la plantilla de recordatorio y avisa al humano que va a atender.
4. Si el lead calificó pero no contestó, un solo seguimiento unas 20 horas después del último mensaje del bot. A propósito dentro de las 24 h: después de eso, exigiría plantilla y se cobra. Un solo seguimiento; el segundo ya es acoso.

> Sobre el 'sí': un modelo solo, decidiendo si la persona aceptó o rechazó el horario, me resultó inconsistente ('va, pero mejor otro día' lo leía como sí). Ahora una capa de palabras clave decide primero (sí/va/dale/ok vs no/otro/mejor) y el modelo solo entra cuando no hay coincidencia clara.

Sobre el calendario: si es Google Calendar, conecta con una cuenta de servicio (service account) y compártele el calendario una vez, no con tu usuario por OAuth. El acceso por tu usuario se consigue en cinco minutos y se cae el día que cambias tu contraseña, revocas sesiones o alguien sale de la empresa; la cuenta de servicio es un usuario que es un programa y sobrevive a todo eso. Para un negocio, siempre la segunda.

Y un detalle que se ve solo en producción: entre que el bot ofrece un horario y la persona elige, alguien más pudo ocupar ese hueco. agendar_cita tiene que volver a consultar disponibilidad antes de crear el evento y, si ya no está, decirlo y ofrecer otro. Confirmar un horario que ya no existe es peor que no haber ofrecido ninguno.

## 08 · La primera semana en modo borrador

_antes de soltarlo_

Del bot salen tres cosas que no se deshacen: un mensaje a un cliente, un evento en tu calendario y una fila en tu base. Mi recomendación, y así lo arranco ahora con cada cliente, es que la primera semana ninguna de las tres ocurra sola. El bot redacta la respuesta, propone la cita y prepara el registro, y una persona ve el borrador en una bandeja y aprueba o corrige. Un borrador que no te gustó se corrige en diez segundos; un mensaje que ya salió con un precio inventado no.

### Cómo se suelta

1. Semana uno: todo en borrador. Cada respuesta pasa por una persona antes de enviarse. Anota qué corregiste y por qué; esa lista es tu siguiente versión del prompt.
2. Cuando de los últimos 20 borradores hayas mandado al menos 16 sin tocar, y no haya aparecido un solo precio fuera de tu lista ni un horario que no existiera, suelta el envío de mensajes. Solo eso.
3. Otra semana después, con el mismo criterio, suelta agendar_cita. La escritura al CRM la sueltas al final: es la más fácil de ensuciar y la más difícil de limpiar.
4. Si algo se descompone (una plantilla pausada, un cambio de precios, un prompt nuevo), regresa ese paso a borrador. Regresar no es fracasar; es lo que evita el incidente.

Al final del primer mes no solo tienes un bot andando: tienes por escrito las objeciones que tu equipo contesta de memoria ('está caro', '¿lo puedo pagar en partes?', '¿y si no me sirve?', 'déjame consultarlo'). Cada una que apareció y el prompt no supo contestar se vuelve una línea en la sección de preguntas frecuentes. Y ojo con una regla que aprendí a la mala: el bot solo contesta objeciones que estén escritas. A las que no están, las nombra y las escala. Improvisar una respuesta a 'está caro' con el margen equivocado cierra peor que no contestar.

| Partida | Cuánto (28 de agosto de 2026) | De dónde sale |
| --- | --- | --- |
| Intermediario (Zernio) | 0 con hasta 2 cuentas; 6 USD/mes por cuenta de la 3 a la 10 | Página de precios de Zernio |
| Número dedicado | 6 USD/mes en México; de 3 a 21 según país | Página de precios de Zernio |
| Plantillas de Meta | Por mensaje entregado, según categoría y país; utilidad gratis dentro de la ventana | Pricing oficial de la Cloud API |
| Respuestas dentro de la ventana | 0 | Pricing oficial de la Cloud API |
| Modelo de lenguaje | Centavos por conversación con el prompt de esta guía; el resumen cada 6 h con el modelo barato | Mi operación |
| Hosting | El plan que ya pagas por tu web; el webhook no agrega servidores | Mi operación |

> Circula por ahí que 'a partir del 1 de octubre de 2026 Meta cobrará también las respuestas dentro de la ventana de 24 horas'. Fui a la página oficial de actualizaciones de precios: lo que hay para esa fecha son ajustes de tarifa de utilidad y autenticación en nueve países (Bangladesh, Irak, Nepal, Sri Lanka a la baja; Kazajistán, Kuwait, Marruecos, Omán, Ucrania al alza), y la misma página repite que las plantillas de utilidad dentro de la ventana siguen siendo gratis. Si eso cambia, lo verás ahí primero; no diseñes tu bot alrededor de un rumor.

## 09 · Qué medir cada semana

_sin esto no sabes si sirve_

Un bot que 'contesta bonito' no es un resultado. Estas son las cinco cifras que reviso; las cuatro primeras salen de tu base de datos de conversaciones, la última del calendario. Un resumen automático cada 6 horas (generado por el modelo barato sobre las conversaciones del periodo) me da además los motivos de escalación y las preguntas que el prompt no supo responder: esas van directo a las preguntas frecuentes del prompt.

| Métrica | Cómo se calcula | Señal de alarma |
| --- | --- | --- |
| Tasa de respuesta del bot | Mensajes entrantes atendidos en menos de 10 s / total | Menos de 95%: el webhook está reintentando o el modelo tarda |
| Calificación completada | Personas que respondieron las 3 preguntas / personas que escribieron | Menos de 40%: las preguntas son largas o llegan muy pronto |
| Citas agendadas | agendar_cita exitosas / calificados | Menos de 50%: los horarios ofrecidos no acomodan o el 'sí' se detecta mal |
| Escalaciones a humano | escalar_a_humano / conversaciones, con motivo | Más de 20%: al prompt le faltan respuestas; revisa los motivos |
| Asistencia a la cita | Citas con la persona presente / citas agendadas | Menos de 60%: revisa el recordatorio (¿llegó?, ¿hora correcta?) |

Y una que no es de negocio pero te salva: mensajes salientes con estado 'fallido'. Si sube, o cerraste ventanas sin darte cuenta o una plantilla fue pausada por Meta. Zernio manda un webhook cuando una plantilla cambia de estado; suscríbete a ese evento también.

## 10 · Errores reales y cómo se evitan

_lo que ya me costó_

- **El aviso interno que nunca llegó** — Usé el número del bot para avisarme a mí mismo de cada cita. Como yo no le escribía al bot, mi propia ventana de 24 h estaba cerrada: el aviso 'se enviaba' y fallaba tres segundos después. Una cita a las 17:20 de un viernes se perdió así. Las alertas internas van por Telegram o correo; WhatsApp Business no es confiable para mensajes a frío, ni contigo.
- **Recordatorios una hora antes** — Para una serie de eventos en distintas ciudades, alguien capturó la hora en huso de la capital. En una ciudad con una hora de diferencia, los recordatorios '3 horas antes' salieron a las 2. Regla desde entonces: el copy dice siempre la hora local del cliente ('10:00 am'), y el huso vive solo en la programación del envío. Y antes de cada evento, una revisión: nombre, fecha, día de la semana y huso.
- **'Sábado 9' cuando el 9 era domingo** — Un mensaje masivo con el día de la semana escrito a mano. Fe de erratas a miles de personas. El día de la semana se deriva de la fecha por código; nadie lo teclea, ni el modelo.
- **Un número, dos sistemas** — Tuve el mismo número de WhatsApp enviando desde el CRM y desde otro sistema. Resultado: mensajes duplicados, reglas de baja que uno respetaba y el otro no, y un incidente con una campaña que salió dos veces. Hoy hay un solo sistema de envío, y cualquier otra app que quiera mandar un WhatsApp pasa por una 'puerta' interna que exige plantilla aprobada, respeta las bajas y registra cada envío.
- **Enrolamientos sin fecha** — Un recordatorio automático que se programa 'X horas antes de la cita' no se programa si la cita no tiene fecha. Un lote quedó con fechas vacías y solo salieron 7 de decenas de recordatorios. Antes de activar cualquier envío programado, cuenta cuántos registros tienen la fecha en blanco.
- **Reintentos = respuestas dobles** — Si tu webhook tarda más de 5 segundos en contestar, Zernio lo da por fallido y reintenta (hasta 7 veces). Meta hace lo mismo. Contesta 200 de inmediato, procesa después, y guarda el id de cada evento para ignorar los repetidos. Y ojo: tras 10 fallos seguidos, Zernio desactiva el webhook solo; si el bot 'se calló', revisa los logs de webhooks primero.

## 11 · Dejar que Claude Code lo construya: kits que sí existen y qué esperar

_si no vas a escribir el código_

Si el archivo de 150 líneas de arriba te parece chino, hay otra vía: repos que no traen un bot sino un guion para que Claude Code lo escriba en tu máquina después de entrevistarte sobre tu negocio. Probé dos, ambos con licencia MIT y en español, ambos sobre la API oficial (Meta directo o Zernio), ambos en Python con FastAPI y con despliegue en Railway: whatsapp-agentkit (Hainrixz), que arma un agente que contesta y recuerda a cada cliente, y whatsapp-closer-agentkit, del mismo autor, que además califica con un puntaje, contesta objeciones desde un playbook tuyo, agenda en Google Calendar y escribe en un CRM en Supabase. Los cloné y corrí lo que se puede correr sin credenciales.

- **Lo que verifiqué** — El start.sh del primero solo revisa que tengas Python 3.11+ y Claude Code, crea una carpeta y te dice que escribas /build-agent; el comando existe en .claude/commands. El segundo trae 11 comandos en .claude/skills (start, armar-cerrador, seguir, configurar, playbook, probar, conectar, revisar, publicar, bandeja, soltar) y un script auditar.py con 23 chequeos: en mi máquina pasaron 11 y 12 quedaron 'salteados' porque todavía no existe el agente que Claude escribiría. El propio reporte dice 'un salteado no es un aprobado'. Bien por ellos.
- **Lo que vale la pena robarles** — Tres ideas que adopté: el modo borrador por defecto con confirmación explícita antes de escribir afuera (sección anterior); un modo 'demo' que reproduce entregas de webhook grabadas, con los mismos bytes y la misma firma, para construir todo sin un solo token; y fijar versiones exactas de cada dependencia con ==, nunca con >=, porque lo que se instala hoy no es lo que se probó ayer.
- **Lo que no te van a resolver** — El playbook de objeciones lo escribes tú (es campo obligatorio, y con razón). La verificación del negocio en Meta la esperas tú. La plantilla del recordatorio la das de alta tú antes de la primera cita, o la cita se crea, la confirmación sale y el recordatorio nunca. Y Railway con Postgres son dos servicios encendidos 24 horas: revisa el plan antes de dar por gratis el hosting.
- **Cuándo prefiero mi versión** — Si ya tienes una web en Next.js, el webhook de esta guía vive ahí mismo, en tu hosting y en tu base, sin un segundo servidor en Python. Si no tienes nada y no vas a programar, un kit que te entrevista es un buen primer paso; solo entra sabiendo que después lo vas a mantener tú, y que Claude escribe distinto en cada corrida.

**Probar el kit sin comprometer nada (terminal, en una carpeta aparte)**

```bash
git clone https://github.com/Hainrixz/whatsapp-closer-agentkit && cd whatsapp-closer-agentkit && python3 scripts/auditar.py
```

> El auditor corre sin red y sin credenciales; te dice qué chequeos pasan y cuáles esperan a que el agente exista. Para construirlo, abre claude adentro de la carpeta y escribe /start.

## 12 · Sección técnica: el webhook en Next.js y el envío de plantilla

_para quien lo construye_

Dos rutas de API en Next.js (App Router), sin server actions. La primera recibe los mensajes desde Zernio, valida la firma HMAC-SHA256 del encabezado X-Zernio-Signature, contesta 200 de inmediato y procesa en segundo plano con after(). La segunda muestra cómo se manda una plantilla por la API de Zernio y, por si conectas directo a Meta, el equivalente contra Graph API con la verificación GET (hub.challenge) y la firma X-Hub-Signature-256.

**app/api/whatsapp/route.ts — webhook de entrada (Zernio)**

```typescript
import { NextResponse, after } from "next/server";
import { createHmac, timingSafeEqual } from "node:crypto";
import { atenderMensaje } from "@/lib/bot";

export const runtime = "nodejs";

interface ZernioAdjunto {
  type: string;
  url: string;
}

interface ZernioMensaje {
  id: string;
  conversationId: string;
  platform: "whatsapp" | "instagram" | "facebook" | "telegram" | "sms";
  direction: "incoming" | "outgoing";
  text: string | null;
  attachments: ZernioAdjunto[];
  sender: { id: string; name?: string; phoneNumber?: string | null };
}

interface ZernioEvento {
  id: string;
  event: string;
  timestamp: string;
  message?: ZernioMensaje;
  account?: { id: string };
}

function firmaValida(cuerpo: string, firma: string | null): boolean {
  const secreto = process.env.ZERNIO_WEBHOOK_SECRET;
  if (!secreto || !firma) return false;
  const esperada = createHmac("sha256", secreto).update(cuerpo).digest("hex");
  const a = Buffer.from(esperada);
  const b = Buffer.from(firma);
  return a.length === b.length && timingSafeEqual(a, b);
}

export async function POST(req: Request) {
  const cuerpo = await req.text();
  if (!firmaValida(cuerpo, req.headers.get("x-zernio-signature"))) {
    return NextResponse.json({ error: "firma invalida" }, { status: 401 });
  }

  const evento = JSON.parse(cuerpo) as ZernioEvento;

  // Solo mensajes entrantes de WhatsApp; el resto se acusa y se ignora.
  const m = evento.message;
  if (evento.event !== "message.received" || !m || m.platform !== "whatsapp" || m.direction !== "incoming") {
    return NextResponse.json({ ok: true, ignorado: true });
  }

  // Responder 200 YA y procesar después: Zernio reintenta si tardas más de 5 s.
  after(async () => {
    await atenderMensaje({
      eventoId: evento.id, // clave para ignorar reintentos duplicados
      conversationId: m.conversationId,
      accountId: evento.account?.id ?? "",
      telefono: m.sender.phoneNumber ?? m.sender.id,
      nombre: m.sender.name ?? "",
      texto: m.text ?? "",
      adjuntos: m.attachments,
    });
  });

  return NextResponse.json({ ok: true });
}
```

atenderMensaje es el cerebro: carga la fila de esa persona, aplica el retraso de 6 segundos para agrupar ráfagas, revisa el triaje, corre el bucle modelo + herramientas y manda la respuesta. Para responder dentro de la ventana se usa el endpoint de mensajes de la conversación; para iniciar o reabrir, el de crear conversación con plantilla:

**lib/wa.ts — enviar texto libre (ventana abierta) y plantilla (Zernio)**

```typescript
const BASE = "https://zernio.com/api/v1";

function cabeceras(): HeadersInit {
  return {
    Authorization: "Bearer " + process.env.ZERNIO_API_KEY,
    "Content-Type": "application/json",
  };
}

interface RespuestaEnvio {
  id?: string;
  status?: string;
  error?: string;
}

/** Texto libre: solo si la persona escribió hace menos de 24 h. */
export async function enviarTexto(conversationId: string, accountId: string, message: string): Promise<RespuestaEnvio> {
  const r = await fetch(BASE + "/inbox/conversations/" + conversationId + "/messages", {
    method: "POST",
    headers: cabeceras(),
    body: JSON.stringify({ accountId, message }),
  });
  return (await r.json()) as RespuestaEnvio;
}

/** Plantilla aprobada: abre o reabre la conversación fuera de la ventana. */
export async function enviarPlantilla(params: {
  accountId: string;
  telefono: string; // dígitos con lada, sin "+": 5213312345678
  nombre: string; // nombre de la plantilla aprobada en Meta
  idioma: string; // p. ej. es_MX
  variables: string[]; // en el orden en que aparecen en la plantilla
}): Promise<RespuestaEnvio> {
  const r = await fetch(BASE + "/inbox/conversations", {
    method: "POST",
    headers: cabeceras(),
    body: JSON.stringify({
      accountId: params.accountId,
      participantId: params.telefono,
      templateName: params.nombre,
      templateLanguage: params.idioma,
      templateParams: params.variables,
    }),
  });
  const data = (await r.json()) as RespuestaEnvio;
  if (!r.ok) {
    // TEMPLATE_REQUIRED = intentaste texto libre fuera de la ventana.
    throw new Error("Zernio " + r.status + ": " + (data.error ?? "sin detalle"));
  }
  return data;
}
```

**Probar el envío de la plantilla de confirmación desde la terminal**

```bash
curl -X POST https://zernio.com/api/v1/inbox/conversations -H "Authorization: Bearer $ZERNIO_API_KEY" -H "Content-Type: application/json" -d '{"accountId":"TU_ACCOUNT_ID","participantId":"5213312345678","templateName":"confirmacion_cita","templateLanguage":"es_MX","templateParams":["Ana","jueves 4 de septiembre","10:00 am"]}'
```

> accountId es el id de la cuenta de WhatsApp conectada en Zernio (GET /v1/accounts). La plantilla debe estar en estado APPROVED; si está PENDING, el envío falla y hay que reintentar más tarde.

Si algún día conectas directo a Meta sin intermediario, cambian dos cosas: la verificación inicial del webhook (Meta manda un GET con hub.mode, hub.verify_token y hub.challenge, y tienes que responder el challenge en texto plano) y la firma, que llega en X-Hub-Signature-256 con el formato 'sha256=<hex>' calculada con el App Secret de tu app de Meta. El envío de plantilla va a POST /v23.0/{PHONE_NUMBER_ID}/messages.

**app/api/meta/whatsapp/route.ts — variante directa contra Meta Cloud API**

```typescript
import { NextResponse, after } from "next/server";
import { createHmac, timingSafeEqual } from "node:crypto";
import { atenderMensaje } from "@/lib/bot";

export const runtime = "nodejs";

interface MetaMensaje {
  from: string;
  id: string;
  timestamp: string;
  type: string;
  text?: { body: string };
}

interface MetaPayload {
  object: string;
  entry: {
    id: string;
    changes: {
      field: string;
      value: {
        messaging_product: string;
        metadata: { phone_number_id: string; display_phone_number: string };
        contacts?: { wa_id: string; profile: { name: string } }[];
        messages?: MetaMensaje[];
        statuses?: { id: string; status: "sent" | "delivered" | "read" | "failed"; recipient_id: string }[];
      };
    }[];
  }[];
}

// 1) Verificación única al registrar el webhook en el panel de Meta.
export async function GET(req: Request) {
  const url = new URL(req.url);
  const modo = url.searchParams.get("hub.mode");
  const token = url.searchParams.get("hub.verify_token");
  const reto = url.searchParams.get("hub.challenge");
  if (modo === "subscribe" && token === process.env.WA_VERIFY_TOKEN && reto) {
    return new Response(reto, { status: 200 });
  }
  return new Response("forbidden", { status: 403 });
}

function firmaMetaValida(cuerpo: string, encabezado: string | null): boolean {
  const secreto = process.env.WA_APP_SECRET;
  if (!secreto || !encabezado || !encabezado.startsWith("sha256=")) return false;
  const esperada = createHmac("sha256", secreto).update(cuerpo).digest("hex");
  const recibida = encabezado.slice("sha256=".length);
  const a = Buffer.from(esperada);
  const b = Buffer.from(recibida);
  return a.length === b.length && timingSafeEqual(a, b);
}

// 2) Cada mensaje y cada cambio de estado llegan aquí.
export async function POST(req: Request) {
  const cuerpo = await req.text();
  if (!firmaMetaValida(cuerpo, req.headers.get("x-hub-signature-256"))) {
    return new Response("firma invalida", { status: 401 });
  }

  const payload = JSON.parse(cuerpo) as MetaPayload;

  after(async () => {
    for (const entrada of payload.entry) {
      for (const cambio of entrada.changes) {
        if (cambio.field !== "messages") continue;
        const nombre = cambio.value.contacts?.[0]?.profile.name ?? "";
        for (const m of cambio.value.messages ?? []) {
          if (m.type !== "text" || !m.text) continue;
          await atenderMensaje({
            eventoId: m.id, // wamid: Meta reintenta hasta 7 días, guarda este id
            conversationId: m.from,
            accountId: cambio.value.metadata.phone_number_id,
            telefono: m.from,
            nombre,
            texto: m.text.body,
            adjuntos: [],
          });
        }
        // cambio.value.statuses trae sent/delivered/read/failed: aquí se detecta la ventana cerrada.
      }
    }
  });

  // Meta exige 200; cualquier otro código dispara reintentos.
  return NextResponse.json({ ok: true });
}
```

El envío de plantilla vive en su propio archivo: un route.ts de Next.js solo puede exportar los manejadores HTTP (GET, POST) y la configuración de la ruta; cualquier otra exportación rompe el build.

**lib/wa-meta.ts — envío de plantilla directo a Graph API**

```typescript
/** Envío de plantilla directo a Graph API (variante sin intermediario). */
export async function enviarPlantillaMeta(telefono: string, nombre: string, variables: string[]): Promise<{ id: string }> {
  const r = await fetch("https://graph.facebook.com/v23.0/" + process.env.WA_PHONE_NUMBER_ID + "/messages", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.WA_ACCESS_TOKEN,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      messaging_product: "whatsapp",
      recipient_type: "individual",
      to: telefono,
      type: "template",
      template: {
        name: nombre,
        language: { code: "es_MX" },
        components: [
          { type: "body", parameters: variables.map((text) => ({ type: "text", text })) },
        ],
      },
    }),
  });
  const data = (await r.json()) as { messages?: { id: string }[]; error?: { message: string } };
  if (!r.ok || !data.messages?.[0]) throw new Error("Meta: " + (data.error?.message ?? r.status));
  return { id: data.messages[0].id };
}
```

- Variables de entorno sin prefijo público: ZERNIO_API_KEY, ZERNIO_WEBHOOK_SECRET (o WA_VERIFY_TOKEN, WA_APP_SECRET, WA_ACCESS_TOKEN, WA_PHONE_NUMBER_ID si vas directo). Nunca en el cliente, nunca en el repo.
- Toda escritura a tu base (historial, estado, citas) ocurre dentro de estas rutas en el servidor con la clave de servicio; el bot nunca expone una clave a un navegador.
- Guarda el id de cada evento (Zernio) o el wamid (Meta) en una tabla con restricción de unicidad. Un reintento intenta insertar el mismo id, falla, y se ignora. Es la forma más barata de no responder dos veces.
- Un tope diario de envíos salientes por iniciativa del negocio (empecé con 200) y una tabla de bajas que se consulta antes de CADA envío, incluso los transaccionales.
- El estado 'failed' con la leyenda de 24 h en cambio.value.statuses (Meta) o el evento message.failed (Zernio) es la señal de que intentaste texto libre fuera de la ventana: registra el intento y programa una plantilla.

## Preguntas frecuentes

**¿Puedo usar mi número de WhatsApp de siempre para el bot?**

No mientras esté dado de alta en la app de WhatsApp o WhatsApp Business del celular. La API oficial exige un número que no esté registrado en esas apps. Puedes migrarlo (pierdes la app en el teléfono) o, lo que recomiendo, usar un número nuevo dedicado al bot y dejar el tuyo para lo personal.

**¿El bot puede escribirle primero a mi lista de contactos?**

Solo con plantilla aprobada, solo a gente que aceptó recibir mensajes tuyos, y pagando por mensaje. Si la plantilla es de marketing, Meta la vigila con el índice de calidad: muchas bajas o reportes y te pausan la plantilla o te limitan el número. Para iniciar conversaciones a frío, la vía que no daña la cuenta es un anuncio de clic a WhatsApp: el que escribe abre él la ventana, y 72 horas gratis.

**¿Qué modelo de IA uso?**

Uno con buen manejo de herramientas (tool calling) para conversar, y el más barato de su familia para clasificar y resumir. Yo uso Claude Sonnet para el turno con el cliente y Haiku para triaje y resúmenes. Lo importante no es el modelo sino el prompt con reglas duras y que las fechas y precios vengan de herramientas y datos, no de la 'memoria' del modelo.

**¿Cómo tomo yo la conversación cuando el bot escala?**

escalar_a_humano cambia el estado de esa persona a 'humano' y el bot deja de responderle. Tu equipo la ve en una bandeja (la de Zernio o una propia) y escribe normal mientras la ventana esté abierta; fuera de la ventana, la bandeja solo debe permitir plantillas. Cuando terminan, 'devolver al bot' reactiva la atención automática. La baja ('stop') gana siempre sobre el bot y sobre el humano.

**¿Necesito Zernio o puedo ir directo a Meta?**

Puedes ir directo: la sección técnica trae la variante. Lo que Zernio te ahorra es el alta del número, el registro de la app de Meta, el manejo de tokens que caducan, la descarga de media y una bandeja para el equipo. Si ya tienes un desarrollador y solo un número, directo es más barato. Si vas a tener varios canales o varias cuentas, el intermediario paga su costo el primer mes.

**¿Cuánto cuesta operar el bot?**

Tres partidas: el número y el intermediario (6 dólares al mes un número de México en Zernio, de 3 a 21 según país; las primeras dos cuentas conectadas gratis y 6 dólares al mes por cuenta de la tercera en adelante), los mensajes de plantilla que Meta cobra por entregado según país y categoría (las respuestas dentro de la ventana son gratis), y el modelo de lenguaje (centavos por conversación con el prompt de esta guía). La tabla con fecha está en la sección 'La primera semana en modo borrador'. Lo que más cuesta no está en la factura: es el humano que revisa las escalaciones y mantiene el prompt al día.

**¿Puedo usar whatsapp-web.js, Baileys u OpenWA en vez de la API oficial? Es gratis.**

Puedes, y por eso lo probé. Pero WhatsApp dice en su centro de ayuda que vincular tu cuenta a versiones no oficiales viola sus Términos y puede terminar en suspensión temporal o permanente, o en restricciones para vincular dispositivos. El README de OpenWA lo repite con sus palabras: riesgo de baneo distinto de cero, número que puedas permitirte perder, y para cualquier uso donde el cumplimiento importe, usar la Cloud API oficial. Para un experimento personal con un número desechable, adelante. Para el número de tu negocio, no: lo que te ahorras en plantillas lo pagas el día que el número desaparece y no hay a quién apelar.

**¿Los 'trucos anti-baneo' (esperar unos segundos, variar saludos, no responder de noche) funcionan?**

Funcionan para lo que son: bajar la probabilidad de que el clasificador de Meta te detecte hoy. No cambian que estés violando los Términos, y Meta dice explícitamente que también actúa con evidencia fuera de la plataforma, por ejemplo cuando una empresa anuncia que automatiza WhatsApp de esa forma. Con la API oficial no necesitas ninguno: la espera de 6 segundos de esta guía existe para agrupar ráfagas de mensajes, no para esconderte.

**¿Cuánta gente puede contactar mi número por día?**

Responder a quien te escribe no tiene tope. Iniciar conversaciones con plantilla sí: 250 teléfonos distintos por 24 horas al arrancar; 2,000 cuando verificas el negocio (o cuando el proveedor que te dio de alta lo hace, o cuando entregas 2,000 plantillas de alta calidad en 30 días); de ahí sube solo a 10,000, 100,000 e ilimitado si mantienes la calidad y usas al menos la mitad del límite en una semana. Si planeas una campaña, verifica el negocio antes de necesitarla: cuenta con varios días hábiles aun con los papeles en orden, y bastante más si rechazan el primer intento.

**¿Debo dejarlo en automático desde el primer día?**

No. La primera semana el bot redacta y una persona aprueba cada mensaje, cada cita y cada escritura al CRM. Sueltas de uno en uno, con un criterio medible (de los últimos 20 borradores, 16 sin corregir; cero precios fuera de tu lista; cero horarios inexistentes) y lo regresas a borrador cuando cambies el prompt, los precios o una plantilla. El detalle está en la sección 'La primera semana en modo borrador'.

**¿Cuándo NO me conviene un bot de WhatsApp?**

Cuando no tienes precios cerrados que se puedan escribir en un prompt (el bot no negocia, y si lo dejas negociar inventa). Cuando la venta se cierra en una llamada larga, una visita o una licitación: ahí el bot solo debería agendar. Cuando lo que quieres es prospección en frío a gente que no te escribió: eso no lo permite la API oficial ni con plantillas a cualquiera, y con librerías por QR es la vía más corta al baneo. Y cuando el número que quieres automatizar es tu WhatsApp personal.

**¿Es cierto que desde octubre de 2026 se cobrará dentro de la ventana de 24 horas?**

No según la página oficial de actualizaciones de precios al 28 de agosto de 2026. Lo que hay para el 1 de octubre de 2026 son cambios de tarifa de utilidad y autenticación en nueve países, y la misma página reitera que las plantillas de utilidad entregadas dentro de la ventana siguen gratis. El cambio grande ya pasó: desde el 1 de julio de 2025 se cobra por plantilla entregada, no por conversación. Revisa esa página cada trimestre; es la única fuente.

## Cierre de la guía

El bot no es la IA: es el conjunto de reglas alrededor de la IA. Un prompt que prohíbe inventar, herramientas que consultan datos reales, una ventana de 24 horas respetada, un solo sistema de envío y un humano a un mensaje de distancia. Con eso, un número de WhatsApp atiende a cientos de personas a la vez sin que Meta te toque la puerta. Empieza con un servicio, tres preguntas y dos plantillas, y mide la semana uno. Esta guía vive en el Lab de David Iriza.

## Fuentes oficiales

- [WhatsApp Business Solution Terms (Meta, modificados el 6 de marzo de 2026)](https://www.whatsapp.com/legal/business-solution-terms): La cláusula de 'AI Providers': prohibición de usar la plataforma cuando la IA de propósito general es la funcionalidad principal, y no incidental, de lo que se ofrece.
- [Pricing de la WhatsApp Business Platform (Meta for Developers)](https://developers.facebook.com/docs/whatsapp/pricing): La ventana de servicio al cliente de 24 h, las categorías de plantilla (marketing, utilidad, autenticación, servicio), qué es gratis y la ventana de 72 h de los anuncios de clic a WhatsApp.
- [Webhooks: Getting Started (Meta Graph API)](https://developers.facebook.com/docs/graph-api/webhooks/getting-started): La verificación con hub.mode, hub.verify_token y hub.challenge, la firma X-Hub-Signature-256 y la obligación de responder 200.
- [WhatsApp en Zernio (documentación oficial)](https://docs.zernio.com/platforms/whatsapp): Conexión del número, plantillas, la regla de 24 h en su API, los endpoints de bandeja y los webhooks con firma X-Zernio-Signature.
- [About unofficial apps (Centro de ayuda de WhatsApp)](https://faq.whatsapp.com/1217634902127718): Vincular tu cuenta a versiones no oficiales viola los Términos de Servicio; consecuencias: suspensión temporal o permanente y restricciones para vincular dispositivos.
- [Unauthorized use of automated or bulk messaging on WhatsApp (Centro de ayuda de WhatsApp)](https://faq.whatsapp.com/5957850900902049): Los productos de WhatsApp no están pensados para mensajería automatizada fuera de sus herramientas de negocio; baneos por clasificadores y acciones legales, incluso con evidencia fuera de la plataforma.
- [WhatsApp Business Terms of Service (Meta)](https://www.whatsapp.com/legal/business-terms): La restricción de desarrollar o usar aplicaciones que interactúen con los Business Services sin consentimiento escrito, y las sanciones: limitación, suspensión o terminación de la cuenta.
- [Messaging limits (Meta for Developers)](https://developers.facebook.com/docs/whatsapp/messaging-limits): Los niveles de 250, 2,000, 10,000, 100,000 e ilimitado, qué hace subir de nivel y el papel de la verificación del negocio.
- [Updates to pricing (Meta for Developers)](https://developers.facebook.com/docs/whatsapp/pricing/updates-to-pricing): Cada cambio de tarifa con fecha, incluido lo que sí y lo que no cambia el 1 de octubre de 2026.
- [Precios de Zernio](https://zernio.com/pricing): Cuentas conectadas gratis, costo por cuenta adicional y precio del número por país (México: 6 USD al mes).
- [OpenWA, README (rmyndharis/OpenWA)](https://github.com/rmyndharis/OpenWA): Gateway no oficial sobre whatsapp-web.js y Baileys. Su propia advertencia sobre riesgo de baneo, número desechable y cuándo usar la Cloud API oficial en su lugar.
- [whatsapp-closer-agentkit (Hainrixz)](https://github.com/Hainrixz/whatsapp-closer-agentkit): Blueprint MIT para que Claude Code construya un agente sobre la API oficial; modo borrador, auditor de 23 chequeos y modo demo con entregas grabadas.

## Repositorios

- [davidiriza-lab/chatbot-whatsapp-zernio](https://github.com/davidiriza-lab/chatbot-whatsapp-zernio): Chatbot de WhatsApp con IA: webhook Zernio/Meta en Next.js, cerebro con reglas + herramientas, prompt de sistema y envío de plantillas. (MIT)

## Guías que se conectan con esta

- [escrituras-seguras-desde-una-landing](https://www.davidiriza.com/lab/escrituras-seguras-desde-una-landing): El mismo patrón de escritura server-side con clave de servicio que usa el webhook de esta guía para guardar conversaciones y citas.
- [chatgpt-gemini-o-claude](https://www.davidiriza.com/lab/chatgpt-gemini-o-claude): Cómo elegir el modelo que va detrás del bot y por qué el manejo de herramientas pesa más que el 'qué tan listo' es.
- [cobrar-sin-links-externos](https://www.davidiriza.com/lab/cobrar-sin-links-externos): Si el bot termina en una compra, el cobro puede ocurrir en una página tuya y confirmarse por plantilla de utilidad.
- [claude-code-sin-saber-programar](https://www.davidiriza.com/lab/claude-code-sin-saber-programar): Si vas por la vía de los kits que Claude Code construye entrevistándote, primero conviene saber cómo se trabaja con él sin escribir código.
- [dos-asistentes-un-repo](https://www.davidiriza.com/lab/dos-asistentes-un-repo): El mismo cerebro atendiendo varios canales y varias 'personas' desde un solo repo: la continuación natural de este bot.
- [preparar-tu-app-para-miles-de-usuarios](https://www.davidiriza.com/lab/preparar-tu-app-para-miles-de-usuarios): Cuando el webhook empiece a recibir cientos de mensajes por minuto, ahí está lo que hay que revisar antes de que se caiga.

---

Guía escrita con la información oficial disponible al 28 de agosto de 2026. Esta página no está afiliada a Meta. Ante la duda, revisa la documentación oficial de la WhatsApp Cloud API: https://developers.facebook.com/docs/whatsapp/cloud-api

---

Versión markdown de https://www.davidiriza.com/lab/chatbot-whatsapp-con-ia-para-tu-negocio — el sitio negocia por `Accept: text/markdown` y por sufijo `.md`. Índice para agentes: https://www.davidiriza.com/llms.txt