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 puedes enviar a un contacto fuera de la ventana de 24 horas — son obligatorias para iniciar conversaciones desde tu negocio.

¿Necesitas enviar a varios destinatarios (lista, contactos por etiqueta)? Mira la guía de broadcasts →
1

Obtén tu API key y orgId

Ve a Configuración → Integraciones → API Keys en tu dashboard, crea una nueva y copia 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.

2

Lista plantillas aprobadas

Solo puedes enviar plantillas en estado APPROVED. Las que estén PENDING o REJECTED en Meta van a fallar al enviar.

GET /organizations/{orgId}/templates
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"]] } }
#       ]
#     }
#   ]
# }

Anota el name y el language — los necesitas en el siguiente paso. Las variables ({{1}}, {{2}}, etc.) se rellenan al enviar.

3

Identifica 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). Lístalos con GET /organizations/{orgId}/phone-numbers.

to es el WhatsApp del destinatario en formato E.164 sin el + (ej. 573001234567).

4

Envía la plantilla

Haz POST /organizations/{orgId}/messages con type: "template". Tienes dos modos:

Modo simple (recomendado) — solo el id de la plantilla en Mosend + los valores de las variables. Mosend arma el payload de Meta internamente.
Modo experto — pasas el payload Meta-passthrough completo con name / language / components / parameters. Útil si ya tienes código que arma el shape oficial de WhatsApp Cloud API.
# Variables — reemplaza 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 omites el button, Meta rechaza con #131008 ("falta info requerida")
# porque el botón COPY_CODE siempre necesita su valor.

# Si prefieres 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 tienes código que arma el shape exacto de WhatsApp Cloud API, puedes 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. Elige uno.

Resolver plantilla por nombre en vez de id

Si prefieres identificar la plantilla por el nombre amigable (como aparece en Meta), usa 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 pasas, Mosend toma la más reciente con ese nombre en la WABA del phoneNumberId.

5

Recibe actualizaciones del estado

Cuando Meta entrega el mensaje, Mosend recibe webhooks de status (sent → delivered → read). Para que tu sistema se entere, registra 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 uses Authorization: Bearer para 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 creas la key con scopes: [] (acceso total) o le agregas el scope.
  • 400 Meta error 132012/132068: la plantilla está en PENDING o REJECTED — solo se pueden enviar las APPROVED.
  • 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 parameters en cada component tiene que matchear las variables de la plantilla aprobada (1 por {{1}}, 2 por {{2}}, etc.).