Enviar una plantilla
Guía paso a paso para mandar una plantilla de WhatsApp aprobada a un cliente vía la API. Las plantillas son el único formato que podés enviar a un contacto fuera de la ventana de 24 horas — son obligatorias para iniciar conversaciones desde tu negocio.
Obtené tu API key y orgId
Andá a Configuración → Integraciones → API Keys en tu dashboard, creá una nueva y copiá el token. Tiene formato mk_live_<prefix>.<secret> y se muestra una sola vez.
En la misma pantalla está tu orgId (UUID), que vas a usar en todas las URLs como /organizations/{orgId}/....
Toda la API se autentica con el header X-Api-Key. Más detalles en Autenticación.
Listá plantillas aprobadas
Solo podés enviar plantillas en estado APPROVED. Las que estén PENDING o REJECTED en Meta van a fallar al enviar.
curl 'https://api.mosend.dev/organizations/{orgId}/templates' \
-H 'X-Api-Key: mk_live_<prefix>.<secret>'
# Respuesta (resumida)
# {
# "data": [
# {
# "id": "9c5a1e8e-...",
# "name": "bienvenida_v1",
# "language": "es_CO",
# "status": "APPROVED",
# "category": "MARKETING",
# "components": [
# { "type": "BODY", "text": "Hola {{1}}, te damos la bienvenida.", "example": { "body_text": [["Juan"]] } }
# ]
# }
# ]
# }Anotá el name y el language — los necesitás en el siguiente paso. Las variables ({{1}}, {{2}}, etc.) se rellenan al enviar.
Identificá el número y el destinatario
phoneNumberId es el UUID interno del número de WhatsApp desde el que vas a enviar (no el número telefónico). Listalos con GET /organizations/{orgId}/phone-numbers.
to es el WhatsApp del destinatario en formato E.164 sin el + (ej. 573001234567).
Enviá la plantilla
Hacé POST /organizations/{orgId}/messages con type: "template". Tenés dos modos:
payload Meta-passthrough completo con name / language / components / parameters. Útil si ya tenés código que arma el shape oficial de WhatsApp Cloud API.# Variables — reemplazá por las tuyas
ORG_ID="<tu-org-uuid>"
API_KEY="mk_live_<prefix>.<secret>"
PHONE_ID="<phone_number_id-uuid>"
TEMPLATE_ID="<uuid-de-la-plantilla>" # del dashboard, columna "ID"
# MODO SIMPLE — body posicional
curl -X POST "https://api.mosend.dev/organizations/${ORG_ID}/messages" \
-H "X-Api-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "'"${PHONE_ID}"'",
"to": "573001234567",
"type": "template",
"templateId": "'"${TEMPLATE_ID}"'",
"variables": ["Juan", "FAC-2026-0042"]
}'
# MODO SIMPLE — header media + botón URL dinámico
curl -X POST "https://api.mosend.dev/organizations/${ORG_ID}/messages" \
-H "X-Api-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "'"${PHONE_ID}"'",
"to": "573001234567",
"type": "template",
"templateId": "'"${TEMPLATE_ID}"'",
"variables": {
"body": ["Juan", "FAC-2026-0042"],
"header": { "type": "image", "link": "https://clientes.tu-empresa.com/factura.png" },
"buttons": [{ "index": 0, "value": "456789" }]
}
}'
# MODO SIMPLE — plantilla OTP con botón COPY_CODE
# (categoría AUTHENTICATION; el body suele tener un {{1}} y el botón "Copiar
# código" requiere el valor a copiar como buttons[index=0].value)
curl -X POST "https://api.mosend.dev/organizations/${ORG_ID}/messages" \
-H "X-Api-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "'"${PHONE_ID}"'",
"to": "573001234567",
"type": "template",
"templateId": "'"${TEMPLATE_ID}"'",
"variables": {
"body": ["847291"],
"buttons": [{ "index": 0, "value": "847291" }]
}
}'
# Si omitís el button, Meta rechaza con #131008 ("falta info requerida")
# porque el botón COPY_CODE siempre necesita su valor.
# Si preferís identificar por nombre + idioma en vez del id:
# (templateName + templateLanguage en lugar de templateId)
# "templateName": "factura_lista",
# "templateLanguage": "es_CO",
# Respuesta esperada (200)
# {
# "data": {
# "id": "msg-uuid", # Message id en Mosend
# "metaMessageId": "wamid....", # id devuelto por Meta
# "status": "sent",
# "conversationId": "conv-uuid"
# },
# "timestamp": "..."
# }Sin variables: variables: [] (array vacío) o omitir el campo. Si la plantilla tiene {{N}} en body y le faltan valores, el backend responde 400 con mensaje claro.
Modo experto: Meta-passthrough
Si ya tenés código que arma el shape exacto de WhatsApp Cloud API, podés seguir mandando payload tal cual:
{
"phoneNumberId": "...",
"to": "573001234567",
"type": "template",
"payload": {
"name": "factura_lista",
"language": { "code": "es_CO" },
"components": [
{ "type": "body",
"parameters": [{ "type": "text", "text": "Juan" }] },
{ "type": "button", "sub_type": "url", "index": "0",
"parameters": [{ "type": "text", "text": "456789" }] }
]
}
}No mandes payload y templateId al mismo tiempo — el backend tira 400. Elegí uno.
Resolver plantilla por nombre en vez de id
Si preferís identificar la plantilla por el nombre amigable (como aparece en Meta), usá templateName y opcionalmente templateLanguage:
{
"phoneNumberId": "...",
"to": "573001234567",
"type": "template",
"templateName": "factura_lista",
"templateLanguage": "es_CO",
"variables": ["Juan", "FAC-2026-0042"]
}Si tu WABA tiene varias plantillas con el mismo nombre en distintos idiomas, templateLanguage es necesario. Si no lo pasás, Mosend toma la más reciente con ese nombre en la WABA del phoneNumberId.
Recibí actualizaciones del estado
Cuando Meta entrega el mensaje, Mosend recibe webhooks de status (sent → delivered → read). Para que tu sistema se entere, registrá un webhook outbound en /api/webhooks-outbound suscrito al evento message.status. Detalles del payload firmado con HMAC en webhooks salientes.
Errores comunes
- 401 Unauthorized: el header no es
X-Api-Key(no usesAuthorization: Bearerpara API keys — eso es para JWT del dashboard), o el token está mal formado. - 403 Forbiddencon "API key sin scope": tu key tiene scopes restringidos y no incluyen
messages:send. O creás la key conscopes: [](acceso total) o le agregás el scope. - 400 Meta error 132012/132068: la plantilla está en
PENDINGoREJECTED— solo se pueden enviar lasAPPROVED. - 400 Meta error 131056: el destinatario nunca opt-in, o estás fuera de la ventana de 24h con una plantilla MARKETING. Las plantillas UTILITY siempre se pueden enviar.
- Parámetros incompletos: la cantidad de
parametersen cada component tiene que matchear las variables de la plantilla aprobada (1 por{{1}}, 2 por{{2}}, etc.).