39 KiB
Documentación Técnica — Bot WhatsApp Cloud API
Propósito: Esta documentación está pensada para ser entregada a otra IA o equipo de desarrollo para reimplementar el bot en cualquier lenguaje/plataforma, con toda la información necesaria sobre flujos, APIs, base de datos y contratos de datos.
1. Resumen del sistema
El sistema es un bot de WhatsApp basado en la WhatsApp Cloud API (Meta/Facebook Graph API v22.0). Permite:
- Recibir y procesar mensajes entrantes mediante un webhook HTTP.
- Responder automáticamente con textos, menús, botones interactivos, listas y archivos multimedia.
- Transferir la conversación a un asesor humano.
- Enviar plantillas de mensaje aprobadas por Meta.
- Enviar mensajes programados.
- Gestionar términos y condiciones (envío y aceptación).
Lenguaje original: PHP 8.x
Base de datos: MySQL / MariaDB
Cola / Workers: Worker PHP con cron (no RabbitMQ ni Redis, aunque se usa Redis como opción para rate-limit).
2. Credenciales y configuración requerida
Todas las credenciales se almacenan en la tabla system_config de la base de datos (no en código duro). También se cargan desde variables de entorno / archivo .env.
2.1 Variables de entorno (.env)
DB_HOST=mysql # Host de la base de datos
DB_PORT=3306
DB_NAME=usite_whatsapp_bot
DB_USER=usite_whatsapp_user
DB_PASS=<password>
DB_CHARSET=utf8mb4
2.2 Configuración de WhatsApp (en tabla system_config)
config_key |
Descripción | Ejemplo |
|---|---|---|
whatsapp_token |
Token de acceso permanente de la App de Meta | EAABz... |
phone_number_id |
ID del número de teléfono de la cuenta de WhatsApp Business | 123456789012345 |
whatsapp_api_url |
URL base de la Graph API | https://graph.facebook.com/v22.0/ |
webhook_verify_token |
Token secreto para verificar el webhook (GET de Meta) | mi_token_secreto |
bot_enabled |
Si el bot responde automáticamente (1/0) |
1 |
business_hours_* |
Horarios de atención (ver sección 8) | — |
terms_message |
Mensaje de términos y condiciones que se envía al usuario | texto libre |
3. API de WhatsApp consumida
Base URL: https://graph.facebook.com/v22.0/{phone_number_id}/
Autenticación: Header Authorization: Bearer {whatsapp_token}
Formato: JSON (Content-Type: application/json) salvo para subida de archivos (multipart/form-data).
3.1 Enviar mensajes — POST /{phone_number_id}/messages
Todos los tipos de mensaje tienen el campo común:
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX"
}
3.1.1 Mensaje de texto
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX",
"type": "text",
"text": {
"body": "Hola, ¿en qué te podemos ayudar?"
}
}
3.1.2 Mensaje de plantilla (template)
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX",
"type": "template",
"template": {
"name": "nombre_plantilla",
"language": { "code": "es" },
"components": [
{
"type": "header",
"parameters": [
{ "type": "text", "text": "Valor del header" }
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "nombre", "text": "Juan" },
{ "type": "text", "parameter_name": "fecha", "text": "20/04/2026" }
]
}
]
}
}
Nota sobre parámetros nombrados vs posicionales:
- Si la plantilla usa variables
{{nombre}}(nombradas): incluir"parameter_name": "nombre".- Si la plantilla usa variables
{{1}},{{2}}(posicionales): omitirparameter_name, usar solo"type": "text", "text": "valor"en orden.
3.1.3 Mensaje interactivo — Botones
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "57300XXXXXXX",
"type": "interactive",
"interactive": {
"type": "button",
"header": { "type": "text", "text": "Encabezado opcional" },
"body": { "text": "Selecciona una opción:" },
"footer": { "text": "Pie de página opcional" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "btn_1", "title": "Opción 1" } },
{ "type": "reply", "reply": { "id": "btn_2", "title": "Opción 2" } }
]
}
}
}
Máximo 3 botones. El
titleno puede superar 20 caracteres.
3.1.4 Mensaje interactivo — Lista
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "57300XXXXXXX",
"type": "interactive",
"interactive": {
"type": "list",
"body": { "text": "Elige una opción de la lista:" },
"action": {
"button": "Ver opciones",
"sections": [
{
"title": "Sección 1",
"rows": [
{ "id": "row_1", "title": "Opción 1", "description": "Descripción opcional" },
{ "id": "row_2", "title": "Opción 2" }
]
}
]
}
}
}
3.1.5 Mensaje de imagen
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX",
"type": "image",
"image": {
"link": "https://ejemplo.com/imagen.jpg",
"caption": "Pie de foto opcional"
}
}
3.1.6 Mensaje de video
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX",
"type": "video",
"video": {
"link": "https://ejemplo.com/video.mp4",
"caption": "Descripción del video"
}
}
3.1.7 Mensaje de audio
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX",
"type": "audio",
"audio": {
"link": "https://ejemplo.com/audio.mp3"
}
}
Para notas de voz, el archivo debe ser
.oggcon códec OPUS y subirse primero al media endpoint.
3.1.8 Mensaje de documento
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX",
"type": "document",
"document": {
"link": "https://ejemplo.com/archivo.pdf",
"filename": "resultados.pdf",
"caption": "Tus resultados de laboratorio"
}
}
3.1.9 Marcar mensaje como leído — POST /{phone_number_id}/conversations
{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.XXXXX"
}
3.1.10 Enviar reacción a un mensaje
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX",
"type": "reaction",
"reaction": {
"message_id": "wamid.XXXXX",
"emoji": "👍"
}
}
3.2 Subir archivo multimedia — POST /{phone_number_id}/media
Content-Type: multipart/form-data
Campos del formulario:
| Campo | Valor |
|---|---|
messaging_product |
whatsapp |
file |
Archivo binario |
type |
MIME type (ej: image/jpeg, video/mp4) |
Respuesta exitosa:
{ "id": "1234567890123456" }
Una vez obtenido el id, se puede usar en mensajes así:
{
"messaging_product": "whatsapp",
"to": "57300XXXXXXX",
"type": "image",
"image": { "id": "1234567890123456" }
}
3.3 Obtener URL de un media — GET /{media_id}
Respuesta:
{
"url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...",
"mime_type": "image/jpeg",
"sha256": "...",
"file_size": 12345,
"id": "1234567890123456",
"messaging_product": "whatsapp"
}
Luego descargar esa URL con el mismo Bearer token.
3.4 Gestión de plantillas — GET /{waba_id}/message_templates
GET https://graph.facebook.com/v22.0/{WABA_ID}/message_templates
Authorization: Bearer {token}
Parámetros opcionales: ?name=nombre_plantilla&status=APPROVED
4. Webhook — Recepción de mensajes
4.1 Verificación del webhook (GET)
Meta envía un GET para verificar el endpoint:
GET /api/webhook.php?hub.mode=subscribe&hub.verify_token=TOKEN&hub.challenge=CHALLENGE
El sistema debe responder con el valor de hub.challenge si hub.verify_token coincide con WEBHOOK_VERIFY_TOKEN.
4.2 Payload de mensaje entrante (POST)
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "WABA_ID",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "57300XXXXXXX",
"phone_number_id": "PHONE_NUMBER_ID"
},
"contacts": [
{
"profile": { "name": "Nombre del Contacto" },
"wa_id": "57300XXXXXXX"
}
],
"messages": [
{
"from": "57300XXXXXXX",
"id": "wamid.XXXXXXXXXXXXX",
"timestamp": "1713900000",
"type": "text",
"text": { "body": "Hola" }
}
]
}
}
]
}
]
}
Tipos de mensaje posibles en el campo type:
type |
Estructura del mensaje |
|---|---|
text |
message.text.body |
image |
message.image.id, message.image.mime_type |
video |
message.video.id, message.video.mime_type |
audio |
message.audio.id, message.audio.voice (bool) |
document |
message.document.id, document.filename, document.mime_type |
sticker |
message.sticker.id |
location |
message.location.latitude, message.location.longitude |
interactive |
message.interactive.type = list_reply o button_reply |
reaction |
message.reaction.emoji, message.reaction.message_id |
Respuesta de acceso interactivo:
{
"interactive": {
"type": "list_reply",
"list_reply": {
"id": "row_1",
"title": "Opción 1",
"description": "descripción"
}
}
}
4.3 Normalización interna de mensajes interactivos
El webhook normaliza las respuestas de lista/botón a texto plano para que el BotService las procese como si fueran texto:
- Si
titleempieza con un número ("1. Opción"), se extrae solo el número. - Si no, se usa el
titlecompleto.
5. Flujo de procesamiento de mensajes
WhatsApp Cloud API
│
▼
POST /api/webhook.php
│
├─ Verificar duplicado (por message_id en conversations)
├─ Obtener/crear usuario en tabla `users`
├─ Guardar nombre del contacto si no tenía
│
▼
WhatsAppWebhook::processConversations()
│
├─ Extraer tipo de mensaje y contenido
├─ Guardar mensaje en tabla `conversations` (direction: incoming)
│
▼
BotService::processMessage($user, $messageText, $messageType)
│
├─ 1. Adquirir advisory lock por usuario (MySQL GET_LOCK) → evita race conditions
├─ 2. Si usuario bloqueado → descartar
├─ 3. Si usuario in_service (atendido por asesor) → descartar
├─ 4. maybeResetConversationAfterIdle() → reinicia si inactividad > 6h
├─ 5. Flujo de Términos:
│ ├─ Si terms_pending → processTermsResponse()
│ └─ Si needsTermsAcceptance() → sendTermsMessage()
├─ 6. Si 'isNewUser' → sendWelcomeMessage()
├─ 7. Si usuario escribe "enviado/a/os/as" → requestAdvisor()
├─ 8. processSpecialCommands() (MENU, ATRÁS, ASESOR, etc.)
├─ 9. Si on_hold activo y no expiró → notificar y retornar
├─10. Si advisor_requested activo → solo navegación explícita
├─11. Si bot_paused_until activo → solo navegación explícita
├─12. Si bot_enabled=false → solo comandos explícitos
├─13. Si current_menu_id → processMenuSelection()
└─14. Fallback → sendDefaultNoMatch()
6. Comandos especiales del bot
Los usuarios pueden escribir estas palabras clave en cualquier momento:
| Palabra clave | Acción |
|---|---|
menu, menú, inicio |
Mostrar menú principal |
atras, atrás, volver, back |
Retroceder al menú anterior |
asesor, agente, humano |
Solicitar atención humana |
enviado, enviada, enviados, enviadas |
Indicar que se envió documentación — transfiere a asesor |
Número (1, 2, 3…) |
Seleccionar opción de menú activo |
7. Base de datos — Tablas principales del bot
7.1 users — Usuarios (contactos de WhatsApp)
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT AUTO_INCREMENT | Clave primaria |
phone_number |
VARCHAR(20) UNIQUE | Número en formato internacional sin + (ej: 573001234567) |
name |
VARCHAR(100) | Nombre del contacto (viene del perfil de WhatsApp) |
status |
ENUM | active, blocked, inactive |
current_menu_id |
INT NULL | FK → menus.id — menú donde está el usuario |
current_step |
INT DEFAULT 0 | Paso dentro del flujo actual |
session_data |
TEXT (JSON) | Datos temporales de sesión |
welcome_sent_at |
DATETIME | Última vez que se envió el mensaje de bienvenida |
in_service |
TINYINT(1) | 1 = está siendo atendido por un asesor |
in_service_by |
INT NULL | FK → admin_users.id — asesor que lo atiende |
on_hold |
TINYINT(1) | 1 = el asesor lo puso en espera |
bot_paused_until |
DATETIME NULL | El bot está pausado hasta esta fecha/hora |
advisor_requested |
TINYINT(1) | 1 = el usuario solicitó asesor |
bot_enabled |
TINYINT(1) DEFAULT 1 | Si el bot responde a este usuario |
terms_pending |
TINYINT(1) | 1 = esperando respuesta de términos |
terms_accepted_at |
DATETIME | Cuándo aceptó los términos |
terms_version_id |
INT NULL | FK → terms_versions.id |
created_at |
TIMESTAMP | Fecha de creación |
updated_at |
TIMESTAMP | Última actualización |
7.2 conversations — Mensajes
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
user_id |
INT | FK → users.id |
message_id |
VARCHAR(255) | ID único del mensaje en WhatsApp (wamid.XXX) |
reply_to_message_id |
VARCHAR(255) | ID del mensaje al que responde |
reaction_to_message_id |
VARCHAR(255) | ID del mensaje al que se reaccionó |
reaction_emoji |
VARCHAR(64) | Emoji de la reacción |
direction |
ENUM | incoming (usuario→bot) / outgoing (bot→usuario) |
message_type |
VARCHAR(32) | text, image, video, audio, document, reaction |
content |
TEXT | Contenido del mensaje (texto o JSON para multimedia/sistema) |
media_url |
TEXT | URL pública del archivo |
whatsapp_media_id |
VARCHAR(255) | ID del media en WhatsApp |
local_file |
VARCHAR(255) | Ruta local del archivo descargado |
local_thumb |
VARCHAR(255) | Ruta local del thumbnail |
media_storage |
VARCHAR(50) | Dónde se almacena (local, whatsapp, etc.) |
status |
VARCHAR(32) | received, sent, delivered, read, failed |
is_read |
TINYINT(1) | Si el admin marcó la conversación como leída |
filename |
VARCHAR(255) | Nombre del archivo adjunto |
mime_type |
VARCHAR(100) | MIME type del archivo |
created_at |
TIMESTAMP | Fecha del mensaje |
7.3 menus — Menús del bot
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
name |
VARCHAR(255) | Nombre interno del menú |
title |
VARCHAR(255) | Título que se muestra al usuario |
message |
TEXT | Texto del menú completo |
parent_id |
INT NULL | FK → menus.id — menú padre (para navegación ATRÁS) |
is_main |
TINYINT(1) | 1 = es el menú principal |
is_active |
TINYINT(1) | 1 = activo |
sort_order |
INT | Orden de presentación |
created_at |
TIMESTAMP |
7.4 menu_options — Opciones de menú
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
menu_id |
INT | FK → menus.id |
option_number |
INT | Número que el usuario debe escribir (1, 2, 3…) |
title |
VARCHAR(255) | Texto de la opción |
action_type |
ENUM | submenu, message, template, url, advisor, flow |
action_value |
TEXT | Depends en action_type: ID de submenú, texto, nombre de plantilla, URL |
is_active |
TINYINT(1) | 1 = activa |
sort_order |
INT | Orden |
7.5 message_templates — Plantillas
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
name |
VARCHAR(255) | Nombre interno |
template_name |
VARCHAR(255) | Nombre en Meta (slug, sin espacios) |
language_code |
VARCHAR(10) | Idioma (es, en, etc.) |
status |
ENUM | pending, approved, rejected, paused |
body_text |
TEXT | Cuerpo del template con variables {{1}} o {{nombre}} |
header_type |
ENUM | text, image, video, document |
header_text |
VARCHAR(255) | Texto del header si aplica |
footer_text |
VARCHAR(255) | Pie del mensaje |
components |
LONGTEXT JSON | Componentes completos del template tal como los devuelve Meta |
example_parameters |
LONGTEXT JSON | Ejemplos de valores para las variables |
created_at |
TIMESTAMP | |
updated_at |
TIMESTAMP |
7.6 autoresponses — Respuestas automáticas por keyword
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
trigger_type |
ENUM | keyword, contains, exact, welcome, default |
trigger_value |
TEXT | Palabras clave separadas por coma |
response_text |
TEXT | Texto de la respuesta |
response_type |
ENUM | text, template, menu |
template_name |
VARCHAR(255) | Nombre de la plantilla si response_type = template |
menu_id |
INT NULL | ID del menú si response_type = menu |
priority |
INT | Prioridad (mayor número = mayor prioridad) |
is_active |
TINYINT(1) | 1 = activa |
7.7 scheduled_messages — Mensajes programados
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
user_id |
INT | FK → users.id |
template_id |
INT NULL | FK → message_templates.id |
template_name |
VARCHAR(100) | Nombre de la plantilla |
template_language |
VARCHAR(10) | Idioma de la plantilla |
template_parameters |
LONGTEXT JSON | Parámetros para las variables de la plantilla |
message_type |
ENUM | text, template |
message_content |
TEXT | Texto si message_type = text |
scheduled_date |
DATE | Fecha programada |
scheduled_time |
TIME | Hora programada |
status |
ENUM | pending, sent, failed, cancelled |
sent_at |
DATETIME | Cuándo se envió efectivamente |
error_message |
TEXT | Error si falló el envío |
created_by |
INT NULL | Admin que creó el recordatorio |
created_at |
DATETIME |
7.8 system_config — Configuración del sistema
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
config_key |
VARCHAR(255) | Clave de configuración (UNIQUE) |
config_value |
TEXT | Valor de la configuración |
description |
TEXT NULL | Descripción opcional |
created_at |
DATETIME | |
updated_at |
DATETIME |
Claves relevantes para el bot:
config_key |
Descripción |
|---|---|
whatsapp_token |
Token de acceso Bearer |
phone_number_id |
ID del número de teléfono |
whatsapp_api_url |
URL base del API |
webhook_verify_token |
Token secreto del webhook |
bot_enabled |
Activar/desactivar el bot (1/0) |
welcome_message |
Texto del mensaje de bienvenida |
default_no_match |
Mensaje cuando no se entiende la entrada del usuario |
advisor_message |
Mensaje al solicitar asesor |
terms_message |
Texto de los términos y condiciones |
business_hours_enabled |
Si se validan horarios de atención |
business_hours_start |
Hora inicio (ej: 06:15) |
business_hours_end |
Hora fin (ej: 17:00) |
7.9 notifications — Notificaciones internas para asesores
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
user_id |
INT | FK → users.id |
type |
VARCHAR(50) | Tipo de notificación (new_message, advisor_requested, etc.) |
message |
TEXT | Cuerpo de la notificación |
is_read |
TINYINT(1) | Si fue leída |
created_at |
TIMESTAMP |
7.10 terms_versions — Versiones de términos y condiciones
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
version |
VARCHAR(20) | Versión (ej: 1.0, 2.0) |
titulo |
VARCHAR(255) | Título del documento |
documento_url |
TEXT | URL pública del PDF de términos |
mensaje_aceptacion |
TEXT | Mensaje que se envía al usuario solicitando aceptación |
activa |
TINYINT(1) | 1 = versión vigente |
forzar_reenvio |
TINYINT(1) | 1 = forzar que todos los usuarios re-acepten |
created_at |
TIMESTAMP |
7.11 terms_acceptance — Aceptación de términos
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
user_id |
INT | FK → users.id |
terms_version_id |
INT | FK → terms_versions.id |
phone_number |
VARCHAR(20) | Teléfono del usuario |
estado |
ENUM | pendiente, aceptado, rechazado |
fecha_envio |
DATETIME | Cuándo se envió la solicitud |
fecha_respuesta |
DATETIME NULL | Cuándo respondió el usuario |
7.12 admin_users — Administradores del sistema
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
username |
VARCHAR(50) | Nombre de usuario (UNIQUE) |
password_hash |
VARCHAR(255) | Bcrypt hash de la contraseña |
full_name |
VARCHAR(100) | Nombre completo |
email |
VARCHAR(100) | Correo electrónico |
is_active |
TINYINT(1) | 1 = activo |
role_id |
INT NULL | FK → roles.id |
last_login |
TIMESTAMP | Último login |
7.13 webhook_logs — Logs del webhook
| Campo | Tipo | Descripción |
|---|---|---|
id |
INT | Clave primaria |
payload |
LONGTEXT | Body JSON recibido de WhatsApp |
response |
TEXT | Respuesta devuelta |
status_code |
INT | Código HTTP |
created_at |
TIMESTAMP | Fecha del evento |
8. Formato del número de teléfono
Los números se almacenan y envían sin el símbolo +, en formato E.164:
- Colombia:
573001234567(código país 57 + número sin prefijo) - Argentina:
5491123456789
El sistema normaliza automáticamente:
- Elimina caracteres no numéricos.
- Si el número tiene 10 dígitos y empieza con
3→ agrega prefijo57(Colombia). - Si ya tiene 12 dígitos → lo usa tal cual.
9. Lógica de estados del usuario
El bot controla el flujo mediante campos en la tabla users:
Estado normal:
bot_enabled=1, in_service=0, on_hold=0, advisor_requested=0, bot_paused_until=NULL
→ Bot responde normalmente
En menú:
current_menu_id=<id> → Bot espera número de opción
Atendido por asesor:
in_service=1, in_service_by=<admin_id>
→ Bot NO responde, asesor humano escribe directamente
En espera (hold activo):
on_hold=1, bot_paused_until=<datetime>
→ Bot notifica que está en espera, solo permite MENU/ATRÁS/números
Asesor solicitado:
advisor_requested=1, bot_paused_until=<datetime>
→ Bot espera al asesor, solo permite navegación explícita
Bot pausado:
bot_paused_until=<datetime futura>
→ Bot no responde mensajes libres, solo menús/comandos
Pendiente de términos:
terms_pending=1
→ Bot solo procesa respuesta "acepto" o "rechazo"
Usuario bloqueado:
status='blocked'
→ Todos los mensajes se descartan silenciosamente
10. Estados de conversación (tabla user_states)
Estados clave/valor persistentes por usuario (como sesión):
state_key |
Descripción |
|---|---|
menu_history |
Array JSON con historial de IDs de menús visitados |
current_menu |
ID del menú actual |
form_data |
Datos temporales de un formulario en curso |
11. Endpoints internos del sistema (API interna)
Estos endpoints son para el panel de administración, no son de WhatsApp. Se implementarían como API REST en el nuevo sistema:
| Método | Ruta interna | Descripción |
|---|---|---|
| GET | /api/get_conversations.php |
Lista de conversaciones con paginación |
| GET | /api/get_conversation_detail.php?user_id=X |
Mensajes de un usuario |
| POST | /api/send_message.php |
Enviar mensaje desde el panel |
| POST | /api/send_reply.php |
Responder a un mensaje |
| POST | /api/attend.php |
Marcar usuario como "en atención" por asesor |
| POST | /api/finish_attend.php |
Liberar usuario de la atención |
| POST | /api/release_hold.php |
Liberar usuario del estado on_hold |
| GET | /api/get_templates.php |
Lista de plantillas aprobadas |
| POST | /api/send_broadcast.php |
Enviar mensaje masivo a múltiples usuarios |
| POST | /api/schedule_message.php |
Programar un mensaje |
| GET | /api/get_menus.php |
Lista de menús configurados |
| POST | /api/save_menu.php |
Crear/editar menú |
| GET | /api/get_settings.php |
Configuración del sistema |
| POST | /api/save_settings.php |
Guardar configuración |
| GET | /api/sse_events.php |
Server-Sent Events para tiempo real |
12. Flujo de términos y condiciones
Usuario envía mensaje
│
▼
¿needsTermsAcceptance()?
│ SÍ
▼
sendTermsMessage()
→ Envía texto con el mensaje de términos + URL del documento
→ Guarda users.terms_pending = 1
→ Inserta en terms_acceptance (estado: 'pendiente')
│
Usuario responde
│
processTermsResponse()
├─ Si respuesta contiene "acepto", "si", "sí", "ok", "1" → ACEPTA
│ → Actualiza users.terms_accepted_at, terms_version_id
│ → Actualiza terms_acceptance.estado = 'aceptado'
│ → Envía mensaje de confirmación
└─ Si respuesta contiene "no", "rechazo", "2" → RECHAZA
→ Actualiza terms_acceptance.estado = 'rechazado'
→ Envía mensaje de rechazo
13. Worker de mensajes programados
Un proceso cron (o worker) ejecuta cada minuto:
- Consulta
scheduled_messagesdondestatus = 'pending'yscheduled_date + scheduled_time <= NOW(). - Para cada registro:
- Si
message_type = 'template'→ llama aWhatsAppService::sendTemplateMessage(). - Si
message_type = 'text'→ llama aWhatsAppService::sendTextMessage().
- Si
- Actualiza
status = 'sent'y registrasent_at. - Si falla →
status = 'failed'+ registraerror_message.
14. Diagrama de clases del bot
WhatsAppWebhook (api/webhook.php)
│
├──uses──▶ WhatsAppService (services/WhatsAppService.php)
│ ├── sendTextMessage($to, $message)
│ ├── sendTemplateMessage($to, $name, $lang, $bodyParams, $headerParams, $rawComponents)
│ ├── sendInteractiveMessage($to, $body, $buttons, $header, $footer)
│ ├── sendListMessage($to, $body, $buttonText, $sections, $header, $footer)
│ ├── sendImageMessage($to, $url, $caption)
│ ├── sendVideoMessage($to, $url, $caption)
│ ├── sendAudioMessage($to, $url)
│ ├── sendDocumentMessage($to, $url, $filename, $caption)
│ ├── sendReactionMessage($to, $messageId, $emoji)
│ ├── markAsRead($messageId)
│ ├── uploadMedia($filePath, $mimeType) → retorna { id: "..." }
│ └── getMediaUrl($mediaId) → retorna URL temporal
│
└──uses──▶ BotService (services/BotService.php)
│
├──uses──▶ MenuService (services/MenuService.php)
│ └── getMenu($id), getMenuOptions($menuId)
│
├──uses──▶ ConversationStateService (services/ConversationStateService.php)
│ ├── getCurrentMenuId($phone)
│ ├── setCurrentMenu($phone, $menuId)
│ └── getStateData($phone, $key)
│
├──uses──▶ BusinessHoursService (services/BusinessHoursService.php)
│ └── isWithinBusinessHours()
│
└──uses──▶ NLPService (services/NLPService.php)
└── analyze($text) → detección de intenciones básica
15. Checklist de implementación
Para reimplementar el bot en otro sistema/lenguaje:
- Configurar la App en Meta for Developers con permisos
whatsapp_business_messaging,whatsapp_business_management. - Obtener:
Access Token,Phone Number ID,WABA ID,Webhook Verify Token. - Crear endpoint público HTTPS para el webhook (GET para verificación, POST para mensajes).
- Crear las tablas de base de datos:
users,conversations,menus,menu_options,message_templates,autoresponses,system_config,notifications,terms_versions,terms_acceptance,scheduled_messages,webhook_logs. - Implementar
WhatsAppServicecon los métodos de envío (ver sección 3). - Implementar la lógica de estados del usuario (sección 9).
- Implementar el flujo de menús dinámicos (mensajes numerados).
- Implementar el flujo de términos y condiciones (sección 12).
- Implementar sistema de advisory lock por usuario para evitar mensajes duplicados en procesamiento concurrente.
- Implementar cron/worker para mensajes programados (sección 13).
- Implementar reinicio de sesión después de 6h de inactividad.
16. Notas de seguridad
- Nunca expongas el
whatsapp_tokenen el frontend. - Validar siempre el
hub.verify_tokenen el handshake del webhook. - Sanear todos los campos de texto antes de almacenar en BD para evitar XSS/SQLi.
- Los mensajes duplicados se detectan por
message_idúnico (wamid.XXX) — siempre validar antes de procesar. - Usar advisory locks a nivel de BD para evitar race conditions cuando WhatsApp envía el mismo webhook dos veces en paralelo.