{"id":45,"date":"2025-11-02T19:30:56","date_gmt":"2025-11-02T19:30:56","guid":{"rendered":"https:\/\/pulki.es\/blog\/?p=45"},"modified":"2025-11-02T19:30:56","modified_gmt":"2025-11-02T19:30:56","slug":"desarrollando-un-asistente-virtual-con-whatsapp-y-openai-parte-2","status":"publish","type":"post","link":"https:\/\/pulki.es\/blog\/index.php\/2025\/11\/02\/desarrollando-un-asistente-virtual-con-whatsapp-y-openai-parte-2\/","title":{"rendered":"Desarrollando un asistente virtual con WhatsApp y OpenAI (Parte 2)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">En esta publicaci\u00f3n veremos c\u00f3mo enviar un mensaje de WhatsApp usando la API de Meta. En la <a href=\"https:\/\/pulki.es\/blog\/index.php\/2025\/10\/24\/desarrollando-un-asistente-virtual-con-whatsapp-y-openai-parte-1\/\">primera parte<\/a> de esta edici\u00f3n, obtuvimos el <strong>Identificador del n\u00famero de tel\u00e9fono<\/strong>, <strong>Identificador de verificaci\u00f3n<\/strong>, <strong>API Key de WhatsApp<\/strong> y la <strong>Clave secreta de la aplicaci\u00f3n<\/strong>. Aquellos ser\u00e1n \u00fatiles para este post, por tanto, si no sabes c\u00f3mo obtenerlos, ve a dicha publicaci\u00f3n.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00ednea, es importante que las tengas en cuenta. En el siguiente <a href=\"https:\/\/business.whatsapp.com\/policy\">enlace<\/a> las puedes consultar. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En t\u00e9rminos pr\u00e1cticos, una norma que ser\u00e1 relevante para efectos de esta publicaci\u00f3n es la ventana de 24h que impone Meta. Su funcionamiento se puede describir de la siguiente forma:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>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.<\/li>\n\n\n\n<li>Si pasan 24h en las que el usuario <strong>no <\/strong>escribe, esa ventana se cierra, y ya no puedes enviar mensajes libres. En este caso, si deseas enviar mensajes debes usar una <strong>plantilla<\/strong> aprobada por Meta.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">En la presente publicaci\u00f3n no ahondaremos en el uso de plantillas, pero extender este c\u00f3digo para aplicarlas es trivial. Dicho lo anterior, en lo que sigue, describiremos detalladamente c\u00f3mo estructurar nuestro c\u00f3digo PHP para enviar mensajes. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Basics<\/em><\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\"><\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Como mencionamos en el post anterior, debes saber que estaremos trabajando en un archivo php que ser\u00e1 el que Meta debe encontrar en la siguiente ruta.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"643\" height=\"165\" src=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2025\/11\/image.png\" alt=\"\" class=\"wp-image-46\" srcset=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2025\/11\/image.png 643w, https:\/\/pulki.es\/blog\/wp-content\/uploads\/2025\/11\/image-300x77.png 300w\" sizes=\"auto, (max-width: 643px) 100vw, 643px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Adem\u00e1s, guardaremos nuestros identificadores y claves en las siguientes variables:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Identificador del n\u00famero de tel\u00e9fono<\/strong>: $PHONE_NUMBER_ID<\/li>\n\n\n\n<li><strong>Identificador de verificaci\u00f3n<\/strong>: $VERIFY_TOKEN<\/li>\n\n\n\n<li><strong>API Key de WhatsApp<\/strong>: $WHATSAPP_TOKEN<\/li>\n\n\n\n<li><strong>Clave secreta de la aplicaci\u00f3n<\/strong>: $APP_SECRET<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Paso 1: Verificaci\u00f3n<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Primero que todo, debemos verificar que lo que estamos recibiendo es veraz, de lo contrario, cualquier persona podr\u00eda hacerse pasar por Meta. A continuaci\u00f3n el c\u00f3digo que lleva a cabo este proceso:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>if ($_SERVER&#91;'REQUEST_METHOD'] === 'GET') {\n$mode = $_GET&#91;'hub.mode'] ?? $_GET&#91;'hub_mode'] ?? '';\n$token = $_GET&#91;'hub.verify_token'] ?? $_GET&#91;'hub_verify_token'] ?? '';\n$challenge = $_GET&#91;'hub.challenge'] ?? $_GET&#91;'hub_challenge'] ?? '';\n\nif ($mode === 'subscribe' &amp;&amp; hash_equals($VERIFY_TOKEN, (string)$token)) {\nheader('Content-Type: text\/plain; charset=utf-8');\nhttp_response_code(200);\necho $challenge; \/\/ devolver EXACTAMENTE el challenge\nexit;\n}\nhttp_response_code(403);\necho 'Verification failed';\nexit;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Este c\u00f3digo recibe la consulta de Meta, y luego compara si nuestro $VERIFY_TOKEN, es igual al que env\u00eda Meta. En caso de que sean iguales, responde 200. Esto es muy importante, puesto que si Meta no recibe 200, reenviar\u00e1 la misma consulta sistem\u00e1ticamente, lo que eventualmente puede saturar nuestro servidor.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>(Opcional) Validaci\u00f3n de lo recibido<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En el siguiente fragmento validaremos la firma contra $APP_SECRET.  En caso de que no sean iguales, enviamos 403.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$raw = file_get_contents('php:\/\/input') ?: '';\n$headerSig = $_SERVER&#91;'HTTP_X_HUB_SIGNATURE_256'] ?? '';\n\nif ($APP_SECRET &amp;&amp; $headerSig) {\n$expected = 'sha256=' . hash_hmac('sha256', $raw, $APP_SECRET);\nif (!hash_equals($expected, $headerSig)) {\nhttp_response_code(403);\nexit;\n}\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Si bien este paso es opcional, nos permite estar seguros de que el contenido no ha sido manipulado. Este c\u00f3digo puede ser escrito antes de enviar el c\u00f3digo 200 en el paso de verificaci\u00f3n.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Paso 2: Leyendo el payload&#8230;<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ahora estamos en condiciones de leer el contenido del mensaje. Un env\u00edo t\u00edpico podr\u00eda venir dado por el siguiente archivo JSON:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;\n{\n\"object\": \"whatsapp_business_account\",\n\"entry\": &#91;\n{\n\"id\": \"00000000000000\",\n\"changes\": &#91;\n{\n\"value\": {\n\"messaging_product\": \"whatsapp\",\n\"metadata\": {\n\"display_phone_number\": \"00000000000\",\n\"phone_number_id\": \"000000000000000\"\n},\n\"contacts\": &#91;\n{\n\"profile\": {\n\"name\": \"xxxxxx\"\n},\n\"wa_id\": \"340000000\"\n}\n],\n\"messages\": &#91;\n{\n\"from\": \"3400000000\",\n\"id\": \"wamid.HBgLMzQ2MjQ5NDQxOTgVAgASGBQzRjYwRTBDNjIzMzFDOTk2MTUyOBB=\",\n\"timestamp\": \"1762106576\",\n\"text\": {\n\"body\": \"quiero un corte de pelo\"\n},\n\"type\": \"text\"\n}\n]\n},\n\"field\": \"messages\"\n}\n]\n}\n]\n},\n{\n\"object\": \"whatsapp_business_account\",\n\"entry\": &#91;\n{\n\"id\": \"000000000000\",\n\"changes\": &#91;\n{\n\"value\": {\n\"messaging_product\": \"whatsapp\",\n\"metadata\": {\n\"display_phone_number\": \"00000000000\",\n\"phone_number_id\": \"000000000000\"\n},\n\"statuses\": &#91;\n{\n\"id\": \"wamid.HBgLMzQ2MjQ5NDQxOTgVAgARGBIwRDk1RUQzRTY4MUQzMEI1QTEE\",\n\"status\": \"sent\",\n\"timestamp\": \"1762106589\",\n\"recipient_id\": \"340000000\",\n\"pricing\": {\n\"billable\": false,\n\"pricing_model\": \"PMP\",\n\"category\": \"service\",\n\"type\": \"free_customer_service\"\n}\n}\n]\n},\n\"field\": \"messages\"\n}\n]\n}\n]\n},\n{\n\"object\": \"whatsapp_business_account\",\n\"entry\": &#91;\n{\n\"id\": \"83862538857987\",\n\"changes\": &#91;\n{\n\"value\": {\n\"messaging_product\": \"whatsapp\",\n\"metadata\": {\n\"display_phone_number\": \"000000000\",\n\"phone_number_id\": \"0000000000000\"\n},\n\"statuses\": &#91;\n{\n\"id\": \"wamid.HBgLMzQ2MjQ5NDQxOTgVAgARGBIwRDk1RUQzRTY4MUQzMEI1QEEE\",\n\"status\": \"delivered\",\n\"timestamp\": \"1762106589\",\n\"recipient_id\": \"340000000000\",\n\"pricing\": {\n\"billable\": false,\n\"pricing_model\": \"PMP\",\n\"category\": \"service\",\n\"type\": \"free_customer_service\"\n}\n}\n]\n},\n\"field\": \"messages\"\n}\n]\n}\n]\n}\n]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">De este JSON, nos interesa [&#8216;entry&#8217;] -> [&#8216;changes&#8217;] -> [&#8216;messages&#8217;] ->[&#8216;text&#8217;] &#8211; [&#8216;body&#8217;], si s\u00f3lo queremos leer el mensaje, que en este caso es \u00abquiero un corte de pelo\u00bb. No obstante, tambi\u00e9n podemos obtener el n\u00famero de WhatsApp del emisor desde la ruta [&#8216;entry&#8217;] -> [&#8216;changes&#8217;] -> [&#8216;messages&#8217;] ->[&#8216;from&#8217;]. El c\u00f3digo php que lleva a cabo esto viene dado por:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>foreach ($payload&#91;'entry'] as $entry) {\nforeach ($entry&#91;'changes'] ?? &#91;] as $change) {\nif (($change&#91;'field'] ?? '') !== 'messages') continue;\n$v = $change&#91;'value'] ?? &#91;];\nforeach ($v&#91;'messages'] ?? &#91;] as $msg) {\n  $mid  = $msg&#91;'id']   ?? null;   \/\/ message id (wamid-...)\n  $from = $msg&#91;'from'] ?? null;   \/\/ wa_id del cliente (E.164 sin '+')\n  $type = $msg&#91;'type'] ?? 'text';\n \/\/ mensaje del cliente\n  $text = ($type === 'text') ? (string)($msg&#91;'text']&#91;'body'] ?? '') : '';\n     }\n   }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puesto que ya recibimos el mensaje del emisor, estamos aptos para procesarlo y generar una respuesta.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Paso 3: Enviando la respuesta&#8230;<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La siguiente funci\u00f3n en PHP env\u00eda la respuesta:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function callWhatsApp(string $path, array $data): array {\nglobal $WHATSAPP_TOKEN, $PHONE_NUMBER_ID;\nif (!$WHATSAPP_TOKEN || !$PHONE_NUMBER_ID) {\nreturn &#91;'code' => 0, 'body' => 'Missing WHATSAPP_TOKEN or PHONE_NUMBER_ID'];\n}\n$graphVer = 'v24.0';\n$url = \"https:\/\/graph.facebook.com\/{$graphVer}\/{$PHONE_NUMBER_ID}\/{$path}\";\n$ch = curl_init($url);\ncurl_setopt_array($ch, &#91;\nCURLOPT_POST => true,\nCURLOPT_HTTPHEADER => &#91;\n'Authorization: Bearer ' . $WHATSAPP_TOKEN,\n'Content-Type: application\/json',\n],\nCURLOPT_POSTFIELDS => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),\nCURLOPT_RETURNTRANSFER => true,\nCURLOPT_CONNECTTIMEOUT => 10,\nCURLOPT_TIMEOUT => 25,\n]);\n$res = curl_exec($ch);\n$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);\nif ($res === false) {\n$err = curl_error($ch);\ncurl_close($ch);\nreturn &#91;'code' => -1, 'body' => 'cURL error: ' . $err];\n}\ncurl_close($ch);\nreturn &#91;'code' => $code, 'body' => $res];\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>callWhatsApp <\/em><\/strong>emplea $WHATSAPP_TOKEN y $PHONE_NUMBER_ID (como variables globales), y como argumento $path y $data. $path ser\u00e1 un string que en este caso tendr\u00e1 el valor &#8216;messages&#8217; (\u00fatil para construir el endpoint), y  $data el siguiente arreglo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;\n'messaging_product' => 'whatsapp',\n'to' => $client,\n'type' => 'text',\n'text' => &#91;\n'body' => $reply,\n'preview_url' => false,\n]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">que es la forma est\u00e1ndar en que Meta recibe la solicitud. En este caso, $reply es la respuesta y $client es el n\u00famero al que enviaremos el mensaje. El resto de la funci\u00f3n es la construcci\u00f3n de un simple cURL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Jos\u00e9 Miguel Mu\u00f1oz Urra \u2013\u00a0<a href=\"mailto:jmunozu@pulki.es\">jmunozu@pulki.es<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>En esta publicaci\u00f3n veremos c\u00f3mo enviar un mensaje de WhatsApp usando la API de Meta. En la primera parte de esta edici\u00f3n, obtuvimos el Identificador del n\u00famero de tel\u00e9fono, Identificador de verificaci\u00f3n, API Key de WhatsApp y la Clave secreta de la aplicaci\u00f3n. Aquellos ser\u00e1n \u00fatiles para este post, por tanto, si no sabes c\u00f3mo [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-45","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/45","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=45"}],"version-history":[{"count":1,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/45\/revisions"}],"predecessor-version":[{"id":47,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/45\/revisions\/47"}],"wp:attachment":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=45"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=45"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=45"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}