npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

npm version Node

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 voice

Para integrar con Claude Desktop / Code, ver docs/AGENT_MCP.md.


Tabla de contenidos


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.x

Instalació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 list

Tip: 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 --version

Desde 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 test

Configuració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_MS es solo el default global. Cada campaña puede sobreescribirlo con su propio delay_ms.


Arranque

# Modo producción
npm start

# Modo desarrollo (recarga automática ante cambios)
npm run dev

# Ejecutar tests
npm test

El 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 -f

El 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 real

CLI 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 list

Configurar 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 list

Arquitectura

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
└── .env

Componentes 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/cancel

Si 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: pendingauthenticatedready

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 ready y 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: pendingauthenticatedready | disconnected


Obtener QR

GET /api/accounts/:id/qr
{ "qr": "data:image/png;base64,iVBORw0KGgo..." }

Estado de una cuenta

GET /api/accounts/:id/status

Editar 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/reset

Borra la sesión guardada y fuerza un QR nuevo. Útil si la sesión quedó corrupta.


Eliminar cuenta

DELETE /api/accounts/:id

Desconecta 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/:id

Subir 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/:imgId

Iniciar / reanudar campaña

POST /api/campaigns/:id/start

Inicia 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/pause

Funciona desde estados running y scheduled. Para reanudar, llama a /start.


Cancelar campaña

POST /api/campaigns/:id/cancel

Eliminar campaña

Solo en estados pending, done, cancelled y scheduled.

DELETE /api/campaigns/:id

Elimina en cascada contactos, imágenes del disco e mensajes entrantes asociados.


Listar campañas

GET /api/campaigns

Incluye stats, accounts e images (variantes A/B) por campaña.


Detalle de campaña

GET /api/campaigns/:id

Tracking 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/replies

CLI

# 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 micuenta

CLI: 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 1

Flags 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 $ID

Comportamiento 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 40

Pará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] → running

Desde 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 a scheduled. 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=2

Al 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 inbound

Pará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 1

Libreta 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 voice

Si --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_delete
  • messages_send — envío directo
  • alerts_trigger — alerta de emergencia con prefijo + opcional createCallLink

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 ready antes 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_start y send_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/ via LocalAuth
  • El servidor reconecta automáticamente al arrancar
  • Si se borra sessions/, hay que escanear QR de nuevo
  • Usar POST /api/accounts/:id/reset para forzar un QR nuevo

Cuenta baneada o mensajes no entregados

  • Aumentar delay_ms y jitter_ms
  • Activar typing_sim=1
  • Configurar burst_size para pausas periódicas
  • Respetar el daily_limit de 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=3 en .env
  • Aumentar o eliminar cuentas inactivas: wa-blast accounts delete <id>