Actualizado el 25/10/2026
Para enviar un mensaje con la API oficial de WhatsApp, haz un POST a https://graph.facebook.com/v26.0/<PHONE_NUMBER_ID>/messages con el token en el encabezado Authorization y un JSON con messaging_product, to, type y el contenido. El texto libre solo se puede enviar dentro de la ventana de 24 horas que abre el cliente; fuera de ella, necesitas una plantilla aprobada. La respuesta 200 indica que Meta aceptó la solicitud, y la entrega se confirma después, por webhook.
A continuación tienes ejemplos para copiar de texto y plantilla en cURL, Node.js y Python, la misma llamada con Vectalk y una tabla con los errores que más aparecen en el primer envío.
Qué necesitas antes
- El Phone Number ID del número que va a enviar. No es el número en sí; es el ID que Meta genera para él. Mira el glosario de la API.
- Un token de acceso con el permiso
whatsapp_business_messaging. Para producción, usa un token de usuario del sistema, como en token permanente. - El número del destinatario con
+y código de país, por ejemplo+5215512345678. - Una plantilla aprobada, si el cliente no escribió en las últimas 24 horas.
Sobre el formato del número: la documentación de envío recomienda incluir siempre el + y el código de país. Sin el +, Meta antepone el código de país de tu número comercial, lo que puede mandar el mensaje a la persona equivocada. La misma página aclara que, para Brasil y México, la API puede ajustar el prefijo del número, y que eso es el comportamiento esperado.
Estructura del payload
Todos los envíos usan el mismo endpoint. Lo que cambia es el type y el objeto con el mismo nombre:
| Campo | Obligatorio | Valor |
|---|---|---|
messaging_product | Sí | Siempre "whatsapp" |
recipient_type | No | "individual" (por defecto) o "group" |
to | Sí | Número del destinatario, de preferencia con + |
type | Sí | text, template, image, document, interactive, etc. |
<type> | Sí | Objeto con el contenido, por ejemplo text: { body } |
context.message_id | No | wamid de un mensaje del cliente, para responder citándolo |
Enviar texto
Un mensaje de texto es un mensaje de servicio: solo funciona con la ventana de atención abierta. El cuerpo acepta hasta 4.096 caracteres, y preview_url: true le pide a WhatsApp que muestre la vista previa del primer enlace (text messages).
cURL
curl 'https://graph.facebook.com/v26.0/TU_PHONE_NUMBER_ID/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer TU_TOKEN' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "+5215512345678",
"type": "text",
"text": {
"preview_url": true,
"body": "Tu pedido fue enviado. Síguelo en https://ejemplo.com/seguimiento/123"
}
}'
Node.js (18 o superior)
const PHONE_NUMBER_ID = process.env.WA_PHONE_NUMBER_ID;
const TOKEN = process.env.WA_TOKEN;
async function enviarTexto(para, texto) {
const r = await fetch(`https://graph.facebook.com/v26.0/${PHONE_NUMBER_ID}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
messaging_product: 'whatsapp',
to: para,
type: 'text',
text: { body: texto },
}),
});
const cuerpo = await r.json();
if (!r.ok) throw new Error(`${cuerpo.error?.code}: ${cuerpo.error?.message}`);
return cuerpo.messages[0].id; // wamid
}
enviarTexto('+5215512345678', '¡Hola! Recibimos tu mensaje.').then(console.log);
Python
import os
import requests
PHONE_NUMBER_ID = os.environ["WA_PHONE_NUMBER_ID"]
TOKEN = os.environ["WA_TOKEN"]
def enviar_texto(para: str, texto: str) -> str:
r = requests.post(
f"https://graph.facebook.com/v26.0/{PHONE_NUMBER_ID}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"messaging_product": "whatsapp",
"to": para,
"type": "text",
"text": {"body": texto},
},
timeout=15,
)
cuerpo = r.json()
if not r.ok:
raise RuntimeError(f"{cuerpo['error']['code']}: {cuerpo['error']['message']}")
return cuerpo["messages"][0]["id"] # wamid
print(enviar_texto("+5215512345678", "¡Hola! Recibimos tu mensaje."))
Enviar una plantilla
La plantilla es lo que permite iniciar la conversación. Debe tener estado APPROVED, y tienes que indicar el nombre y el código de idioma exactos con los que se creó. Su categoría (marketing, utilidad o autenticación) se definió al crearla y determina el precio; el tema está en categorías de plantilla.
Supón una plantilla de utilidad confirmacion_pedido con el cuerpo «¡Hola, {{1}}! Tu pedido {{2}} fue confirmado.»:
cURL
curl 'https://graph.facebook.com/v26.0/TU_PHONE_NUMBER_ID/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer TU_TOKEN' \
-d '{
"messaging_product": "whatsapp",
"to": "+5215512345678",
"type": "template",
"template": {
"name": "confirmacion_pedido",
"language": { "code": "es_MX" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "María" },
{ "type": "text", "text": "#12345" }
]
}
]
}
}'
Node.js
async function enviarPlantilla(para, nombre, variables, idioma = 'es_MX') {
const r = await fetch(`https://graph.facebook.com/v26.0/${PHONE_NUMBER_ID}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
messaging_product: 'whatsapp',
to: para,
type: 'template',
template: {
name: nombre,
language: { code: idioma },
components: [
{ type: 'body', parameters: variables.map((v) => ({ type: 'text', text: v })) },
],
},
}),
});
const cuerpo = await r.json();
if (!r.ok) throw new Error(`${cuerpo.error?.code}: ${cuerpo.error?.message}`);
return cuerpo.messages[0].id;
}
enviarPlantilla('+5215512345678', 'confirmacion_pedido', ['María', '#12345']).then(console.log);
Python
def enviar_plantilla(para: str, nombre: str, variables: list[str], idioma: str = "es_MX") -> str:
r = requests.post(
f"https://graph.facebook.com/v26.0/{PHONE_NUMBER_ID}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"messaging_product": "whatsapp",
"to": para,
"type": "template",
"template": {
"name": nombre,
"language": {"code": idioma},
"components": [
{"type": "body", "parameters": [{"type": "text", "text": v} for v in variables]}
],
},
},
timeout=15,
)
cuerpo = r.json()
if not r.ok:
raise RuntimeError(f"{cuerpo['error']['code']}: {cuerpo['error']['message']}")
return cuerpo["messages"][0]["id"]
print(enviar_plantilla("+5215512345678", "confirmacion_pedido", ["María", "#12345"]))
Si la plantilla se creó con parámetros con nombre ({{nombre_cliente}}), cada parámetro lleva también parameter_name, y el orden deja de importar (templates):
{ "type": "text", "parameter_name": "nombre_cliente", "text": "María" }
Para plantillas con imagen, documento o video en el encabezado, agrega un componente header con el archivo. El envío de multimedia está en cómo enviar imágenes, PDF, audio y video.
Qué devuelve la API
Cuando Meta acepta la solicitud, la respuesta es esta:
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "+5215512345678", "wa_id": "5215512345678" }],
"messages": [{ "id": "wamid.HBgNNTIxNTUxMjM0NTY3OBUCABEYEg..." }]
}
Tres cuidados:
- Guarda el
wamid. Es el que aparece en los webhooks de estadosent,delivered,readyfailed. Mira estados de mensaje por webhook. - 200 no es entrega. Errores como el límite de marketing por usuario llegan después, en el webhook, con estado
failed. - El orden no está garantizado. Si envías varios mensajes seguidos, Meta no garantiza que lleguen en el orden de las solicitudes. Para asegurar la secuencia, espera el estado
deliveredde uno antes de enviar el siguiente.
Meta intenta entregar el mensaje durante hasta 30 días (10 minutos para plantillas de autenticación). Si el estado delivered no llega en ese plazo, considera el mensaje descartado.
Errores más comunes en el primer envío
| Código | Qué significa | Cómo resolverlo |
|---|---|---|
131047 | Pasaron más de 24 horas desde el último mensaje del cliente | Envía una plantilla |
132001 | La plantilla no existe en ese idioma o no está aprobada | Revisa nombre, código de idioma y estado |
132000 | La cantidad de variables no coincide con la plantilla | Envía un valor por cada variable |
132012 | Variables con formato incorrecto | Usa el formato de la plantilla (posicional o con nombre) |
131026 | No se pudo entregar (número sin WhatsApp, versión antigua, términos no aceptados) | Confirma el número con el cliente |
130429 | Se alcanzó el límite de mensajes por segundo | Baja el ritmo y reintenta con espera creciente |
131056 | Demasiados mensajes al mismo destinatario en poco tiempo | Espera antes de volver a escribir a ese número |
133010 | Número comercial no registrado en la plataforma | Registra el número |
190 | Token vencido | Genera un nuevo token de usuario del sistema |
Fuente: códigos de error de Meta. La lista completa, con causas y soluciones, está en errores de la API de WhatsApp.
Buenas prácticas para el código de envío
Los ejemplos de arriba son lo mínimo para que el mensaje salga. En producción, algunos cuidados evitan mensajes duplicados, costos innecesarios y problemas de calidad:
- Registra cada envío antes de llamar a la API. Guarda la solicitud con un ID interno y, tras la respuesta, el
wamid. Si el proceso se cae a mitad de camino, sabes qué ya salió y no lo reenvías. - Reintenta solo lo que vale la pena. Reintenta con espera creciente en
130429,131056y fallas de red o HTTP 5xx. Los errores de parámetros (132000,132012) y de plantilla (132001) no mejoran al reintentar: corrige el payload. - Respeta el límite de marketing por usuario. Meta indica esperar al menos 24 horas antes de reenviar una plantilla que volvió con
131049. - Define un timeout en las llamadas. Una solicitud sin timeout puede trabar toda la cola si la red falla.
- No expongas el token. Léelo de una variable de entorno o de un gestor de secretos y nunca lo pongas en el front-end ni en el repositorio.
- Valida el número antes. Normaliza los números a formato internacional con
+al registrarlos, no al momento del envío. - Marca como leído. Al procesar el mensaje del cliente, puedes marcarlo como leído por la API, lo que le muestra los dos tildes azules.
El mismo envío con Vectalk
Vectalk es un proxy de la WhatsApp Cloud API oficial. Los payloads son idénticos a los de Meta; solo cambian la URL base y el token, según la introducción de la documentación.
| Meta (directo) | Vectalk | |
|---|---|---|
| URL de envío | https://graph.facebook.com/v26.0/<PHONE_NUMBER_ID>/messages | https://apis.vectalk.com.br/api/messages |
| Token | Token de Meta (usuario del sistema) | Token generado en Vectalk |
| Phone Number ID | En la URL | Lo resuelve el token de instancia, o va en el cuerpo con token de organización |
| Payload y respuesta | Formato de Meta | Iguales |
Con un token de instancia, el número se resuelve automáticamente:
curl -X POST https://apis.vectalk.com.br/api/messages \
-H "Authorization: Bearer TU_TOKEN_VECTALK" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "5215512345678",
"type": "template",
"template": {
"name": "confirmacion_pedido",
"language": { "code": "es_MX" },
"components": [
{ "type": "body", "parameters": [
{ "type": "text", "text": "María" },
{ "type": "text", "text": "#12345" }
] }
]
}
}'
Con un token de organización, que accede a todas tus instancias, incluye el phone_number_id en el cuerpo:
import os
import requests
r = requests.post(
"https://apis.vectalk.com.br/api/messages",
headers={"Authorization": f"Bearer {os.environ['VECTALK_TOKEN']}"},
json={
"messaging_product": "whatsapp",
"phone_number_id": "1234567890",
"to": "5215512345678",
"type": "text",
"text": {"body": "¡Hola! Recibimos tu mensaje."},
},
timeout=15,
)
print(r.json()["messages"][0]["id"])
Cada tipo de mensaje está documentado en Mensajes, y los tipos de token en Autenticación. Para generar tu token, crea tu cuenta.
Preguntas frecuentes
¿Puedo enviar texto a alguien que nunca me escribió?
No. Fuera de la ventana de 24 horas, incluso para quien nunca escribió, solo se pueden enviar plantillas aprobadas. La API devuelve el error 131047 si intentas mandar texto libre. Y, según la política de WhatsApp, la persona debe haber dado su opt-in.
La API respondió 200, pero el mensaje no llegó. ¿Por qué?
El 200 solo confirma que Meta aceptó la solicitud. La entrega se informa en el webhook de estado. Si llega failed, el código de error explica el motivo, como 131049 (límite de marketing por usuario) o 131026 (número sin WhatsApp).
¿Qué versión de Graph API debo usar?
Los ejemplos de la documentación actual de Meta usan la v26.0. Fija la versión en la URL y revisa el changelog de Meta antes de actualizar, porque cada versión tiene fecha de baja.
¿Cómo envío a muchos contactos a la vez?
No existe un endpoint de envío por lotes: cada mensaje es una llamada. Controla el ritmo, respeta el límite de mensajes del portafolio y maneja el error 130429.
¿Cómo respondo citando el mensaje del cliente?
Incluye "context": { "message_id": "<wamid del mensaje del cliente>" } en el payload. La respuesta aparece en WhatsApp como una cita del mensaje original.
Fuentes oficiales consultadas el 25/10/2026: Send messages (service messages), Text messages, Templates, Error codes, Status webhook, Access tokens, documentación de Vectalk: introducción, mensajes y autenticación.
