El problema real: mensajes que llegan a la hora equivocada
Mandar un mensaje promocional a las 2 de la mañana no es un error de principiante — es un error de descuido. En 2026, con la madurez que tiene el ecosistema de WhatsApp Business API, ya no hay pretexto para no programar tus envíos con precisión quirúrgica. Este tutorial te explica cómo hacerlo bien desde el inicio: arquitectura, código, costos y las trampas que te van a morder si no las conoces.

Requisitos previos
Antes de escribir una sola línea de código, necesitas tener esto en orden:
- Cuenta de Meta Business Manager verificada y aprobada.
- Número de teléfono registrado en WhatsApp Business API (Cloud API de Meta o proveedor BSP como Twilio, 360dialog, Infobip).
- Access Token de larga duración (o un sistema de refresh automático — los tokens de 24 horas te van a causar dolores de cabeza en producción).
- Base de datos con los números de tus usuarios y sus zonas horarias almacenadas. Este punto es crítico y la mayoría lo ignora.
- Cola de tareas (BullMQ sobre Redis, o Celery si usas Python). No intentes programar mensajes con
setTimeouten Node.js — eso no sobrevive un reinicio de servidor. - Webhook configurado para recibir confirmaciones de entrega y actualizar el estado de cada mensaje.
Paso 1: Estructura tu base de datos para zonas horarias
El error más común: guardar solo el número de teléfono. Guarda también la zona horaria del usuario desde el momento del registro o de su primera interacción.
CREATE TABLE subscribers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
phone VARCHAR(20) NOT NULL UNIQUE,
display_name VARCHAR(100),
timezone VARCHAR(50) NOT NULL DEFAULT 'America/Mexico_City',
opted_in BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Cuando programes un envío masivo, la query que necesitas es esta — convierte el horario deseado (por ejemplo, 10:00 AM) a UTC según la zona de cada usuario:
SELECT
id,
phone,
timezone,
(NOW()::DATE + '10:00:00'::TIME) AT TIME ZONE timezone AT TIME ZONE 'UTC' AS send_at_utc
FROM subscribers
WHERE opted_in = TRUE;
Esto te da la hora exacta en UTC para encolar cada mensaje individualmente.
Paso 2: Configurar la cola de mensajes con BullMQ
Instala las dependencias necesarias:
npm install bullmq ioredis axios
Crea el worker que procesa los mensajes:
// worker.js
import { Worker } from 'bullmq';
import axios from 'axios';
const META_TOKEN = process.env.META_ACCESS_TOKEN;
const PHONE_NUMBER_ID = process.env.META_PHONE_NUMBER_ID;
const worker = new Worker('whatsapp-scheduled', async (job) => {
const { phone, templateName, languageCode, components } = job.data;
const response = await axios.post(
`https://graph.facebook.com/v20.0/${PHONE_NUMBER_ID}/messages`,
{
messaging_product: 'whatsapp',
to: phone,
type: 'template',
template: {
name: templateName,
language: { code: languageCode },
components: components || [],
},
},
{
headers: {
Authorization: `Bearer ${META_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
return response.data;
}, {
connection: { host: 'localhost', port: 6379 },
concurrency: 10, // ajusta según tu rate limit
});
worker.on('failed', (job, err) => {
console.error(`Job ${job.id} falló:`, err.message);
// aquí conectas tu sistema de alertas (Slack, PagerDuty, lo que uses)
});
Paso 3: Encolar los mensajes con delay calculado
// scheduler.js
import { Queue } from 'bullmq';
import { db } from './db.js'; // tu cliente de base de datos
const queue = new Queue('whatsapp-scheduled', {
connection: { host: 'localhost', port: 6379 },
});
async function scheduleBlast(templateName, languageCode, targetHour = 10) {
const subscribers = await db.query(`
SELECT id, phone, timezone,
(NOW()::DATE + $1::TIME) AT TIME ZONE timezone AT TIME ZONE 'UTC' AS send_at_utc
FROM subscribers
WHERE opted_in = TRUE
`, [`${targetHour}:00:00`]);
for (const user of subscribers.rows) {
const delay = new Date(user.send_at_utc).getTime() - Date.now();
if (delay < 0) {
console.warn(`Hora pasada para ${user.phone}, saltando.`);
continue;
}
await queue.add(
'send-message',
{ phone: user.phone, templateName, languageCode },
{
delay,
attempts: 3,
backoff: { type: 'exponential', delay: 5000 },
jobId: `msg-${user.id}-${Date.now()}`, // evita duplicados
}
);
}
console.log(`${subscribers.rows.length} mensajes encolados.`);
}
scheduleBlast('promo_julio', 'es_MX', 10);
Paso 4: Costos de conversación en 2026 — no te sorprendas
Desde que Meta migró a su modelo de cobro por conversación, los mensajes de marketing (que incluyen prácticamente todos los envíos promocionales programados) tienen tarifas que varían por país. En México, el costo por conversación de marketing ronda los $0.0588 USD a mediados de 2026 — verifica siempre en la documentación oficial de Meta porque estos precios cambian trimestralmente.
Lo que debes entender:
- Una conversación = 24 horas de ventana desde el primer mensaje de tu parte. Si mandas 5 mensajes en ese lapso, pagas una sola conversación.
- Las conversaciones de servicio (cuando el usuario escribe primero) tienen tarifa distinta y generalmente más baja.
- Los mensajes de utilidad (confirmaciones de pedido, actualizaciones de envío) tienen su propia categoría y precio.
Implicación práctica: si tienes una lista de 50,000 suscriptores y mandas un blast semanal, estás hablando de aproximadamente $2,940 USD por envío solo en México. Calcula tu ROI antes de apretar el botón.
Paso 5: Manejo de errores en producción
Los errores más frecuentes que verás en producción y cómo responder a cada uno:
| Código de error | Causa | Acción |
|---|---|---|
131026 |
Número no en WhatsApp | Marcar como inválido en DB, no reintentar |
131047 |
Ventana de 24h cerrada para template no aprobado | Verificar categoría del template |
130429 |
Rate limit alcanzado | Backoff exponencial, reducir concurrency |
132001 |
Template no aprobado o suspendido | Revisar en Meta Business Manager |
100 |
Token inválido o expirado | Renovar token, alertar al equipo |
Nunca reintentar a ciegas. Cada reintento innecesario tiene costo y puede llevar a que Meta suspenda tu número si acumulas demasiados errores en poco tiempo.
Advertencias de producción que nadie te dice
No uses el mismo número para pruebas y producción. Meta tiene memoria larga con los números que generan errores masivos durante testing.
Aprueba tus templates con al menos 5 días de anticipación. El proceso de revisión de Meta puede tardarse entre 24 horas y varios días si hay revisión manual, y no te avisan si hay problema — tienes que ir a revisar.
Respeta el opt-out de inmediato. Si un usuario manda "STOP" o cualquier variación, debes actualizar tu base de datos antes del siguiente ciclo de envío. En México, la LFPDPPP y las políticas de Meta lo exigen — el incumplimiento puede costarte el número.
Monitorea tu Quality Rating en Meta Business Manager. Si baja a "Low", Meta limita tu capacidad de envío. Si llega a suspensión, recuperar el número puede tomar semanas.
Resultado final
Con esta arquitectura tienes mensajes que llegan exactamente a las 10 AM en la zona horaria de cada usuario, con reintentos inteligentes, control de costos y logs de todo lo que pasa. Nada de setTimeout, nada de cron jobs que se pierden en un restart, nada de números baneados por descuido.
Esto es lo mínimo que deberías tener corriendo antes de hacer tu primer blast masivo en 2026.