Plantillas de WhatsApp por API
Crea plantillas de WhatsApp desde tu código, entérate cuando Meta las aprueba o las rechaza y entiende por qué son de la cuenta, no del número.
Fuera de la ventana de 24 horas, lo único que WhatsApp deja entrar es una plantilla aprobada por Meta. Esta guía es para crearlas desde tu código, no desde el panel.
Antes de empezar: una plantilla NO es del número
Es el malentendido que más caro sale, así que va primero.
Una plantilla pertenece a la cuenta de WhatsApp —la WABA—, no a un número de teléfono. Si tu cuenta tiene tres números, la plantilla que crees queda disponible para los tres. No existe «una plantilla para este número».
Y si tienes varias cuentas conectadas al mismo canal, ahí sí importa en cuál la creas: una plantilla de la cuenta A no se puede enviar desde un número de la cuenta B.
El identificador del canal
Todo empieza por el canal:
const canales = await met.channels.list();
const whatsapp = canales.find((c) => c.type === 'whatsapp')!;
Y las plantillas que ya tienes, con la cuenta de la que salió cada una:
const { data, accounts } = await met.channels.templates(whatsapp.id);
Mira siempre
accounts. Si una de tus cuentas no respondió, la lista llega incompleta y con200, no con error. Ahí también salen loswaba_idy sus alias, que es de donde sacas el valor del paso siguiente.
Crear una plantilla
const creada = await met.channels.createTemplate(whatsapp.id, {
name: 'recordatorio_cita',
language: 'es',
category: 'UTILITY',
components: [
{ type: 'BODY', text: 'Hola {{1}}, te recordamos tu cita del {{2}}.' },
],
waba_id: '102938475610293', // opcional: si lo omites, va a tu cuenta primaria
});
creada.template_status; // 'PENDING'
Pide el permiso channels:manage, y una clave de prueba no alcanza: esto manda una solicitud real a revisión de Meta sobre la cuenta de tu cliente.
Si el encabezado lleva imagen
Meta exige una muestra. Pásala como sample_url dentro del componente HEADER:
components: [
{
type: 'HEADER',
format: 'IMAGE',
sample_url: 'https://tu-cdn.com/muestras/factura.png',
},
{ type: 'BODY', text: 'Tu factura de {{1}} ya está lista.' },
]
Tiene que ser https y accesible desde internet: nuestro servidor la descarga para subírsela a Meta, y por seguridad rechaza direcciones internas y de red privada. Pesa máximo 5 MB.
Hay un tope, y conviene entender por qué
25 creaciones por cuenta y por hora. Pasado ese punto sale un 429 con rate_limited.
El motivo no es proteger nuestros servidores: una plantilla rechazada le baja la calificación de calidad a la cuenta de WhatsApp. Si estás integrando para un cliente, esa calificación es de él. Un script que prueba veinte variantes de una plantilla hasta que una pase le deja el daño a alguien que no participó de la decisión.
Enterarte cuando Meta resuelve
createTemplate vuelve al instante con PENDING. Meta decide después, y lo normal es que tarde. Puedes volver a pedir la lista, o suscribirte y que te avisen:
await met.webhooks.subscriptions.create({
url: 'https://tu-servidor.com/webhooks/met',
enabled_events: [
'channel.template.approved',
'channel.template.rejected',
'channel.template.status_updated',
],
});
| Evento | Cuándo | Qué hacer |
|---|---|---|
channel.template.approved | Meta la aprobó | Ya puedes enviarla. |
channel.template.rejected | Meta la rechazó | Trae reason. Corrige y vuelve a crearla con otro nombre. |
channel.template.status_updated | Cambió de estado por otra razón | Léelo. Ver abajo. |
El tercero es el que no conviene ignorar. Meta manda catorce estados distintos por ese canal, y varios —pausada, deshabilitada, marcada, límite excedido— le pasan a plantillas que ya estaban aprobadas y funcionando. Operativamente eso es peor que un rechazo: un rechazo lo descubres cuando vas a mirar, una pausa te corta envíos en curso sin que nadie toque nada.
El data del evento trae template_name, template_language, waba_id, el status crudo de Meta y, cuando existe, el reason.
Enviarla
Una vez aprobada, el envío es el de siempre:
await met.contacts.sendTemplate(contactId, {
name: 'recordatorio_cita',
language: 'es',
namespace: process.env.WA_TEMPLATE_NAMESPACE!,
params: { '1': 'Ana', '2': 'martes a las 3' },
});
Borrarla
await met.channels.deleteTemplate(whatsapp.id, { name: 'recordatorio_cita' });
Deja de existir para todos los números de esa cuenta.
Y de paso: los atajos
Los atajos —las respuestas guardadas que tu equipo inserta en el chat— también están en la API, y no tienen nada que ver con Meta: son texto tuyo, dentro del workspace.
const atajos = await met.conversations.quickReplies();
await met.conversations.createQuickReply({ title: 'Horario', content: 'Atendemos de 8 a 6.' });
Leer pide conversations:read; crear, editar y borrar piden conversations:write.
Y después
- Un agente que contesta WhatsApp — la ventana de 24 horas y el traspaso a una persona.
- Eventos — el registro guarda 30 días, los escuche alguien o no.