En esta publicación veremos cómo enviar un mensaje de WhatsApp usando la API de Meta. En la primera parte de esta edición, obtuvimos el Identificador del número de teléfono, Identificador de verificación, API Key de WhatsApp y la Clave secreta de la aplicación. Aquellos serán útiles para este post, por tanto, si no sabes cómo obtenerlos, ve a dicha publicación.
Antes que todo, es necesario recordar que Meta impone restricciones para enviar y recibir mensajes utilizando WhatsApp Business. Si quieres iniciar un proyecto en esta línea, es importante que las tengas en cuenta. En el siguiente enlace las puedes consultar.
En términos prácticos, una norma que será relevante para efectos de esta publicación es la ventana de 24h que impone Meta. Su funcionamiento se puede describir de la siguiente forma:
- Cada vez que el usuario te escribe o llama por WhatsApp, se abre (o reinicia) una ventana de 24h, en la que puedes responder con tu Bot libremente.
- Si pasan 24h en las que el usuario no escribe, esa ventana se cierra, y ya no puedes enviar mensajes libres. En este caso, si deseas enviar mensajes debes usar una plantilla aprobada por Meta.
En la presente publicación no ahondaremos en el uso de plantillas, pero extender este código para aplicarlas es trivial. Dicho lo anterior, en lo que sigue, describiremos detalladamente cómo estructurar nuestro código PHP para enviar mensajes.
Basics
Como mencionamos en el post anterior, debes saber que estaremos trabajando en un archivo php que será el que Meta debe encontrar en la siguiente ruta.

Además, guardaremos nuestros identificadores y claves en las siguientes variables:
- Identificador del número de teléfono: $PHONE_NUMBER_ID
- Identificador de verificación: $VERIFY_TOKEN
- API Key de WhatsApp: $WHATSAPP_TOKEN
- Clave secreta de la aplicación: $APP_SECRET
Paso 1: Verificación
Primero que todo, debemos verificar que lo que estamos recibiendo es veraz, de lo contrario, cualquier persona podría hacerse pasar por Meta. A continuación el código que lleva a cabo este proceso:
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$mode = $_GET['hub.mode'] ?? $_GET['hub_mode'] ?? '';
$token = $_GET['hub.verify_token'] ?? $_GET['hub_verify_token'] ?? '';
$challenge = $_GET['hub.challenge'] ?? $_GET['hub_challenge'] ?? '';
if ($mode === 'subscribe' && hash_equals($VERIFY_TOKEN, (string)$token)) {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(200);
echo $challenge; // devolver EXACTAMENTE el challenge
exit;
}
http_response_code(403);
echo 'Verification failed';
exit;
}
Este código recibe la consulta de Meta, y luego compara si nuestro $VERIFY_TOKEN, es igual al que envía Meta. En caso de que sean iguales, responde 200. Esto es muy importante, puesto que si Meta no recibe 200, reenviará la misma consulta sistemáticamente, lo que eventualmente puede saturar nuestro servidor.
(Opcional) Validación de lo recibido
En el siguiente fragmento validaremos la firma contra $APP_SECRET. En caso de que no sean iguales, enviamos 403.
$raw = file_get_contents('php://input') ?: '';
$headerSig = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if ($APP_SECRET && $headerSig) {
$expected = 'sha256=' . hash_hmac('sha256', $raw, $APP_SECRET);
if (!hash_equals($expected, $headerSig)) {
http_response_code(403);
exit;
}
}
Si bien este paso es opcional, nos permite estar seguros de que el contenido no ha sido manipulado. Este código puede ser escrito antes de enviar el código 200 en el paso de verificación.
Paso 2: Leyendo el payload…
Ahora estamos en condiciones de leer el contenido del mensaje. Un envío típico podría venir dado por el siguiente archivo JSON:
[
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "00000000000000",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "00000000000",
"phone_number_id": "000000000000000"
},
"contacts": [
{
"profile": {
"name": "xxxxxx"
},
"wa_id": "340000000"
}
],
"messages": [
{
"from": "3400000000",
"id": "wamid.HBgLMzQ2MjQ5NDQxOTgVAgASGBQzRjYwRTBDNjIzMzFDOTk2MTUyOBB=",
"timestamp": "1762106576",
"text": {
"body": "quiero un corte de pelo"
},
"type": "text"
}
]
},
"field": "messages"
}
]
}
]
},
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "000000000000",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "00000000000",
"phone_number_id": "000000000000"
},
"statuses": [
{
"id": "wamid.HBgLMzQ2MjQ5NDQxOTgVAgARGBIwRDk1RUQzRTY4MUQzMEI1QTEE",
"status": "sent",
"timestamp": "1762106589",
"recipient_id": "340000000",
"pricing": {
"billable": false,
"pricing_model": "PMP",
"category": "service",
"type": "free_customer_service"
}
}
]
},
"field": "messages"
}
]
}
]
},
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "83862538857987",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "000000000",
"phone_number_id": "0000000000000"
},
"statuses": [
{
"id": "wamid.HBgLMzQ2MjQ5NDQxOTgVAgARGBIwRDk1RUQzRTY4MUQzMEI1QEEE",
"status": "delivered",
"timestamp": "1762106589",
"recipient_id": "340000000000",
"pricing": {
"billable": false,
"pricing_model": "PMP",
"category": "service",
"type": "free_customer_service"
}
}
]
},
"field": "messages"
}
]
}
]
}
]
De este JSON, nos interesa [‘entry’] -> [‘changes’] -> [‘messages’] ->[‘text’] – [‘body’], si sólo queremos leer el mensaje, que en este caso es «quiero un corte de pelo». No obstante, también podemos obtener el número de WhatsApp del emisor desde la ruta [‘entry’] -> [‘changes’] -> [‘messages’] ->[‘from’]. El código php que lleva a cabo esto viene dado por:
foreach ($payload['entry'] as $entry) {
foreach ($entry['changes'] ?? [] as $change) {
if (($change['field'] ?? '') !== 'messages') continue;
$v = $change['value'] ?? [];
foreach ($v['messages'] ?? [] as $msg) {
$mid = $msg['id'] ?? null; // message id (wamid-...)
$from = $msg['from'] ?? null; // wa_id del cliente (E.164 sin '+')
$type = $msg['type'] ?? 'text';
// mensaje del cliente
$text = ($type === 'text') ? (string)($msg['text']['body'] ?? '') : '';
}
}
}
Puesto que ya recibimos el mensaje del emisor, estamos aptos para procesarlo y generar una respuesta.
Paso 3: Enviando la respuesta…
La siguiente función en PHP envía la respuesta:
function callWhatsApp(string $path, array $data): array {
global $WHATSAPP_TOKEN, $PHONE_NUMBER_ID;
if (!$WHATSAPP_TOKEN || !$PHONE_NUMBER_ID) {
return ['code' => 0, 'body' => 'Missing WHATSAPP_TOKEN or PHONE_NUMBER_ID'];
}
$graphVer = 'v24.0';
$url = "https://graph.facebook.com/{$graphVer}/{$PHONE_NUMBER_ID}/{$path}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $WHATSAPP_TOKEN,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 25,
]);
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($res === false) {
$err = curl_error($ch);
curl_close($ch);
return ['code' => -1, 'body' => 'cURL error: ' . $err];
}
curl_close($ch);
return ['code' => $code, 'body' => $res];
}
callWhatsApp emplea $WHATSAPP_TOKEN y $PHONE_NUMBER_ID (como variables globales), y como argumento $path y $data. $path será un string que en este caso tendrá el valor ‘messages’ (útil para construir el endpoint), y $data el siguiente arreglo:
[
'messaging_product' => 'whatsapp',
'to' => $client,
'type' => 'text',
'text' => [
'body' => $reply,
'preview_url' => false,
]
que es la forma estándar en que Meta recibe la solicitud. En este caso, $reply es la respuesta y $client es el número al que enviaremos el mensaje. El resto de la función es la construcción de un simple cURL.
José Miguel Muñoz Urra – jmunozu@pulki.es