Tres paneles de código uno al lado del otro mostrando el mismo código de envío de mensaje de WhatsApp Cloud API en Node.js, Python y PHP sobre fondo oscuro de terminal
Secciones: Fundamentos del API · Mensaje de texto · Plantilla con variables · Imagen y documento · Botones interactivos · Ubicación · Envío masivo con rate limiting · Manejo de errores y reintentos · Wrappers de clase completos (Node / Python / PHP)

Fundamentos del API: el único endpoint por el que pasa todo

Cada mensaje saliente de WhatsApp — sin importar el tipo — pasa por un único endpoint HTTP POST. El tipo de mensaje y el contenido están en el body JSON. Eso es todo. Sin WebSockets, sin streaming, sin long-polling.

Tres valores que necesitas antes de que se ejecute cualquier código:

  • Phone Number ID — un ID numérico para tu número de WhatsApp Business (se parece a 123456789012345). Distinto de tu número de teléfono real.
  • Access Token — un token permanente de System User con el permiso whatsapp_business_messaging. Nunca uses un token de usuario temporal en producción.
  • Número de teléfono del destinatario — en formato E.164: código de país + número, sin espacios, sin guiones, sin signo + en el valor JSON (p.ej. 15550001234 para un número de EE.UU., o con + también se acepta: +15550001234).

Un envío exitoso devuelve HTTP 200 con un body como:

Tipos de mensaje: sesión vs plantilla

Antes de elegir un tipo de mensaje, necesitas saber si estás dentro de una ventana de servicio. La ventana de sesión (24 h después de que un cliente te escriba, o 72 h tras un clic en anuncio Click-to-WhatsApp) determina qué tipos de mensaje son gratuitos y sin restricciones.

text
Texto plano. Soporta el formato similar a markdown de WhatsApp. Hasta 4.096 caracteres.
Gratis en ventana
template
Formato pre-aprobado con huecos de variables. Necesario para iniciar contacto fuera de la ventana.
Requiere aprobación
image
JPEG o PNG. Máx 5MB. URL o media ID. Caption opcional.
Gratis en ventana
document
PDF, DOCX, XLSX. Máx 100MB. URL o media ID. Nombre de archivo visible.
Gratis en ventana
video
MP4 o 3GPP. Máx 16MB. URL o media ID. Caption opcional.
Gratis en ventana
interactive
Respuestas con botones (máx 3) o mensajes de lista (máx 10 ítems). El cliente toca para responder.
Gratis en ventana
location
Envía lat/lng con nombre y dirección opcionales. Se renderiza como mapa en WhatsApp.
Gratis en ventana
audio
MP3 o AAC. Máx 16MB. URL o media ID. No soporta caption.
Gratis en ventana

Enviar un mensaje de texto

El envío más simple. Funciona dentro de la ventana de servicio de 24 horas sin cargo de Meta (dentro de la cuota mensual de 1.000 conversaciones de servicio gratuitas). Fuera de la ventana, usa una plantilla en su lugar.

Node.js (native fetch)
sendText.js
async function sendText(to, message) { const url = `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_ID}/messages`; const res = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.WA_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'text', text: { body: message, preview_url: false }, }), }); if (!res.ok) throw new Error(`WhatsApp error: ${await res.text()}`); return (await res.json()).messages[0].id; // wamid — guárdalo para rastrear el estado } // Uso const messageId = await sendText('+15550001234', 'Hello from WhatsApp Cloud API! 👋');
Python (requests)
send_text.py
import os, requests def send_text(to: str, message: str) -> str: url = f"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_ID']}/messages" res = requests.post(url, headers={ "Authorization": f"Bearer {os.environ['WA_TOKEN']}", "Content-Type": "application/json", }, json={ "messaging_product": "whatsapp", "recipient_type": "individual", "to": to, "type": "text", "text": { "body": message, "preview_url": False }, } ) res.raise_for_status() return res.json()["messages"][0]["id"] # Uso message_id = send_text("+15550001234", "Hello from WhatsApp Cloud API! 👋")
PHP (cURL)
send_text.php
function sendText(string $to, string $message): string { $url = "https://graph.facebook.com/v21.0/" . getenv('WA_PHONE_ID') . "/messages"; $body = [ 'messaging_product' => 'whatsapp', 'recipient_type' => 'individual', 'to' => $to, 'type' => 'text', 'text' => ['body' => $message, 'preview_url' => false], ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('WA_TOKEN'), 'Content-Type: application/json', ], ]); $response = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException('WhatsApp error: ' . json_encode($response)); } return $response['messages'][0]['id']; // guárdalo para rastrear el estado } // Uso $messageId = sendText('+15550001234', 'Hello from WhatsApp Cloud API! 👋');

Enviar un mensaje de plantilla con variables

Las plantillas son obligatorias para el primer mensaje de una conversación nueva, o cualquier mensaje enviado fuera de la ventana de servicio de 24 horas. Las plantillas se definen en WhatsApp Manager, las aprueba Meta, y pueden incluir marcadores de variables {{1}} {{2}}. Tu llamada al API rellena esas variables en el momento del envío.

Tiempo de aprobación de plantillas: Las plantillas utility simples (confirmaciones de pedido, actualizaciones de envío) suelen aprobarse en 1–24 horas. Las plantillas de marketing tardan 1–5 días hábiles. Puedes comprobar el estado de aprobación vía la suscripción webhook message_template_status_update o en WhatsApp Manager. Las plantillas en estado rechazado no se pueden enviar — enviar una plantilla no-APPROVED devuelve un error 400.
Node.js
sendTemplate.js
async function sendTemplate(to, templateName, languageCode, variables = []) { const url = `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_ID}/messages`; const body = { messaging_product: 'whatsapp', to, type: 'template', template: { name: templateName, language: { code: languageCode }, // p.ej. 'en', 'pt_BR', 'de' components: variables.length ? [{ type: 'body', parameters: variables.map(v => ({ type: 'text', text: v })), }] : [], }, }; const res = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.WA_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`Template send failed: ${await res.text()}`); return (await res.json()).messages[0].id; } // Ejemplo: plantilla "order_confirmation" con texto del body {{1}}="order #1234" {{2}}="2h" await sendTemplate( '+15550001234', 'order_confirmation', 'en', ['order #1234', '2 hours'] );
Python
send_template.py
def send_template(to: str, template_name: str, language_code: str, variables: list = []) -> str: url = f"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_ID']}/messages" components = [] if variables: components.append({ "type": "body", "parameters": [{ "type": "text", "text": v } for v in variables], }) res = requests.post(url, headers={ "Authorization": f"Bearer {os.environ['WA_TOKEN']}", "Content-Type": "application/json", }, json={ "messaging_product": "whatsapp", "to": to, "type": "template", "template": { "name": template_name, "language": { "code": language_code }, "components": components, }, } ) res.raise_for_status() return res.json()["messages"][0]["id"] # Uso send_template("+15550001234", "order_confirmation", "en", ["order #1234", "2 hours"])
PHP
send_template.php
function sendTemplate(string $to, string $templateName, string $langCode, array $variables = []): string { $url = "https://graph.facebook.com/v21.0/" . getenv('WA_PHONE_ID') . "/messages"; $components = []; if (!empty($variables)) { $components[] = [ 'type' => 'body', 'parameters' => array_map( fn($v) => ['type' => 'text', 'text' => $v], $variables ), ]; } $body = [ 'messaging_product' => 'whatsapp', 'to' => $to, 'type' => 'template', 'template' => [ 'name' => $templateName, 'language' => ['code' => $langCode], 'components' => $components, ], ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('WA_TOKEN'), 'Content-Type: application/json', ], ]); $res = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) throw new RuntimeException(json_encode($res)); return $res['messages'][0]['id']; }

Enviar una imagen o documento

Los mensajes de media referencian un archivo por URL (el archivo debe ser accesible públicamente vía HTTPS) o por Media ID (obtenido tras subir el archivo al endpoint de media de Meta). URL es más simple para la mayoría de casos — el Cloud API lo descarga y lo cachea. Los Media IDs son mejores para activos reutilizados con frecuencia, como imágenes de producto o PDFs con tu marca.

Node.js
sendMedia.js
// Enviar imagen por URL pública async function sendImage(to, imageUrl, caption = '') { return sendMedia(to, 'image', { link: imageUrl, caption }); } // Enviar documento por URL pública async function sendDocument(to, docUrl, filename, caption = '') { return sendMedia(to, 'document', { link: docUrl, filename, caption }); } // Sender genérico de media — sirve para imagen, vídeo, audio, documento async function sendMedia(to, type, mediaObj) { const res = await fetch( `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_ID}/messages`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.WA_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ messaging_product: 'whatsapp', to, type, [type]: mediaObj, // { link, caption } O { id, caption } }), } ); if (!res.ok) throw new Error(`Media send failed: ${await res.text()}`); return (await res.json()).messages[0].id; } // Uso await sendImage('+15550001234', 'https://yourdomain.com/promo.jpg', 'Check out our new product! 🚀'); await sendDocument('+15550001234', 'https://yourdomain.com/invoice.pdf', 'invoice-2026.pdf');
Python
send_media.py
def send_media(to: str, media_type: str, media_obj: dict) -> str: url = f"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_ID']}/messages" res = requests.post(url, headers={ "Authorization": f"Bearer {os.environ['WA_TOKEN']}", "Content-Type": "application/json", }, json={ "messaging_product": "whatsapp", "to": to, "type": media_type, media_type: media_obj, # p.ej. {"link": url, "caption": "..."} } ) res.raise_for_status() return res.json()["messages"][0]["id"] def send_image(to: str, url: str, caption: str = "") -> str: return send_media(to, "image", { "link": url, "caption": caption }) def send_document(to: str, url: str, filename: str, caption: str = "") -> str: return send_media(to, "document", { "link": url, "filename": filename, "caption": caption })
PHP
send_media.php
function sendMedia(string $to, string $type, array $mediaObj): string { $url = "https://graph.facebook.com/v21.0/" . getenv('WA_PHONE_ID') . "/messages"; $body = [ 'messaging_product' => 'whatsapp', 'to' => $to, 'type' => $type, $type => $mediaObj, // ['link' => url, 'caption' => '...'] ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('WA_TOKEN'), 'Content-Type: application/json', ], ]); $res = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) throw new RuntimeException(json_encode($res)); return $res['messages'][0]['id']; } // Uso sendMedia('+15550001234', 'image', ['link' => 'https://yourdomain.com/img.jpg', 'caption' => 'New product!']); sendMedia('+15550001234', 'document', ['link' => 'https://yourdomain.com/invoice.pdf', 'filename' => 'invoice.pdf']);

Enviar un mensaje interactivo con botones

Los mensajes interactivos de botones permiten a los clientes responder con un toque en lugar de escribir. Máximo 3 botones por mensaje. Los títulos de botón están limitados a 20 caracteres. Cuando un cliente toca un botón, tu webhook recibe un mensaje de tipo interactive con el id del botón en msg.interactive.button_reply.id.

Node.js
sendButtons.js
async function sendButtons(to, bodyText, buttons) { // buttons: [{ id: 'btn_yes', label: 'Yes' }, { id: 'btn_no', label: 'No' }] const res = await fetch( `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_ID}/messages`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.WA_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ messaging_product: 'whatsapp', to, type: 'interactive', interactive: { type: 'button', body: { text: bodyText }, action: { buttons: buttons.map(b => ({ type: 'reply', reply: { id: b.id, title: b.label.slice(0, 20) }, // límite 20 caracteres })), }, }, }), } ); if (!res.ok) throw new Error(`Button send failed: ${await res.text()}`); return (await res.json()).messages[0].id; } // Uso await sendButtons( '+15550001234', 'Would you like a callback from our team?', [ { id: 'yes_callback', label: 'Yes, call me!' }, { id: 'no_thanks', label: 'No thanks' }, ] );
Python
send_buttons.py
def send_buttons(to: str, body_text: str, buttons: list) -> str: # buttons: [{"id": "btn_yes", "label": "Yes"}, ...] máx 3 botones url = f"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_ID']}/messages" res = requests.post(url, headers={ "Authorization": f"Bearer {os.environ['WA_TOKEN']}", "Content-Type": "application/json", }, json={ "messaging_product": "whatsapp", "to": to, "type": "interactive", "interactive": { "type": "button", "body": { "text": body_text }, "action": { "buttons": [ { "type": "reply", "reply": { "id": b["id"], "title": b["label"][:20] }} for b in buttons ] }, }, } ) res.raise_for_status() return res.json()["messages"][0]["id"]
PHP
send_buttons.php
function sendButtons(string $to, string $bodyText, array $buttons): string { $url = "https://graph.facebook.com/v21.0/" . getenv('WA_PHONE_ID') . "/messages"; $btns = array_map(fn($b) => [ 'type' => 'reply', 'reply' => ['id' => $b['id'], 'title' => mb_substr($b['label'], 0, 20)], ], $buttons); $body = [ 'messaging_product' => 'whatsapp', 'to' => $to, 'type' => 'interactive', 'interactive' => [ 'type' => 'button', 'body' => ['text' => $bodyText], 'action' => ['buttons' => $btns], ], ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('WA_TOKEN'), 'Content-Type: application/json', ], ]); $res = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) throw new RuntimeException(json_encode($res)); return $res['messages'][0]['id']; }

Enviar una ubicación

Los mensajes de ubicación se renderizan como un pin en un mapa dentro de WhatsApp con un nombre y una dirección opcionales debajo. Los clientes pueden tocar para abrirlo en su app de mapas. No se necesita URL ni media ID — solo coordenadas.

Envío masivo: rate limits y el patrón correcto

La WhatsApp Cloud API permite 80 mensajes por segundo por número de teléfono por defecto. A ese ritmo, enviar 10.000 mensajes tarda unos 2 minutos. Sin rate limiting, un bucle ingenuo agota el límite casi inmediatamente y empiezas a recibir errores 429 a mitad de campaña. El patrón correcto: añadir un delay fijo entre envíos e implementar exponential backoff en los 429.

Node.js — Sender masivo con rate limiting
bulkSend.js
const DELAY_MS = 20; // 20ms entre envíos = 50 msg/s — seguro bajo el límite de 80 msg/s const MAX_RETRIES = 4; async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function sendWithRetry(to, message, attempt = 0) { try { return await sendText(to, message); } catch (err) { const is429 = err.message.includes('429'); if (!is429 || attempt >= MAX_RETRIES) throw err; const waitMs = Math.min(1000 * 2 ** attempt + Math.random() * 500, 60000); console.warn(`Rate limited — waiting ${Math.round(waitMs)}ms (attempt ${attempt + 1})`); await sleep(waitMs); return sendWithRetry(to, message, attempt + 1); } } async function bulkSend(contacts, messageBody) { const results = { sent: 0, failed: [] }; for (const { phone, name } of contacts) { try { const personalised = messageBody.replace('{{name}}', name); await sendWithRetry(phone, personalised); results.sent++; } catch (err) { results.failed.push({ phone, error: err.message }); } await sleep(DELAY_MS); // espaciado para rate limit } console.log(`Sent: ${results.sent}, Failed: ${results.failed.length}`); return results; } // Uso await bulkSend( [{ phone: '+15550001234', name: 'Alice' }, { phone: '+15550005678', name: 'Bob' }], 'Hi {{name}}, your order is ready! 🎉' );

Manejo de errores: qué significa cada status code

HTTP StatusCausa comúnSolución
200 OKMensaje aceptado por Meta — aún no entregadoGuarda el wamid devuelto y escucha los webhooks de estado de entrega
400 Bad RequestJSON inválido, campo obligatorio faltante, plantilla no aprobada, número de variables de plantilla no coincideRevisa el body de la respuesta de error — Meta devuelve un error.message detallado explicando el campo o restricción exacta que falló
401 UnauthorizedToken de acceso inválido o expirado; falta el header AuthorizationRegenera un token permanente de System User. Nunca uses tokens de usuario temporales en producción — expiran tras 60 días
404 Not FoundPhone Number ID equivocado en la URL; número no registrado en Cloud APIVerifica el Phone Number ID en Meta Developer Dashboard → WhatsApp → Phone Numbers. Confirma que el número está registrado y verificado.
429 Too Many RequestsSuperado el límite de 80 msg/s, o el rate limit de llamadas al APIImplementa exponential backoff con jitter. Añade 15–20ms de delay entre envíos en operaciones masivas. No reintentes inmediatamente.
460 (Meta interno)El número del destinatario no está registrado en WhatsAppEl número no es usuario de WhatsApp. Quítalo de tu lista. No es un número entregable.
131026Mensaje no entregable — restricciones en la cuenta de WhatsApp del destinatarioEl destinatario puede haber bloqueado tu número o su cuenta está restringida. Registra y omite.
500 / 503Caída de infraestructura de Meta o error temporalReintenta con exponential backoff. Revisa metastatus.com para incidentes activos. No reintentes más de 3 veces.

Clase de cliente WhatsApp completa (Node.js)

Las funciones anteriores empaquetadas en una clase reutilizable con lógica de retry incorporada, manejo de errores consistente y configuración basada en variables de entorno:

Node.js
WhatsAppClient.js
class WhatsAppClient { constructor({ phoneId = process.env.WA_PHONE_ID, token = process.env.WA_TOKEN, version = 'v21.0', } = {}) { this.baseUrl = `https://graph.facebook.com/${version}/${phoneId}/messages`; this.headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }; } async #send(payload) { const res = await fetch(this.baseUrl, { method: 'POST', headers: this.headers, body: JSON.stringify({ messaging_product: 'whatsapp', ...payload }), }); if (!res.ok) { const err = await res.json(); throw Object.assign(new Error(err?.error?.message ?? 'WhatsApp API error'), { code: err?.error?.code, status: res.status, }); } return (await res.json()).messages[0].id; } text(to, body) { return this.#send({ to, type: 'text', text: { body } }); } image(to, link, caption = '') { return this.#send({ to, type: 'image', image: { link, caption } }); } document(to, link, filename, caption = '') { return this.#send({ to, type: 'document', document: { link, filename, caption } }); } location(to, lat, lng, name = '', address = '') { return this.#send({ to, type: 'location', location: { latitude: lat, longitude: lng, name, address } }); } buttons(to, bodyText, buttons) { return this.#send({ to, type: 'interactive', interactive: { type: 'button', body: { text: bodyText }, action: { buttons: buttons.map(b => ({ type: 'reply', reply: { id: b.id, title: b.label.slice(0, 20) } })) }, }}); } template(to, name, lang, variables = []) { return this.#send({ to, type: 'template', template: { name, language: { code: lang }, components: variables.length ? [{ type: 'body', parameters: variables.map(v => ({ type: 'text', text: v })) }] : [], }}); } } // Uso const wa = new WhatsAppClient(); await wa.text('+15550001234', 'Hello! 👋'); await wa.template('+15550001234', 'order_shipped', 'en', ['#1234']); await wa.buttons('+15550001234', 'Rate your experience:', [{ id: 'great', label: 'Great! 🌟' }, { id: 'poor', label: 'Could be better' }]); module.exports = WhatsAppClient;

La otra mitad: recibir respuestas con SocialHook

Enviar mensajes es solo la mitad del panorama. Cuando tu cliente responde, la WhatsApp Cloud API dispara un webhook a tu servidor — pero necesitas un endpoint HTTPS accesible públicamente para recibirlo, además de verificación de firma HMAC-SHA256, extracción de payload anidado y manejo de reintentos.

SocialHook se encarga de toda la capa de entrada. Conecta tu número de WhatsApp a SocialHook, pega la URL de tu servidor como destino, y cada respuesta de cliente llega como un evento JSON normalizado — verificado, extraído del envoltorio anidado de Meta y reenviado a tu endpoint en menos de 50ms. El mismo formato plano funciona también para Facebook Messenger e Instagram DMs.

La configuración completa: tu servidor usa el WhatsAppClient anterior para los envíos, SocialHook entrega los eventos entrantes a tu handler de webhook. Dos direcciones, una sola tarifa plana de 50 $/mes. Mira la guía completa de webhook de entrada o empieza con el quickstart de 5 minutos.

Preguntas frecuentes

¿Cómo envío un mensaje de WhatsApp usando la Cloud API?
POST a https://graph.facebook.com/v21.0/{PHONE_NUMBER_ID}/messages con tu header Authorization: Bearer {ACCESS_TOKEN} y un body JSON con messaging_product: "whatsapp", to: "+E.164_number", type: "text", y text: { body: "message" }. Código completo para Node.js, Python y PHP está en la sección de mensaje de texto arriba.
¿Qué es el Phone Number ID de WhatsApp y dónde lo encuentro?
El Phone Number ID es un identificador numérico para tu número específico de WhatsApp Business en la Cloud API — es distinto del número de teléfono real. Encuéntralo en Meta Developer Dashboard → WhatsApp → Phone Numbers. Tiene aspecto de 15–16 dígitos. Úsalo en la URL: graph.facebook.com/v21.0/{PHONE_NUMBER_ID}/messages. Un número de teléfono tiene un ID que nunca cambia.
¿Cuál es la diferencia entre un mensaje de sesión y uno de plantilla?
Un mensaje de sesión (texto, imagen, botones, ubicación, documento) solo puede enviarse dentro de la ventana de servicio de 24 horas que se abre cuando un cliente te escribe primero. Estos son gratuitos dentro de la cuota mensual. Un mensaje de plantilla es un formato aprobado por Meta necesario para iniciar nuevas conversaciones o enviar fuera de la ventana de servicio. Las plantillas deben aprobarse (1–24 h para utility, hasta 5 días para marketing) antes de poder enviarse.
¿Qué pasa cuando recibo un 429 de la WhatsApp Cloud API?
Has alcanzado el rate limit (80 msg/s por defecto). Implementa exponential backoff: espera 1000 * 2^attempt + random(500) ms antes de reintentar, con tope de 60 segundos. Añade jitter (el componente aleatorio) para evitar reintentos sincronizados desde múltiples workers. Para campañas masivas, añade un delay fijo de 15–20ms entre envíos para mantenerte con seguridad bajo el límite en vez de alcanzarlo y reintentar.
¿Cómo envío una plantilla de WhatsApp con variables en Python?
Pon "type": "template" e incluye un array components con un componente body que contenga parameters — un objeto por cada variable en tu plantilla. Cada parámetro es {"type": "text", "text": "variable_value"}. Se mapean secuencialmente a {{1}}, {{2}}, {{3}} en tu plantilla. El código Python completo está en la sección de mensaje de plantilla arriba.
¿Cómo recibo las respuestas de los clientes a mensajes de WhatsApp?
Las respuestas de clientes llegan vía webhook — Meta dispara un HTTP POST a tu endpoint registrado cuando ocurre cualquier evento de mensaje. Necesitas una URL HTTPS accesible públicamente, verificación de firma HMAC-SHA256 y parseo de payload anidado. SocialHook se encarga de todo esto automáticamente y entrega JSON limpio a tu endpoint en menos de 50ms. Mira la configuración completa en nuestra guía de webhook de entrada.

Puedes enviar. Ahora
recibe respuestas también.

Tu código de salida está listo. Conecta SocialHook para la entrada — cada respuesta de cliente llega como JSON limpio a tu endpoint de webhook. Sin código boilerplate de HMAC, sin parseo de payload anidado, sin infraestructura de reintentos que construir.

Sin tarjeta de crédito · 50 $/mes tras la prueba · Cancela cuando quieras