DevOps & Development 10 min de lectura 5 de junio de 2026

Envío de mensajes con la API de WhatsApp Cloud y Python

Cómo automatizar notificaciones de WhatsApp correctamente y evitar errores comunes de API.

Solución en "Dos Clics" (TL;DR)

Conversación sobre cómo enviar mensajes de WhatsApp usando la API de Meta Graph con Python, y la importancia de usar plantillas pre-aprobadas para la entrega de mensajes.

En DOSCLIC, nos enfrentamos al reto de automatizar el envío de mensajes de WhatsApp a nuestros usuarios utilizando la API de Meta Graph. Lo que comenzó como una simple adaptación de un comando curl a Python para enviar mensajes de texto, se convirtió en una valiosa lección sobre las políticas de entrega de Meta y la crucial necesidad de utilizar plantillas pre-aprobadas para garantizar que nuestros mensajes lleguen a su destino.

1. El Desafío Inicial: De curl a Python con Mensajes Largos

Nuestro punto de partida fue un comando curl básico, diseñado para enviar un mensaje de plantilla predefinido como el famoso "hello_world". Sin embargo, nuestra necesidad real era enviar un mensaje mucho más extenso y personalizado, dividido inicialmente en tres partes, a varios números de contacto.

Terminal (Ejemplo curl)
curl -i -X POST \
  https://graph.facebook.com/v25.0/[ID_DEL_NUMERO_DE_TELEFONO]/messages \
  -H 'Authorization: Bearer [TU_TOKEN_DE_ACCESO]' \
  -H 'Content-Type: application/json' \
  -d '{ "messaging_product": "whatsapp", "to": "[número de destinatario]", "type": "template", "template": { "name": "hello_world", "language": { "code": "en_US" } } }'

Adaptando el Envío a Python: Múltiples Mensajes

El primer paso fue adaptar este enfoque a Python, utilizando la librería requests. La idea inicial era enviar el contenido de nuestro mensaje en tres partes separadas, iterando tanto por los destinatarios como por cada segmento del mensaje. Para ello, cambiamos el type: template a type: text, asumiendo que podríamos enviar texto libre.

Python (send_multiple_messages.py)
import requests
import time

TOKEN = "[TU_TOKEN_DE_ACCESO]"
PHONE_NUMBER_ID = "[ID_DEL_NUMERO_DE_TELEFONO]"
URL = f"https://graph.facebook.com/v25.0/{PHONE_NUMBER_ID}/messages"

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json"
}

numeros = ["[número de destinatario 1]", "[número de destinatario 2]"]
mensajes = [
    "Buenos días",
    "Le saluda un representante de DOSCLIC. Nos comunicamos con usted para agradecerle su interés y confirmar su inscripción en el programa de aprendizaje colaborativo \"Diagnóstico y Tratamiento [Nombre del Programa]\". Para mantenerle al tanto de los accesos a las sesiones virtuales y compartirle material de apoyo, utilizaremos una lista de difusión de WhatsApp. Para asegurarse de recibir correctamente todas nuestras actualizaciones, le solicitamos cordialmente que guarde este número en sus contactos.",
    "Aprovechamos para recordarle que su participación inicial es integrante de la sesión; sin embargo, la metodología del Proyecto ECHO se enriquece con la práctica real. Por ello, si usted tiene algún caso clínico actual en su consulta que desee exponer para analizarlo en conjunto con el especialista, háganoslo saber para coordinar su espacio en la sesión. Le hemos inscrito en la plataforma, le llegará un correo a su bandeja. El proceso de inscripción es sencillo, le llevará menos de 2 minutos, pero ante cualquier duda, estamos a la orden."
]

def enviar_mensaje(numero, texto):
    payload = {
        "messaging_product": "whatsapp",
        "to": numero,
        "type": "text",
        "text": {
            "body": texto
        }
    }
    response = requests.post(URL, headers=headers, json=payload)
    print(numero, response.status_code, response.text)

for numero in numeros:
    for msg in mensajes:
        enviar_mensaje(numero, msg)
        time.sleep(1) # Pequeño delay para evitar ser marcado como spam
Consejo Práctico: Para evitar que los mensajes sean percibidos como spam por WhatsApp, es buena práctica añadir un pequeño retardo (por ejemplo, time.sleep(1)) entre cada envío, especialmente cuando se trata de múltiples mensajes a un mismo destinatario o a varios en un corto periodo.

Consolidando el Mensaje: Un Solo Envío por Destinatario

Rápidamente surgió la pregunta de si era posible enviar todo el contenido como un único mensaje, en lugar de tres. La respuesta fue afirmativa: si el texto completo se consolidaba en una sola cadena, el bucle interno para los mensajes se volvía innecesario, y solo se requeriría un envío por cada número de destinatario.

Python (send_single_message.py)
import requests

TOKEN = "[TU_TOKEN_DE_ACCESO]"
PHONE_NUMBER_ID = "[ID_DEL_NUMERO_DE_TELEFONO]"
URL = f"https://graph.facebook.com/v25.0/{PHONE_NUMBER_ID}/messages"

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json"
}

mensaje = """Buenos días.

Le saluda un representante de DOSCLIC. Nos comunicamos con usted para agradecerle su interés y confirmar su inscripción en el programa de aprendizaje colaborativo "Diagnóstico y Tratamiento [Nombre del Programa]".

Para mantenerle al tanto de los accesos a las sesiones virtuales y compartirle material de apoyo, utilizaremos una lista de difusión de WhatsApp. Para asegurarse de recibir correctamente todas nuestras actualizaciones, le solicitamos cordialmente que guarde este número en sus contactos.

Aprovechamos para recordarle que su participación inicial es integrante de la sesión; sin embargo, la metodología del Proyecto ECHO se enriquece con la práctica real. Por ello, si usted tiene algún caso clínico actual en su consulta que desee exponer para analizarlo en conjunto con el especialista, háganoslo saber para coordinar su espacio en la sesión.

Le hemos inscrito en la plataforma; le llegará un correo a su bandeja.

El proceso de inscripción es sencillo, le llevará menos de 2 minutos, pero ante cualquier duda, estamos a la orden."""
numeros = ["[número de destinatario 1]", "[número de destinatario 2]"]

def enviar_mensaje(numero):
    payload = {
        "messaging_product": "whatsapp",
        "to": numero,
        "type": "text",
        "text": {
            "body": mensaje
        }
    }
    response = requests.post(URL, headers=headers, json=payload)
    print(numero, response.status_code, response.text)

for numero in numeros:
    enviar_mensaje(numero)

2. El Misterio del 200 OK sin Entrega

Al ejecutar el script de Python, obtuvimos una respuesta aparentemente exitosa:

Terminal (Salida del script)
C:\utilidades\piton\whatsapp-messages>py send.py
C:\Users\TIC\AppData\Local\Programs\Python\Python313\Lib\site-packages\requests\__init__.py:113: RequestsDependencyWarning: urllib3 (2.5.0) or chardet (7.2.0)/charset_normalizer (3.4.3) doesn't match a supported version!
  warnings.warn(
[número de destinatario] 200 {"messaging_product":"whatsapp","contacts":[{"input":"[número de destinatario]","wa_id":"[número de destinatario]"}],"messages":[{"id":"wamid.HBgLNTA1ODY2OTUzMzAVAgARGBI2QzVBODFFMTZEQTQwODEwNzIA"}]}

El código de estado 200 OK y la estructura de la respuesta JSON indicaban que la API de Meta había aceptado nuestra solicitud. El número estaba en formato internacional correcto y el mensaje había sido enviado al sistema de WhatsApp. Sin embargo, el mensaje nunca llegó a los teléfonos de los destinatarios.

Error detectado: Un código de estado 200 OK de la API de Meta Graph NO garantiza que el mensaje haya sido entregado al teléfono del usuario. Solo confirma que Meta recibió y procesó la solicitud de envío.

Razones Comunes para la No Entrega

Investigando, descubrimos varias razones por las que un mensaje podría ser aceptado por la API pero no entregado:

  • Modo de prueba de WhatsApp Cloud API: Si el número de destinatario no está registrado como "test recipient" en Meta Developers o no está verificado en la aplicación, los mensajes pueden ser aceptados pero no entregados.
  • Número no registrado correctamente: Aunque el formato internacional sea correcto y WhatsApp esté activo, el número debe estar permitido en la configuración de la aplicación (usuarios de prueba o modo de producción).
  • Token en modo de desarrollo: Si la aplicación no está en producción, solo ciertos números configurados específicamente pueden recibir mensajes. Otros números pueden "aceptarse" pero no llegar.
  • Fuera de la ventana de conversación de 24 horas: Aunque menos probable para un mensaje inicial, WhatsApp impone una ventana de 24 horas para mensajes de texto libre iniciados por el negocio. Fuera de esta ventana, solo se permiten mensajes de plantilla.

Diagnóstico y Verificación en Meta Developers

Para verificar el estado real de la entrega, se recomienda:

  1. Revisar los Logs en Meta Developers: Navegar a Meta Developers > WhatsApp > Logs para verificar el estado de entrega (Delivery status), posibles errores de envío y el estado del ID del mensaje.
  2. Activar Webhooks: Configurar webhooks para los eventos messages y statuses. Esto permite recibir notificaciones en tiempo real sobre el estado del mensaje (delivered, sent, failed, read).
  3. Probar con un mensaje de plantilla: Si se quiere validar que la configuración básica funciona, enviar un mensaje de plantilla como hello_world es el método más confiable. Si este llega, el problema radica en el uso de mensajes de texto libre.
Consejo Práctico: La advertencia RequestsDependencyWarning: urllib3 ... doesn't match a supported version! que aparece en la consola de Python no afecta el envío de mensajes. Se refiere a dependencias desalineadas y puede ignorarse o resolverse actualizando los paquetes con pip install -U requests urllib3 charset-normalizer chardet.

3. El Punto de Quiebre: La Necesidad de Plantillas Aprobadas

La clave de la solución residía en entender que, para iniciar una conversación o enviar mensajes fuera de la ventana de 24 horas, la API de WhatsApp Cloud exige el uso de plantillas de mensaje pre-aprobadas por Meta. Nuestro intento de enviar texto libre, aunque técnicamente aceptado por la API, no cumplía con esta política de entrega.

Procedimos a crear una plantilla de mensaje en la plataforma de Meta Developers, que requiere definir campos como Header, Body, Footer y Botones.

Entendiendo las Cuentas de WhatsApp Business (WABA) Duplicadas

Durante la configuración, notamos que aparecían dos cuentas de WhatsApp Business (WABA) con el mismo nombre visible ("DOSCLIC"), pero con IDs diferentes (por ejemplo, [ID_WABA_1] y [ID_WABA_2]). Esto es normal en Meta; se pueden tener múltiples WABA con el mismo nombre, pero solo una estará conectada a nuestro número de teléfono API (Phone Number ID) en un momento dado. Es crucial asegurarse de que se está utilizando la WABA correcta en la configuración de la aplicación para evitar envíos fallidos o desde un entorno de pruebas no deseado.

Diseñando la Plantilla de Mensaje para Aprobación

Para la plantilla, utilizamos el texto completo que queríamos enviar, optimizándolo ligeramente para la aprobación de Meta:

  • Header (Opcional): No es obligatorio para nuestro caso. Si se usara, sería un texto corto como "Confirmación de inscripción DOSCLIC".
  • Body (Obligatorio y Crítico): Aquí se colocó el mensaje principal. Es importante que el texto sea estable y no "dinámico libre" para facilitar la aprobación.
Contenido del Body de la Plantilla
Buenos días.
Le saluda un representante de DOSCLIC. Nos comunicamos con usted para agradecerle su interés y confirmar su inscripción en el programa de aprendizaje colaborativo "Diagnóstico y Tratamiento [Nombre del Programa]".
Para mantenerle al tanto de los accesos a las sesiones virtuales y compartirle material de apoyo, utilizaremos una lista de difusión de WhatsApp. Para asegurarse de recibir correctamente todas nuestras actualizaciones, le solicitamos cordialmente que guarde este número en sus contactos.
Aprovechamos para recordarle que su participación inicial es integrante de la sesión; sin embargo, la metodología del Proyecto ECHO se enriquece con la práctica real. Si usted tiene algún caso clínico actual en su consulta que desee exponer, puede informárnoslo para coordinar su espacio con el especialista.
Le hemos inscrito en la plataforma; recibirá un correo en su bandeja de entrada.
El proceso de inscripción es sencillo, le llevará menos de 2 minutos. Ante cualquier duda, estamos a la orden.
  • Footer (Opcional): Un texto corto como "DOSCLIC - Educación Continua".
  • Botones (Opcional): Configuramos un botón de "Visitar sitio web" con el texto "DOSCLIC" y la URL https://www.dosclic.com/, lo cual era correcto.
Advertencia: Las plantillas de mensaje son obligatorias cuando se inicia una conversación con un usuario o cuando se intenta enviar un mensaje fuera de la ventana de conversación de 24 horas. Ignorar esta regla es la causa más común de no entrega de mensajes.

4. La Solución Final: Implementando la Plantilla Aprobada

Una vez que la plantilla fue creada y aprobada por Meta (un proceso que puede tomar desde minutos hasta varias horas), el paso final fue modificar nuestro script de Python para utilizar esta plantilla en lugar de texto libre. Esto implicó cambiar el type del payload a template y especificar el name de la plantilla y el language code.

Python (Uso de plantilla)
import requests

TOKEN = "[TU_TOKEN_DE_ACCESO]"
PHONE_NUMBER_ID = "[ID_DEL_NUMERO_DE_TELEFONO]"
URL = f"https://graph.facebook.com/v25.0/{PHONE_NUMBER_ID}/messages"

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json"
}

numeros = ["[número de destinatario 1]", "[número de destinatario 2]"]
TEMPLATE_NAME = "nombre_de_mi_plantilla_aprobada" # Reemplaza con el nombre real de tu plantilla

def enviar_plantilla(numero):
    payload = {
        "messaging_product": "whatsapp",
        "to": numero,
        "type": "template",
        "template": {
            "name": TEMPLATE_NAME,
            "language": { "code": "es" } # O "es_MX", "en_US", etc.
        }
    }
    response = requests.post(URL, headers=headers, json=payload)
    print(numero, response.status_code, response.text)

for numero in numeros:
    enviar_plantilla(numero)
La historia detrás de la nota

Esta experiencia nos recordó que las APIs de plataformas grandes como Meta tienen sus propias reglas de negocio y políticas de uso. No basta con que el código funcione; es esencial comprender el ecosistema para asegurar la entrega y evitar frustraciones. Una buena lectura de la documentación y la paciencia en el proceso de aprobación son clave.