@mbermeo/wa-blaster
v0.3.2
Published
Sistema de mensajes masivos por WhatsApp con CLI y MCP server para agentes
Readme
Sistema de Mensajes Masivos por WhatsApp
Sistema para enviar mensajes masivos por WhatsApp con soporte multi-cuenta, plantillas con variables, imágenes con A/B testing, scheduling por hora/día, control de campañas y comportamiento anti-spam. Incluye API HTTP, interfaz web, CLI y MCP server para que agentes de IA (Claude, etc.) lo controlen nativamente.
Inicio rápido
# Levantar el server (auto-detecta puerto libre, datos en ~/.wa-blast/)
npx -y @mbermeo/wa-blaster@latest serve
# Desde otra terminal: usar el CLI
npx -y @mbermeo/wa-blaster@latest accounts list
# Alerta de emergencia a un grupo de soporte (transaccional, sin campaña)
npx -y @mbermeo/wa-blaster@latest contacts add \
--alias jefe --phone 593987654321 --group oncall-night
npx -y @mbermeo/wa-blaster@latest alert oncall-night "DB caída" --call voicePara integrar con Claude Desktop / Code, ver docs/AGENT_MCP.md.
Tabla de contenidos
- Requisitos
- Instalación
- Configuración
- Arranque
- Despliegue con Docker
- CLI global (
wa-blast) - Arquitectura
- Flujo de uso
- Interfaz web
- API HTTP
- CLI
- Comportamiento anti-spam
- Scheduling y límites de envío
- Imágenes y A/B testing
- Campañas inbound
- Libreta de contactos y alertas de emergencia
- Formato de archivos de contactos
- Sistema de plantillas
- Base de datos
- Solución de problemas
Requisitos
- Node.js 20.x, 22.x o 24.x — el paquete distribuye bytecode V8 compilado para esas versiones específicas. Si usás otra versión, el binario falla con un mensaje claro al ejecutarse.
- macOS, Linux o Windows — Puppeteer descarga Chromium automáticamente la primera vez (~170 MB).
- Un teléfono con WhatsApp para escanear el QR de autenticación.
¿No tenés una versión compatible de Node?
# Con nvm (recomendado)
nvm install 22 && nvm use 22
# Con fnm
fnm install 22 && fnm use 22
# Con volta
volta install node@22
# Manual: descargá la LTS desde https://nodejs.org/Verificá con:
node --version # debe imprimir v20.x, v22.x o v24.xInstalación
Como CLI/MCP (recomendado)
No requiere instalación: usá npx directamente.
# Probar
npx -y @mbermeo/wa-blaster@latest --version
# Levantar el server (puerto auto-asignado por el OS)
npx -y @mbermeo/wa-blaster@latest serve
# Usar el CLI desde otra terminal (auto-detecta el server)
npx -y @mbermeo/wa-blaster@latest accounts listTip: agregá un alias en tu ~/.zshrc o ~/.bashrc para no repetir el namespace:
alias wab='npx -y @mbermeo/wa-blaster@latest'Como dependencia global
npm install -g @mbermeo/wa-blaster
wa-blast --versionDesde el código fuente (desarrollo)
git clone <repo>
cd agente_whatsapp
npm install
npm run build # compila bytecode para tu versión actual de Node
npm run build:all # compila para Node 20, 22, 24 (requiere nvm)
npm testConfiguración
Crea un archivo .env en la raíz del proyecto:
PORT=3000 # Puerto del servidor HTTP (default: 3000)
DELAY_MS=3000 # Delay por defecto entre mensajes en ms (default: 3000)
MAX_ACCOUNTS=3 # Máximo de cuentas WhatsApp activas simultáneas (default: 3)
DELAY_MSes solo el default global. Cada campaña puede sobreescribirlo con su propiodelay_ms.
Arranque
# Modo producción
npm start
# Modo desarrollo (recarga automática ante cambios)
npm run dev
# Ejecutar tests
npm testEl servidor arranca en http://localhost:3000.
Al arrancar, el sistema reconecta automáticamente todas las cuentas que tienen sesión guardada en ./sessions/ — no es necesario volver a escanear el QR.
Despliegue con Docker
La opción recomendada para producción. El contenedor incluye Chromium del sistema (no descarga el suyo propio), lo que hace la imagen reproducible sin sorpresas de red.
Requisitos
- Docker + Docker Compose
Levantar el servidor
# (Opcional) Copiar y ajustar variables de entorno
cp .env.example .env
# Primera vez — construir imagen y arrancar
docker compose up -d --build
# Ver logs
docker compose logs -fEl servidor queda disponible en http://localhost:3000.
Verificar que funciona
curl http://localhost:3000/health
# → {"status":"ok","timestamp":"..."}Persistencia de datos
Los datos sobreviven reinicios mediante volúmenes montados en el directorio local:
| Directorio local | Montura en contenedor | Contenido |
|---|---|---|
| sessions/ | /app/sessions | Sesiones de WhatsApp (~500 MB por cuenta) |
| data/ | /app/data | Base de datos SQLite (data.db) |
| uploads/ | /app/uploads | Archivos CSV temporales |
| campaign_images/ | /app/campaign_images | Imágenes de campañas (A/B testing) |
Comandos útiles
docker compose restart # Reiniciar sin reconstruir
docker compose up -d --build # Actualizar tras cambios de código
docker compose down # Detener y eliminar contenedor
docker compose logs -f # Seguir logs en tiempo realCLI global (wa-blast)
Tras instalar globalmente el CLI, el comando wa-blast queda disponible en el PATH para controlar cualquier servidor (local o remoto) desde scripts o desde otro equipo.
Instalación
# En el directorio del repo clonado
npm install -g .
# Verificar
wa-blast accounts listConfigurar la URL del servidor
# Via flag en cada comando
wa-blast --url http://mi-servidor:3000 accounts list
# Via variable de entorno (más cómodo para scripts)
export WA_BLAST_URL=http://mi-servidor:3000
wa-blast accounts listArquitectura
agente_whatsapp/
├── src/
│ ├── server.js # Entry point: Express + arranque + auto-reconexión + scheduler
│ ├── db.js # SQLite (better-sqlite3) + schema + migraciones automáticas
│ ├── accountManager.js # Gestión de clientes whatsapp-web.js en memoria
│ ├── campaignRunner.js # Motor de envío: round-robin, anti-spam, scheduling, A/B
│ └── routes/
│ ├── accounts.js # Endpoints REST de cuentas
│ ├── campaigns.js # Endpoints REST de campañas e imágenes
│ └── inbound.js # Endpoints REST de campañas inbound
│ └── utils/
│ ├── csvParser.js # Parseo de CSV y XLSX
│ └── templateEngine.js # Reemplazo de {{variables}}
├── cli/
│ └── index.js # CLI para uso por agente (salida JSON, exit codes)
├── sessions/ # Sesiones de WhatsApp (LocalAuth, generado en runtime)
├── uploads/ # Archivos CSV temporales (se limpian tras importar)
├── campaign_images/ # Imágenes de campañas (persisten)
├── data.db # Base de datos SQLite (generada en runtime)
├── public/
│ └── index.html # Frontend web (SPA estático)
├── test/
│ └── campaigns.test.js # Tests de integración (node:test + supertest)
├── package.json
└── .envComponentes principales
| Componente | Responsabilidad |
|---|---|
| server.js | Arranca Express, monta rutas, reconecta cuentas, ejecuta scheduler de campañas |
| db.js | Conexión singleton a SQLite, schema y migraciones idempotentes |
| accountManager.js | Map en memoria de clientes whatsapp-web.js con sus estados y QR |
| campaignRunner.js | Distribuye contactos en round-robin, lanza un loop por cuenta, anti-spam, scheduling, A/B |
| csvParser.js | Lee CSV/XLSX, normaliza columnas, valida columna phone |
| templateEngine.js | Reemplaza {{variable}} con valores del objeto de datos del contacto |
| cli/index.js | CLI sin dependencias extra, salida JSON limpia, exit codes claros |
Flujo de uso
1. Agregar cuenta WhatsApp → POST /api/accounts
↓
2. Escanear QR con el teléfono → GET /api/accounts/:id/qr
↓
3. Crear campaña (nombre + cuentas + template + opciones) → POST /api/campaigns
↓
4. (Opcional) Subir imagen(es) para A/B testing → POST /api/campaigns/:id/images
↓
5. Subir CSV/XLSX con contactos → POST /api/campaigns/:id/contacts
↓
6. Iniciar campaña → POST /api/campaigns/:id/start
↓
7. Monitorear / pausar / cancelar → GET /api/campaigns/:id
POST /api/campaigns/:id/pause
POST /api/campaigns/:id/cancelSi la campaña tiene límites de scheduling configurados, pasará automáticamente a estado scheduled al alcanzar el tope diario/horario o salir de la ventana de envío, y se reanudará sola cuando las condiciones lo permitan.
Interfaz web
Abre http://localhost:3000 en el navegador.
Panel Cuentas
- Agregar cuenta: ingresa un ID (ej.
micuenta) y haz clic en AGREGAR - Escanear QR: el botón QR aparece mientras la cuenta espera autenticación
- LÍMITE/DÍA: campo inline para configurar cuántos mensajes puede enviar esa cuenta por día (default 50)
- El estado cambia automáticamente:
pending→authenticated→ready
Panel Campañas
- Nueva campaña: abre el modal con todos los parámetros (anti-spam + scheduling)
- UPLOAD CSV: sube contactos (solo en estado
pending) - IMAGEN / 📷 N IMGS: sube imágenes para A/B testing (hasta 5 variantes)
- START: inicia el envío (requiere cuentas
readyy contactos cargados) - PAUSE / RESUME / CANCEL / DEL: botones según estado
- EDIT: edita cualquier parámetro de campañas
pending - TRACKING: modal con funnel ACK en tiempo real, respuestas recibidas y stats por variante A/B
Estados de campaña en la UI
| Badge | Significado |
|---|---|
| PENDING | Lista para configurar |
| RUNNING | Enviando activamente |
| PAUSED | Pausada manualmente |
| ⏰ SCHEDULED | Auto-suspendida por límite/horario; se reanuda sola |
| DONE | Completada |
| CANCELLED | Cancelada definitivamente |
API HTTP
Base URL: http://localhost:3000
Todos los endpoints de recursos devuelven JSON. Los errores incluyen { "error": "..." }.
Health check
GET /health{ "status": "ok", "timestamp": "2026-03-31T10:00:00.000Z" }Cuentas — /api/accounts
Crear / inicializar cuenta
POST /api/accounts
Content-Type: application/json
{ "id": "micuenta" }Respuesta 202:
{
"message": "Cuenta \"micuenta\" inicializando. Use GET /api/accounts/micuenta/qr para obtener el QR.",
"id": "micuenta",
"status": "pending"
}La inicialización es asíncrona. El QR estará disponible en ~5-10 segundos.
Listar cuentas
GET /api/accounts[
{
"id": "micuenta",
"phone": "5491112345678",
"status": "ready",
"daily_limit": 50,
"created_at": "2026-03-31T10:00:00",
"in_memory": true
}
]Estados de cuenta: pending → authenticated → ready | disconnected
Obtener QR
GET /api/accounts/:id/qr{ "qr": "data:image/png;base64,iVBORw0KGgo..." }Estado de una cuenta
GET /api/accounts/:id/statusEditar cuenta
PATCH /api/accounts/:id
Content-Type: application/json
{ "daily_limit": 30 }Actualiza el límite diario de envío. Valor mínimo: 1.
Reset de cuenta
POST /api/accounts/:id/resetBorra la sesión guardada y fuerza un QR nuevo. Útil si la sesión quedó corrupta.
Eliminar cuenta
DELETE /api/accounts/:idDesconecta WhatsApp, destruye el cliente en memoria y elimina el registro de la DB.
Campañas — /api/campaigns
Crear campaña
POST /api/campaigns
Content-Type: application/json
{
"name": "Promo mayo",
"account_ids": ["cuenta1"],
"template": "Hola {{nombre}}, tu código es {{codigo}}",
"delay_ms": 4000,
"jitter_ms": 2000,
"typing_sim": 1,
"burst_size": 15,
"burst_pause_ms": 60000,
"daily_cap": 40,
"hourly_cap": 10,
"send_window_start": "09:00",
"send_window_end": "18:00",
"active_days": "1,2,3,4,5"
}Campos:
| Campo | Tipo | Req. | Default | Descripción |
|---|---|---|---|---|
| name | string | ✓ | — | Nombre de la campaña |
| account_ids | string[] | ✓ | — | IDs de cuentas WhatsApp asignadas |
| template | string | ✓ | — | Texto con {{variables}} |
| delay_ms | integer | — | env DELAY_MS | ms entre mensajes (mín. 1000) |
| jitter_ms | integer | — | 2000 | Variación aleatoria del delay |
| typing_sim | 0|1 | — | 1 | Simular "escribiendo..." |
| burst_size | integer | — | 0 | Pausa cada N mensajes (0=off) |
| burst_pause_ms | integer | — | 60000 | Duración de la pausa periódica |
| daily_cap | integer | — | 0 | Máx. mensajes por día para esta campaña (0=usa límite de cuenta) |
| hourly_cap | integer | — | 0 | Máx. mensajes por hora (0=desactivado) |
| send_window_start | string | — | null | Inicio de ventana de envío (HH:MM) |
| send_window_end | string | — | null | Fin de ventana de envío (HH:MM) |
| active_days | string | — | null | Días activos: "1,2,3,4,5" (1=Lun … 7=Dom). null=todos |
También se acepta
account_id(string) para compatibilidad legacy con una sola cuenta.
Respuesta 201: campaña creada con todos sus campos.
Editar campaña
Solo campañas en estado pending. Acepta todos los mismos campos de creación (todos opcionales).
PATCH /api/campaigns/:idSubir contactos (CSV/XLSX)
Solo campañas en estado pending.
POST /api/campaigns/:id/contacts
Content-Type: multipart/form-data
file=<archivo.csv>Imágenes para A/B testing
Ver sección Imágenes y A/B testing.
GET /api/campaigns/:id/images
POST /api/campaigns/:id/images # multipart/form-data, campo: image
DELETE /api/campaigns/:id/images/:imgIdIniciar / reanudar campaña
POST /api/campaigns/:id/startInicia o reanuda el envío. Funciona desde estados pending, paused y scheduled.
Los contactos se distribuyen en round-robin entre las cuentas ready. Si hay imágenes, también se asignan en round-robin (A/B).
Pausar campaña
POST /api/campaigns/:id/pauseFunciona desde estados running y scheduled. Para reanudar, llama a /start.
Cancelar campaña
POST /api/campaigns/:id/cancelEliminar campaña
Solo en estados pending, done, cancelled y scheduled.
DELETE /api/campaigns/:idElimina en cascada contactos, imágenes del disco e mensajes entrantes asociados.
Listar campañas
GET /api/campaignsIncluye stats, accounts e images (variantes A/B) por campaña.
Detalle de campaña
GET /api/campaigns/:idTracking ACK
GET /api/campaigns/:id/tracking{
"campaign": { "id": 1, "name": "Promo mayo", "status": "scheduled" },
"total": 500,
"sent": 80,
"ack": [
{ "ack_status": "delivered", "count": 65 },
{ "ack_status": "read", "count": 15 }
],
"replies": 3,
"image_variants": [
{
"label": "A", "image_id": 1, "original_name": "promo_verano.jpg",
"total": 250, "sent": 40, "failed": 0, "delivered": 35, "read": 8
},
{
"label": "B", "image_id": 2, "original_name": "promo_invierno.jpg",
"total": 250, "sent": 40, "failed": 0, "delivered": 30, "read": 7
}
]
}image_variants solo aparece si la campaña tiene imágenes configuradas.
Respuestas recibidas
GET /api/campaigns/:id/repliesCLI
# Instalado globalmente
wa-blast <resource> <action> [args] [flags]
# Directo
node cli/index.js <resource> <action> [args] [flags]CLI: Cuentas
wa-blast accounts list
wa-blast accounts add micuenta
wa-blast accounts status micuenta
wa-blast accounts qr micuenta
wa-blast accounts edit micuenta --daily-limit 30
wa-blast accounts reset micuenta
wa-blast accounts delete micuenta
wa-blast accounts messages micuentaCLI: Campañas
# Listar y ver detalle
wa-blast campaigns list
wa-blast campaigns show 1
# Crear campaña básica
wa-blast campaigns create \
--name "Promo mayo" \
--accounts "cuenta1" \
--template "Hola {{nombre}}, tu oferta: {{oferta}}" \
--delay 4000
# Crear campaña con scheduling completo
wa-blast campaigns create \
--name "Campaña semana laboral" \
--accounts "cuenta1" \
--template "Hola {{nombre}}" \
--delay 4000 \
--jitter 2000 \
--typing 1 \
--burst-size 15 \
--burst-pause 60000 \
--daily-cap 40 \
--hourly-cap 10 \
--window-start 09:00 \
--window-end 18:00 \
--active-days "1,2,3,4,5"
# Subir contactos
wa-blast campaigns upload 1 ./contactos.csv
# Imágenes A/B testing
wa-blast campaigns images 1
wa-blast campaigns image-add 1 ./imagen_verano.jpg
wa-blast campaigns image-add 1 ./imagen_invierno.jpg
wa-blast campaigns image-delete 1 2
# Ciclo de vida
wa-blast campaigns start 1
wa-blast campaigns pause 1
wa-blast campaigns cancel 1
wa-blast campaigns delete 1
# Editar campaña pending
wa-blast campaigns edit 1 \
--daily-cap 30 \
--window-start 10:00 \
--window-end 17:00 \
--active-days "1,2,3,4,5"
# Esperar resultado (imprime puntos hasta done/paused/cancelled)
wa-blast campaigns wait 1
wa-blast campaigns wait 1 --interval 10
# Stats y respuestas
wa-blast campaigns tracking 1
wa-blast campaigns replies 1Flags de campaigns create y campaigns edit
Anti-spam:
| Flag | Descripción | Default |
|---|---|---|
| --name <texto> | Nombre | — |
| --accounts <c1,c2> | Cuentas separadas por coma | — |
| --template <texto> | Plantilla del mensaje | — |
| --delay <ms> | Delay base entre mensajes | 3000 |
| --jitter <ms> | Variación aleatoria del delay | 2000 |
| --typing <0\|1> | Simular "escribiendo..." | 1 |
| --burst-size <n> | Pausa cada N mensajes (0=off) | 0 |
| --burst-pause <ms> | Duración de la pausa periódica | 60000 |
Scheduling:
| Flag | Descripción | Default |
|---|---|---|
| --daily-cap <n> | Máx. mensajes/día para esta campaña (0=usa cuenta) | 0 |
| --hourly-cap <n> | Máx. mensajes/hora (0=desactivado) | 0 |
| --window-start HH:MM | Inicio de ventana de envío | null |
| --window-end HH:MM | Fin de ventana de envío | null |
| --active-days <1,2,3,4,5> | Días activos (1=Lun … 7=Dom) | null (todos) |
Ejemplo completo con CLI (uso por agente)
export WA_BLAST_URL=http://mi-servidor:3000
# 1. Verificar cuenta lista
wa-blast accounts status cuenta1
# 2. Crear campaña con límite 50/día, solo días hábiles 9-18h
CAMPAIGN=$(wa-blast campaigns create \
--name "Promo 500 contactos" \
--accounts "cuenta1" \
--template "Hola {{nombre}}, tu descuento es {{descuento}}%" \
--delay 4000 --jitter 2000 --typing 1 \
--daily-cap 50 --window-start 09:00 --window-end 18:00 \
--active-days "1,2,3,4,5")
ID=$(echo $CAMPAIGN | jq '.id')
# 3. Agregar imagen (opcional)
wa-blast campaigns image-add $ID ./promo.jpg
# 4. Subir 500 contactos
wa-blast campaigns upload $ID ./contactos.csv
# 5. Iniciar — enviará 50 hoy, quedará en 'scheduled', se reanuda sola mañana
wa-blast campaigns start $ID
# 6. Monitorear
wa-blast campaigns show $ID | jq '{status: .status, stats: .stats}'
# 7. Ver tracking con stats por variante
wa-blast campaigns tracking $IDComportamiento anti-spam
El sistema implementa cuatro capas de humanización para reducir el riesgo de ban por parte de Meta.
1. Jitter en el delay
delay_final = delay_ms + random(0, jitter_ms)Ejemplo: delay_ms=4000 + jitter_ms=2000 → espera entre 4s y 6s por mensaje.
2. Simulación de escritura (typing_sim)
Antes de enviar: activa "escribiendo...", espera proporcional al largo del mensaje (±30%), detiene el estado, envía.
base = clamp(largo × 60ms, 1500ms, 7000ms)
tiempo = base × (0.7 + random × 0.6)3. Presencia online
Al inicio de cada loop de cuenta se llama sendPresenceAvailable().
4. Pausa periódica (burst pause)
Cada burst_size mensajes, pausa de burst_pause_ms. Ejemplo: burst_size=15 + burst_pause_ms=60000 → pausa de 1 minuto cada 15 mensajes.
Valores recomendados por volumen
| Volumen | delay_ms | jitter_ms | typing_sim | burst_size | burst_pause_ms |
|---|---|---|---|---|---|
| Bajo (<100/día) | 3000 | 1000 | 1 | 0 | — |
| Medio (100-500/día) | 4000 | 2000 | 1 | 20 | 60000 |
| Alto (500+/día) | 5000 | 3000 | 1 | 15 | 90000 |
Scheduling y límites de envío
Permite que una campaña se distribuya automáticamente en varios días respetando límites de WhatsApp.
Límite diario por cuenta
Cada cuenta tiene un daily_limit (default: 50). Cuando una cuenta alcanza su límite del día, la campaña pasa automáticamente a estado scheduled y el scheduler la reanuda al día siguiente.
Configurar via UI (campo "LÍMITE/DÍA" en la tarjeta de cuenta) o CLI:
wa-blast accounts edit cuenta1 --daily-limit 40Parámetros de scheduling por campaña
| Parámetro | Descripción |
|---|---|
| daily_cap | Tope diario de la campaña. Si 0, usa el daily_limit de la cuenta. Útil para usar solo una parte del cupo cuando hay varias campañas activas. |
| hourly_cap | Tope por hora. 0 = sin límite horario. |
| send_window_start / send_window_end | Solo envía dentro de este rango horario (formato HH:MM). |
| active_days | Días de la semana activos: "1,2,3,4,5" = lunes a viernes. 1=Lun, 2=Mar, … 7=Dom. null o vacío = todos los días. |
Estado scheduled
Cuando cualquier límite se alcanza o la campaña está fuera de ventana, el runner cambia automáticamente el estado a scheduled:
running → [límite alcanzado o fuera de ventana] → scheduled
scheduled → [condiciones ok, scheduler cada 60s] → runningDesde la UI/CLI también se puede resumir manualmente una campaña scheduled.
Ejemplo: 500 contactos con cuenta de 50/día
Con daily_cap=50, ventana 09:00-18:00, días 1,2,3,4,5:
- Día 1 (lunes): runner envía 50 →
scheduled - Día 2 (martes): scheduler detecta nuevo día, ventana activa →
running→ envía 50 →scheduled - …10 días hábiles después →
done
Nota: Si la campaña usa múltiples cuentas y una de ellas alcanza su
daily_limit, la campaña completa pasa ascheduled. El scheduler la reanudará cuando al menos una cuenta tenga capacidad.
Imágenes y A/B testing
Permite adjuntar hasta 5 imágenes (variantes A-E) a una campaña. Se distribuyen en round-robin entre los contactos y se trackea la performance de cada variante.
Subir imágenes
# Subir variante A
wa-blast campaigns image-add 1 ./imagen_a.jpg
# → { "id": 1, "label": "A", "original_name": "imagen_a.jpg", ... }
# Subir variante B
wa-blast campaigns image-add 1 ./imagen_b.png
# → { "id": 2, "label": "B", ... }Formatos aceptados: .jpg, .jpeg, .png, .gif, .webp. Máximo 5MB por imagen.
Distribución
Al iniciar la campaña, los contactos se asignan en round-robin a las variantes:
- 9 contactos + 3 variantes → 3 contactos por variante (A, B, C, A, B, C, ...)
- Si hay solo 1 imagen → todos los contactos reciben esa imagen (imagen única sin A/B)
Envío
La imagen se envía con el texto del template como caption.
Tracking por variante
El endpoint GET /api/campaigns/:id/tracking incluye image_variants con métricas separadas por variante: total, sent, delivered, read.
En la UI, el tab "A/B IMAGES" en el modal de tracking muestra estas métricas.
Eliminar imagen
wa-blast campaigns image-delete 1 2 # elimina imagen con id=2Al eliminar, los labels se reordenan automáticamente (A, B, C, ... consecutivos).
Las imágenes solo se pueden agregar/eliminar en campañas en estado
pending.
Campañas inbound
Las campañas inbound son auto-respuestas que se envían automáticamente cuando un contacto escribe a una cuenta WhatsApp. Soportan distribución de cupones únicos por contacto.
Endpoints
GET /api/inbound
POST /api/inbound
GET /api/inbound/:id
POST /api/inbound/:id/activate
POST /api/inbound/:id/pause
DELETE /api/inbound/:id
POST /api/inbound/:id/coupons # multipart, campo: file (.txt/.csv)
GET /api/inbound/:id/coupons
GET /api/inbound/:id/contacts
GET /api/inbound/:id/contacts/export # descarga CSV
POST /api/inbound/:id/to-campaign # crear campaña outbound con los contactos inboundParámetros
| Campo | Descripción |
|---|---|
| account_id | Cuenta que recibirá mensajes entrantes |
| name | Nombre descriptivo |
| template | Mensaje de respuesta (soporta {{nombre}} y {{cupon}}) |
| max_uses | Máximo de usos (0=ilimitado) |
| reply_mode | once_per_campaign (default), always, new_contacts_only |
CLI inbound
wa-blast inbound list
wa-blast inbound create --account cuenta1 --name "Promo QR" --template "Tu cupón: {{cupon}}"
wa-blast inbound coupons-upload 1 ./cupones.txt
wa-blast inbound coupons 1
wa-blast inbound contacts 1
wa-blast inbound export 1 > contactos.csv
wa-blast inbound to-campaign 1 --name "Follow-up" --template "Hola {{nombre}}" --accounts "cuenta1"
wa-blast inbound activate 1
wa-blast inbound pause 1
wa-blast inbound delete 1Libreta de contactos y alertas de emergencia
Funcionalidad pensada para agentes IA o scripts de monitoreo que necesitan contactar a soporte por WhatsApp cuando detectan un incidente — típicamente fuera de horario, donde no hay tiempo de armar una campaña con CSV.
A diferencia de las campañas, este flujo es transaccional: el mensaje sale
inmediatamente vía client.sendMessage(), sin pasar por el motor anti-spam.
No respeta send_window_*, active_days, daily_cap, jitter_ms ni
typing_sim. Eso lo hace adecuado para alertas, pero peligroso si se usa para
marketing.
Salvaguardas
Para evitar que un bug-loop queme la cuenta:
| Salvaguarda | Default | Editable |
|---|---|---|
| direct_daily_cap por cuenta | 200 mensajes/día | PATCH /api/accounts/:id { "direct_daily_cap": N } |
| Rate-limit in-memory | 10 mensajes/min por cuenta | No (hard-coded) |
Si pasás cualquiera de los dos, los envíos extra fallan con código
DIRECT_CAP_EXCEEDED o RATE_LIMITED.
Modelado: alias y grupos
La tabla contacts_book guarda contactos persistentes con un alias único y
un group_name opcional. El input to del envío puede ser:
- Un teléfono crudo:
"593987654321" - Un alias:
"soporte-prod"→ resuelve a 1 contacto - Un
group_name:"oncall-night"→ resuelve a todos los contactos del grupo - Un array mixto:
["soporte-prod", "593555000111", "oncall-night"](deduplicado por phone)
Endpoints HTTP
GET /api/contacts ?group=... Lista (filtrable por grupo)
POST /api/contacts { alias, phone, group_name?, notes? }
GET /api/contacts/groups [{ group_name, count }]
GET /api/contacts/:id
PATCH /api/contacts/:id
DELETE /api/contacts/:id
POST /api/messages/send { account_id, to, message, media_path? }
POST /api/alerts { account_id, to, message, prefix?,
include_call_link?, call_type? }Respuesta de send/alerts:
{
"summary": { "total": 2, "sent": 2, "failed": 0 },
"results": [
{ "phone": "593...", "alias": "jefe", "status": "sent",
"message_id": "[email protected]_yyy", "sent_at": "2026-04-28 03:14:01",
"call_link": "https://call.whatsapp.com/voice/abc..." }
]
}CLI
# Libreta
wa-blast contacts add --alias jefe --phone 593987654321 --group oncall-night
wa-blast contacts list --group oncall-night
wa-blast contacts groups
wa-blast contacts edit 1 --notes "principal"
wa-blast contacts delete 1
# Envío directo (sin prefijo)
wa-blast send oncall-night "Hola, llegó el reporte" --account alertas-bot
# Alerta (prefijo "🚨 ALERTA URGENTE" + opcional link de llamada)
wa-blast alert oncall-night "DB caída" --call voiceSi --account se omite y hay UNA sola cuenta ready, el CLI la usa.
MCP tools
7 tools nuevas, todas documentadas en docs/AGENT_MCP.md:
contacts_list,contacts_groups,contacts_add,contacts_edit,contacts_deletemessages_send— envío directoalerts_trigger— alerta de emergencia con prefijo + opcionalcreateCallLink
Sobre las "llamadas"
whatsapp-web.js no permite iniciar llamadas salientes reales (limitación
de la librería). Lo más cercano es client.createCallLink(), que genera un
link tipo https://call.whatsapp.com/... que el receptor puede tap-ear para
unirse a una sala.
En el modo alerta, el "timbre" efectivo es la push notification del mensaje con prefijo URGENTE — eso es lo que despierta al destinatario. El link de llamada es un acelerador para que pueda unirse a una llamada con un solo tap.
Si createCallLink falla (versión vieja de whatsapp-web.js, error de red,
etc.), la alerta igual se envía sin link y la respuesta incluye
call_link_warning.
Skill para agentes IA
El paquete incluye una skill (skills/wa-blaster-alerts/) con el contexto y
los flujos típicos para que un agente IA opere las tools MCP correctamente.
Ver instrucciones de instalación en skills/wa-blaster-alerts/SKILL.md.
Auditoría
Cada envío directo o alerta se registra en la tabla direct_messages con
account_id, phone, alias, kind, status, message_id, error? y
call_link?. Útil para diagnosticar "por qué no llegó la alerta de las 3 AM".
Recomendado purgar manualmente cada 90 días.
Formato de archivos de contactos
Se aceptan CSV y Excel (.xlsx, .xls). Tamaño máximo: 10MB.
La columna de teléfono puede llamarse: phone, telefono, numero, número.
El resto de columnas se convierten en variables para el template.
Ejemplo
phone,nombre,empresa,descuento
5491112345678,Juan García,Acme SA,20%
5491133445566,María López,Tech Corp,15%
5491144556677,Carlos Martínez,StartupXYZ,30%Formato de teléfonos
- Incluir código de país sin el símbolo
+ - Se eliminan automáticamente: espacios, guiones, paréntesis,
+ - Argentina:
54+9+ código de área + número →5491112345678
Sistema de plantillas
Hola {{nombre}}, te contactamos de {{empresa}}.
Tu código es {{codigo}} válido hasta {{vencimiento}}.Si una variable no existe para ese contacto, el placeholder queda tal cual.
Cuando la campaña tiene imagen, el template se envía como caption de la imagen.
Base de datos
El archivo data.db (SQLite) se crea automáticamente. Las migraciones son idempotentes.
accounts
| Columna | Tipo | Default | Descripción |
|---|---|---|---|
| id | TEXT PK | — | Identificador de cuenta |
| phone | TEXT | — | Número de teléfono (disponible tras autenticar) |
| status | TEXT | pending | pending / authenticated / ready / disconnected |
| daily_limit | INTEGER | 50 | Máx. mensajes que esta cuenta puede enviar por día |
| created_at | TEXT | now | |
campaigns
| Columna | Tipo | Default | Descripción |
|---|---|---|---|
| id | INTEGER PK | autoincrement | |
| name | TEXT | — | Nombre |
| template | TEXT | — | Plantilla con {{...}} |
| status | TEXT | pending | pending / running / paused / scheduled / done / cancelled |
| delay_ms | INTEGER | 3000 | Delay base entre mensajes |
| jitter_ms | INTEGER | 2000 | Variación aleatoria del delay |
| typing_sim | INTEGER | 1 | Simular escritura (0/1) |
| burst_size | INTEGER | 0 | Pausa cada N mensajes |
| burst_pause_ms | INTEGER | 60000 | Duración de pausa periódica |
| daily_cap | INTEGER | 0 | Tope diario de campaña (0=usa daily_limit de cuenta) |
| hourly_cap | INTEGER | 0 | Tope por hora (0=desactivado) |
| send_window_start | TEXT | null | Inicio de ventana horaria (HH:MM) |
| send_window_end | TEXT | null | Fin de ventana horaria (HH:MM) |
| active_days | TEXT | null | Días activos como CSV de números 1-7 |
| created_at | TEXT | now | |
campaign_accounts
Relación N:N entre campañas y cuentas.
campaign_images
| Columna | Tipo | Descripción |
|---|---|---|
| id | INTEGER PK | |
| campaign_id | INTEGER FK | |
| label | TEXT | A, B, C, D o E |
| file_path | TEXT | Ruta relativa en campaign_images/ |
| original_name | TEXT | Nombre original del archivo |
| created_at | TEXT | |
contacts
| Columna | Tipo | Descripción |
|---|---|---|
| id | INTEGER PK | |
| campaign_id | INTEGER FK | |
| phone | TEXT | Número destino |
| data | TEXT | JSON con variables del CSV |
| status | TEXT | pending / sent / failed |
| error | TEXT | Mensaje de error si falló |
| sent_at | TEXT | Timestamp de envío exitoso |
| assigned_account_id | TEXT | Cuenta asignada (round-robin) |
| image_id | INTEGER FK | Variante de imagen asignada (A/B testing) |
| message_id | TEXT | ID interno del mensaje en WhatsApp |
| ack_status | TEXT | sent / delivered / read / played |
| delivered_at | TEXT | Timestamp de entrega |
| read_at | TEXT | Timestamp de lectura |
incoming_messages
Mensajes recibidos de contactos durante campañas activas.
inbound_campaigns, inbound_coupons, inbound_replies
Tablas para el sistema de auto-respuesta inbound con distribución de cupones.
contacts_book
Libreta persistente para envío directo y alertas.
| Columna | Tipo | Descripción |
|---|---|---|
| id | INTEGER PK | |
| alias | TEXT UNIQUE | Identificador único, ej: "soporte-prod" |
| phone | TEXT | Teléfono normalizado (solo dígitos, 8-15) |
| group_name | TEXT | Grupo opcional, ej: "oncall-night" |
| notes | TEXT | Notas libres |
| created_at / updated_at | TEXT | |
direct_messages
Auditoría de envíos directos y alertas (no campañas).
| Columna | Tipo | Descripción |
|---|---|---|
| id | INTEGER PK | |
| account_id | TEXT | Cuenta que envió |
| phone | TEXT | Destinatario |
| alias | TEXT | Alias resuelto (si aplica) |
| message | TEXT | Cuerpo del mensaje |
| kind | TEXT | direct o alert |
| message_id | TEXT | ID de WhatsApp para tracking ACK (opcional) |
| status | TEXT | sent o failed |
| error | TEXT | Mensaje de error si failed |
| call_link | TEXT | URL de createCallLink si se usó |
| sent_at | TEXT | |
Columna accounts.direct_daily_cap
Tope diario de envíos directos/alertas por cuenta. Default 200. Editable
vía PATCH /api/accounts/:id. Independiente de daily_limit (que aplica
solo a campañas).
Solución de problemas
El QR no aparece
- Esperar ~10 segundos después de
accounts add - Verificar que el servidor esté corriendo:
npm start - Revisar logs: buscar
[clientId] QR generado
"Ninguna cuenta asignada está lista (ready)"
- La cuenta debe estar en estado
readyantes de iniciar - Verificar:
wa-blast accounts status <id>
La campaña queda en scheduled y no reanuda
- Verificar que la hora actual esté dentro de
send_window_startysend_window_end - Verificar que hoy sea un
active_day - Verificar que la cuenta no haya alcanzado su
daily_limit:wa-blast campaigns tracking <id> - El scheduler corre cada 60s; esperar o resumir manualmente con
wa-blast campaigns start <id>
La campaña queda en running indefinidamente
- Si todas las cuentas asignadas se desconectaron, el loop no termina
- Pausar o cancelar manualmente, reconectar cuentas y reiniciar
Error de compilación de better-sqlite3
- Requiere
better-sqlite3 v12+con Node.js v24 - Reinstalar:
npm install better-sqlite3@latest
Sesiones perdidas tras reiniciar
- Las sesiones se guardan en
./sessions/viaLocalAuth - El servidor reconecta automáticamente al arrancar
- Si se borra
sessions/, hay que escanear QR de nuevo - Usar
POST /api/accounts/:id/resetpara forzar un QR nuevo
Cuenta baneada o mensajes no entregados
- Aumentar
delay_msyjitter_ms - Activar
typing_sim=1 - Configurar
burst_sizepara pausas periódicas - Respetar el
daily_limitde 50 mensajes/día por cuenta - Una cuenta nueva necesita warm-up gradual (no enviar 100 el primer día)
Límite de cuentas activas superado
- Default es
MAX_ACCOUNTS=3en.env - Aumentar o eliminar cuentas inactivas:
wa-blast accounts delete <id>
