{"id":536,"date":"2026-10-28T16:06:52","date_gmt":"2026-10-28T16:06:52","guid":{"rendered":"https:\/\/vectalk.com.br\/blog\/bot-whatsapp-python-fastapi\/"},"modified":"2026-10-28T16:07:03","modified_gmt":"2026-10-28T16:07:03","slug":"bot-whatsapp-python-fastapi","status":"publish","type":"post","link":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/","title":{"rendered":"Bot de WhatsApp con Python en la API oficial (FastAPI, desde cero)"},"content":{"rendered":"\n<p><em>Actualizado el 28\/10\/2026<\/em><\/p>\n\n\n\n<p>Para crear un bot de WhatsApp con Python en la API oficial necesitas un servidor HTTPS con dos rutas: un <code>GET<\/code> que responde al desaf\u00edo de verificaci\u00f3n de Meta y un <code>POST<\/code> que recibe los mensajes, valida la firma <code>X-Hub-Signature-256<\/code> y contesta llamando al endpoint <code>\/messages<\/code> de la Cloud API. Con FastAPI y httpx todo cabe en un archivo de unas 150 l\u00edneas, y eso es lo que este tutorial arma desde cero.<\/p>\n\n\n\n<p>El bot final responde a &#8220;hola&#8221; o &#8220;menu&#8221; con tres botones, procesa el toque en cada bot\u00f3n, marca el mensaje como le\u00eddo, ignora eventos repetidos y corre en producci\u00f3n con Docker y HTTPS autom\u00e1tico. El c\u00f3digo se prob\u00f3 con FastAPI 0.128 y funciona tanto directo con Meta como a trav\u00e9s de Vectalk, cambiando solo dos variables de entorno.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">C\u00f3mo funciona el bot<\/h2>\n\n\n\n<p>El flujo tiene cuatro pasos:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>El cliente env\u00eda un mensaje a tu n\u00famero.<\/li>\n\n\n<li>Meta env\u00eda un <code>POST<\/code> con el evento a la URL de tu webhook.<\/li>\n\n\n<li>Tu servidor valida la firma, responde <code>200<\/code> de inmediato y procesa el evento en segundo plano.<\/li>\n\n\n<li>El bot llama a <code>POST \/&lt;PHONE_NUMBER_ID&gt;\/messages<\/code> para responder.<\/li>\n\n<\/ol>\n\n\n\n<p>Como la respuesta sale justo despu\u00e9s del mensaje del cliente, est\u00e1 dentro de la ventana de atenci\u00f3n de 24 horas y puede ser texto libre o un mensaje interactivo, sin plantilla. Si todav\u00eda est\u00e1s decidiendo c\u00f3mo acceder a la API, la gu\u00eda de <a href=\"https:\/\/vectalk.com.br\/blog\/es\/whatsapp-cloud-api-es\/\">WhatsApp Cloud API<\/a> explica las opciones.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Qu\u00e9 necesitas antes<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Variable<\/th><th>Qu\u00e9 es<\/th><th>D\u00f3nde encontrarla<\/th><\/tr><\/thead><tbody><tr><td><code>PHONE_NUMBER_ID<\/code><\/td><td>ID del n\u00famero en la Cloud API<\/td><td>Panel de apps, secci\u00f3n de WhatsApp<\/td><\/tr><tr><td><code>ACCESS_TOKEN<\/code><\/td><td>Token con el permiso <code>whatsapp_business_messaging<\/code><\/td><td>Token de usuario del sistema<\/td><\/tr><tr><td><code>APP_SECRET<\/code><\/td><td>Clave secreta de la app, usada en la firma<\/td><td>Configuraci\u00f3n de la app &gt; B\u00e1sica<\/td><\/tr><tr><td><code>VERIFY_TOKEN<\/code><\/td><td>Cualquier texto que elijas<\/td><td>Lo defines t\u00fa y lo repites en el panel<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Usa un <a href=\"https:\/\/vectalk.com.br\/blog\/es\/token-permanente-api-whatsapp-es\/\">token permanente de usuario del sistema<\/a>: el token temporal del panel vence y tumba el bot. El n\u00famero tambi\u00e9n debe estar registrado en la Cloud API; si todav\u00eda no lo est\u00e1, mira <a href=\"https:\/\/vectalk.com.br\/blog\/es\/registrar-numero-cloud-api-whatsapp\/\">c\u00f3mo registrar el n\u00famero<\/a>.<\/p>\n\n\n\n<p>En tu m\u00e1quina necesitas Python 3.10 o superior.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Prepara el proyecto<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">mkdir bot-whatsapp &amp;&amp; cd bot-whatsapp\npython -m venv .venv\nsource .venv\/bin\/activate\npip install \"fastapi[standard]\" httpx<\/code><\/pre>\n\n\n\n<p>El paquete <code>fastapi[standard]<\/code> instala Uvicorn y el comando <code>fastapi<\/code>. httpx hace las llamadas as\u00edncronas a la API.<\/p>\n\n\n\n<p>Crea el archivo <code>main.py<\/code>. Los bloques de c\u00f3digo de las secciones 1 a 5 forman el archivo completo: p\u00e9galos en orden.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">import hashlib\nimport hmac\nimport json\nimport logging\nimport os\n\nimport httpx\nfrom fastapi import BackgroundTasks, FastAPI, HTTPException, Request\nfrom fastapi.responses import PlainTextResponse\n\nVERIFY_TOKEN = os.environ[\"VERIFY_TOKEN\"]\nACCESS_TOKEN = os.environ[\"ACCESS_TOKEN\"]\nAPP_SECRET = os.environ.get(\"APP_SECRET\", \"\")\nWEBHOOK_KEY = os.environ.get(\"WEBHOOK_KEY\", \"\")\nPHONE_NUMBER_ID = os.environ.get(\"PHONE_NUMBER_ID\", \"\")\nAPI_URL = os.environ.get(\n    \"API_URL\", f\"https:\/\/graph.facebook.com\/v26.0\/{PHONE_NUMBER_ID}\/messages\"\n)\n\nlog = logging.getLogger(\"bot\")\napp = FastAPI()\nclient = httpx.AsyncClient(timeout=10)\nprocessed = set()  # en producci\u00f3n, usa Redis o tu base de datos<\/code><\/pre>\n\n\n\n<p><code>API_URL<\/code> usa por defecto la URL de Meta. Para usar Vectalk, solo cambias esta variable y el token, como se muestra m\u00e1s abajo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Verificaci\u00f3n del webhook (GET)<\/h2>\n\n\n\n<p>Cuando guardas la URL de callback en el panel, Meta env\u00eda un <code>GET<\/code> con <code>hub.mode=subscribe<\/code>, <code>hub.verify_token<\/code> y <code>hub.challenge<\/code>. Si el token coincide con el tuyo, responde <code>200<\/code> con el valor de <code>hub.challenge<\/code> en texto plano. Cualquier otra respuesta deja el endpoint sin verificar y Meta no env\u00eda eventos.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">@app.get(\"\/webhook\")\ndef verify(request: Request):\n    q = request.query_params\n    if q.get(\"hub.mode\") == \"subscribe\" and q.get(\"hub.verify_token\") == VERIFY_TOKEN:\n        return PlainTextResponse(q.get(\"hub.challenge\", \"\"))\n    raise HTTPException(status_code=403)<\/code><\/pre>\n\n\n\n<p>Los nombres de los par\u00e1metros llevan punto (<code>hub.mode<\/code>), por eso el c\u00f3digo lee <code>request.query_params<\/code> en lugar de declarar par\u00e1metros en la funci\u00f3n.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Validar la firma X-Hub-Signature-256<\/h2>\n\n\n\n<p>Cada <code>POST<\/code> de Meta trae el encabezado <code>X-Hub-Signature-256: sha256=&lt;hash&gt;<\/code>. El hash es un HMAC-SHA256 del cuerpo de la solicitud, calculado con la clave secreta de la app. Sin esta validaci\u00f3n, cualquiera que descubra tu URL puede falsificar mensajes de clientes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">def signature_ok(body: bytes, header: str) -&gt; bool:\n    if not APP_SECRET:\n        return True\n    expected = \"sha256=\" + hmac.new(APP_SECRET.encode(), body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, header or \"\")<\/code><\/pre>\n\n\n\n<p>Dos cuidados que ahorran horas de depuraci\u00f3n:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Usa los bytes originales.<\/strong> Calcula el HMAC sobre <code>await request.body()<\/code>, nunca sobre un JSON serializado de nuevo. Un espacio distinto cambia el hash.<\/li>\n\n\n<li><strong>Compara en tiempo constante.<\/strong> <code>hmac.compare_digest<\/code> evita que el tiempo de comparaci\u00f3n revele el hash.<\/li>\n\n<\/ul>\n\n\n\n<p>El art\u00edculo sobre <a href=\"https:\/\/vectalk.com.br\/blog\/es\/validar-firma-webhook-whatsapp\/\">c\u00f3mo validar la firma del webhook<\/a> profundiza en el tema y muestra los errores m\u00e1s comunes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Recibir el evento y responder r\u00e1pido<\/h2>\n\n\n\n<p>Meta espera un <code>200<\/code> r\u00e1pido. Si la entrega falla, reintenta con frecuencia decreciente durante hasta 7 d\u00edas, as\u00ed que el mismo evento puede llegar m\u00e1s de una vez. Por eso el c\u00f3digo responde antes de procesar e ignora los ID de mensaje ya vistos.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">@app.post(\"\/webhook\")\nasync def receive(request: Request, background: BackgroundTasks):\n    if WEBHOOK_KEY and request.query_params.get(\"key\") != WEBHOOK_KEY:\n        raise HTTPException(status_code=403)\n    body = await request.body()\n    if not signature_ok(body, request.headers.get(\"x-hub-signature-256\", \"\")):\n        raise HTTPException(status_code=401, detail=\"firma invalida\")\n    background.add_task(process, json.loads(body))\n    return {\"ok\": True}\n\n\nasync def process(payload: dict) -&gt; None:\n    for entry in payload.get(\"entry\", []):\n        for change in entry.get(\"changes\", []):\n            for msg in change.get(\"value\", {}).get(\"messages\", []):\n                if msg[\"id\"] in processed:\n                    continue  # Meta puede reenviar el mismo evento\n                processed.add(msg[\"id\"])\n                try:\n                    await handle(msg)\n                except httpx.HTTPError:\n                    log.exception(\"error al responder %s\", msg[\"id\"])<\/code><\/pre>\n\n\n\n<p><code>BackgroundTasks<\/code> de FastAPI ejecuta <code>process<\/code> despu\u00e9s de enviar la respuesta. Los eventos de estado (<code>sent<\/code>, <code>delivered<\/code>, <code>read<\/code>, <code>failed<\/code>) llegan al mismo webhook dentro de <code>value.statuses<\/code>, y este bot los ignora. Para tratarlos, mira <a href=\"https:\/\/vectalk.com.br\/blog\/es\/estado-mensaje-webhook-whatsapp\/\">estado del mensaje v\u00eda webhook<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Responder texto y botones interactivos<\/h2>\n\n\n\n<p>La l\u00f3gica del bot est\u00e1 en <code>handle<\/code>. &#8220;hola&#8221; o &#8220;menu&#8221; abre el men\u00fa de botones; el toque en un bot\u00f3n llega como <code>type: \"interactive\"<\/code> con <code>interactive.type: \"button_reply\"<\/code> y el <code>id<\/code> que definiste al enviar.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">ANSWERS = {\n    \"horario\": \"Atendemos de lunes a viernes, de 9 a 18 h.\",\n    \"precios\": \"Consulta la lista completa en https:\/\/example.com\/precios\",\n    \"humano\": \"\u00a1Listo! Un agente seguir\u00e1 la conversaci\u00f3n por aqu\u00ed.\",\n}\n\n\nasync def handle(msg: dict) -&gt; None:\n    dest = recipient_of(msg)\n    await mark_read(msg[\"id\"])\n    kind = msg.get(\"type\")\n    if kind == \"text\":\n        text = msg[\"text\"][\"body\"].strip().lower()\n        if text in {\"hola\", \"menu\", \"men\u00fa\"}:\n            await send_menu(dest)\n        else:\n            await send_text(dest, \"No entend\u00ed. Escribe *menu* para ver las opciones.\")\n    elif kind == \"interactive\" and msg[\"interactive\"][\"type\"] == \"button_reply\":\n        choice = msg[\"interactive\"][\"button_reply\"][\"id\"]\n        await send_text(dest, ANSWERS.get(choice, \"Opci\u00f3n no v\u00e1lida.\"))\n    else:\n        await send_text(dest, \"Por ahora solo entiendo texto y botones.\")\n\n\ndef recipient_of(msg: dict) -&gt; dict:\n    # Usuarios con nombre de usuario pueden llegar sin \"from\", solo con el BSUID\n    if \"from\" in msg:\n        return {\"to\": msg[\"from\"]}\n    return {\"recipient\": msg[\"from_user_id\"]}<\/code><\/pre>\n\n\n\n<p>La funci\u00f3n <code>recipient_of<\/code> prepara el bot para los nombres de usuario de WhatsApp. Seg\u00fan la documentaci\u00f3n de Meta, cuando el cliente adopta un nombre de usuario el campo <code>from<\/code> puede no venir en el webhook, pero el identificador <code>from_user_id<\/code> (BSUID) llega siempre, y desde julio de 2026 se pueden enviar mensajes a ese identificador en el campo <code>recipient<\/code>. El art\u00edculo sobre <a href=\"https:\/\/vectalk.com.br\/blog\/es\/nombres-de-usuario-whatsapp-bsuid\/\">usernames y BSUID<\/a> explica el cambio.<\/p>\n\n\n\n<p>Por \u00faltimo, las funciones que llaman a la API:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">async def call_api(payload: dict) -&gt; dict:\n    r = await client.post(\n        API_URL,\n        headers={\"Authorization\": f\"Bearer {ACCESS_TOKEN}\"},\n        json={\"messaging_product\": \"whatsapp\", **payload},\n    )\n    if r.is_error:\n        log.error(\"API %s: %s\", r.status_code, r.text)\n    r.raise_for_status()\n    return r.json()\n\n\nasync def mark_read(message_id: str) -&gt; None:\n    try:\n        await call_api({\n            \"status\": \"read\",\n            \"message_id\": message_id,\n            \"typing_indicator\": {\"type\": \"text\"},\n        })\n    except httpx.HTTPError:\n        pass  # no bloquea la respuesta\n\n\nasync def send_text(dest: dict, text: str) -&gt; dict:\n    return await call_api({\n        \"recipient_type\": \"individual\",\n        **dest,\n        \"type\": \"text\",\n        \"text\": {\"body\": text},\n    })\n\n\nasync def send_menu(dest: dict) -&gt; dict:\n    buttons = [(\"horario\", \"Horario\"), (\"precios\", \"Precios\"), (\"humano\", \"Hablar con agente\")]\n    return await call_api({\n        \"recipient_type\": \"individual\",\n        **dest,\n        \"type\": \"interactive\",\n        \"interactive\": {\n            \"type\": \"button\",\n            \"body\": {\"text\": \"\u00a1Hola! \u00bfC\u00f3mo te puedo ayudar?\"},\n            \"action\": {\n                \"buttons\": [\n                    {\"type\": \"reply\", \"reply\": {\"id\": i, \"title\": t}} for i, t in buttons\n                ]\n            },\n        },\n    })<\/code><\/pre>\n\n\n\n<p><code>mark_read<\/code> marca el mensaje como le\u00eddo y muestra &#8220;escribiendo&#8230;&#8221; al cliente. Meta quita el indicador cuando respondes o despu\u00e9s de 25 segundos.<\/p>\n\n\n\n<p>L\u00edmites de los botones de respuesta seg\u00fan la documentaci\u00f3n de Meta:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Campo<\/th><th>L\u00edmite<\/th><\/tr><\/thead><tbody><tr><td>Botones por mensaje<\/td><td>hasta 3<\/td><\/tr><tr><td><code>title<\/code> del bot\u00f3n<\/td><td>20 caracteres<\/td><\/tr><tr><td><code>id<\/code> del bot\u00f3n<\/td><td>256 caracteres<\/td><\/tr><tr><td><code>body.text<\/code><\/td><td>1.024 caracteres<\/td><\/tr><tr><td><code>footer.text<\/code><\/td><td>60 caracteres<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">6. Probar en local<\/h2>\n\n\n\n<p>Exporta las variables y levanta el servidor en modo desarrollo, con recarga autom\u00e1tica:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">export VERIFY_TOKEN=\"un-texto-cualquiera\"\nexport ACCESS_TOKEN=\"EAAG...\"\nexport APP_SECRET=\"tu-clave-secreta\"\nexport PHONE_NUMBER_ID=\"123456789012345\"\nfastapi dev main.py<\/code><\/pre>\n\n\n\n<p>Prueba la verificaci\u00f3n y un evento firmado sin depender de Meta:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">curl \"http:\/\/127.0.0.1:8000\/webhook?hub.mode=subscribe&amp;hub.verify_token=un-texto-cualquiera&amp;hub.challenge=42\"\n\nBODY='{\"entry\":[{\"changes\":[{\"value\":{\"messages\":[{\"from\":\"5215512345678\",\"id\":\"wamid.prueba1\",\"type\":\"text\",\"text\":{\"body\":\"menu\"}}]}}]}]}'\nSIG=$(printf '%s' \"$BODY\" | openssl dgst -sha256 -hmac \"$APP_SECRET\" | awk '{print $NF}')\ncurl -X POST http:\/\/127.0.0.1:8000\/webhook \\\n  -H \"Content-Type: application\/json\" \\\n  -H \"X-Hub-Signature-256: sha256=$SIG\" \\\n  --data-binary \"$BODY\"<\/code><\/pre>\n\n\n\n<p>El primer comando debe devolver <code>42<\/code>. El segundo devuelve <code>{\"ok\":true}<\/code> y hace que el bot env\u00ede el men\u00fa al n\u00famero del campo <code>from<\/code> (usa tu n\u00famero de prueba).<\/p>\n\n\n\n<p>Para recibir eventos reales, Meta exige una URL p\u00fablica con certificado TLS v\u00e1lido. En desarrollo, un t\u00fanel lo resuelve: la gu\u00eda para <a href=\"https:\/\/vectalk.com.br\/blog\/es\/probar-webhook-whatsapp-localmente\/\">probar el webhook en local con ngrok<\/a> muestra el paso a paso. Con la URL p\u00fablica lista, ve a Panel de apps &gt; WhatsApp &gt; Configuraci\u00f3n, completa la URL de callback (<code>https:\/\/...\/webhook<\/code>) y el token de verificaci\u00f3n, guarda y suscr\u00edbete al campo <code>messages<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Despliegue simple con Docker y HTTPS<\/h2>\n\n\n\n<p>Crea el <code>requirements.txt<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">fastapi[standard]\nhttpx<\/code><\/pre>\n\n\n\n<p>Y el <code>Dockerfile<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">FROM python:3.12-slim\nWORKDIR \/app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY main.py .\nCMD [\"fastapi\", \"run\", \"main.py\", \"--port\", \"8000\"]<\/code><\/pre>\n\n\n\n<p><code>fastapi run<\/code> es el modo de producci\u00f3n, sin recarga autom\u00e1tica. En un servidor con Docker:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">docker build -t bot-whatsapp .\ndocker run -d --restart unless-stopped -p 127.0.0.1:8000:8000 \\\n  -e VERIFY_TOKEN -e ACCESS_TOKEN -e APP_SECRET -e PHONE_NUMBER_ID \\\n  bot-whatsapp<\/code><\/pre>\n\n\n\n<p>Falta el HTTPS. Una opci\u00f3n simple es Caddy, que obtiene y renueva el certificado solo cuando el DNS del dominio apunta al servidor y los puertos 80 y 443 est\u00e1n abiertos. El <code>Caddyfile<\/code> completo tiene dos l\u00edneas:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">bot.tuempresa.com\nreverse_proxy 127.0.0.1:8000<\/code><\/pre>\n\n\n\n<p>Actualiza la URL de callback en el panel a <code>https:\/\/bot.tuempresa.com\/webhook<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Checklist antes de pasar a producci\u00f3n<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Deduplicaci\u00f3n persistente.<\/strong> El <code>set<\/code> en memoria se pierde en cada reinicio y no se comparte entre procesos. Guarda los ID procesados en Redis o en tu base de datos, con expiraci\u00f3n.<\/li>\n\n\n<li><strong>Cola.<\/strong> Si la respuesta depende de IA, de la base de datos o de sistemas lentos, cambia <code>BackgroundTasks<\/code> por una cola (Redis, RabbitMQ, SQS) con un worker aparte.<\/li>\n\n\n<li><strong>Un solo proceso por ahora.<\/strong> Con el estado en memoria, corre un \u00fanico worker. Antes de escalar, saca el estado del proceso.<\/li>\n\n\n<li><strong>Registra los errores de la API.<\/strong> El c\u00f3digo guarda el cuerpo de error de Meta. La tabla de <a href=\"https:\/\/vectalk.com.br\/blog\/es\/errores-api-whatsapp\/\">errores de la API de WhatsApp<\/a> ayuda a interpretarlos.<\/li>\n\n\n<li><strong>Secretos fuera del c\u00f3digo.<\/strong> Token y clave secreta solo en variables de entorno o en un gestor de secretos.<\/li>\n\n\n<li><strong>Multimedia.<\/strong> Im\u00e1genes, audios y documentos llegan al webhook solo con un ID; la descarga es una llamada aparte a la API de medios.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">C\u00f3mo hacerlo con Vectalk<\/h2>\n\n\n\n<p>Vectalk es un proxy de la WhatsApp Cloud API oficial de Meta: los payloads de env\u00edo y los eventos del webhook son los mismos de Meta, as\u00ed que <code>main.py<\/code> funciona sin cambios de l\u00f3gica. Cambian tres cosas:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>URL y token.<\/strong> Define <code>API_URL=https:\/\/apis.vectalk.com.br\/api\/messages<\/code> y usa en <code>ACCESS_TOKEN<\/code> un token de instancia generado en Vectalk. Con \u00e9l, el <code>phone_number_id<\/code> se resuelve autom\u00e1ticamente.<\/li>\n\n\n<li><strong>Registro del webhook por API.<\/strong> Registras la URL con <code>POST https:\/\/apis.vectalk.com.br\/api\/webhooks<\/code>, indicando <code>instance_id<\/code>, <code>url<\/code> y <code>name<\/code>, y puedes tener varias URL por instancia.<\/li>\n\n\n<li><strong>Protecci\u00f3n del endpoint.<\/strong> La <a href=\"https:\/\/docs.vectalk.com.br\/concepts\/webhooks\">documentaci\u00f3n de webhooks de Vectalk<\/a> no describe un encabezado de firma. Deja <code>APP_SECRET<\/code> vac\u00edo, define <code>WEBHOOK_KEY<\/code> con un valor aleatorio largo y registra la URL como <code>https:\/\/bot.tuempresa.com\/webhook?key=TU_VALOR<\/code>.<\/li>\n\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">curl -X POST https:\/\/apis.vectalk.com.br\/api\/webhooks \\\n  -H \"Authorization: Bearer TU_TOKEN_VECTALK\" \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"instance_id\": \"TU_INSTANCIA\", \"url\": \"https:\/\/bot.tuempresa.com\/webhook?key=TU_VALOR\", \"name\": \"Bot Python\"}'<\/code><\/pre>\n\n\n\n<p>Vectalk espera un <code>200<\/code> en menos de 20 segundos y reenv\u00eda el evento si no lo recibe, algo que la deduplicaci\u00f3n ya resuelve. Crea tu cuenta en <a href=\"https:\/\/apps.vectalk.com.br\/register\">apps.vectalk.com.br\/register<\/a> y consulta la <a href=\"https:\/\/docs.vectalk.com.br\">documentaci\u00f3n<\/a> para los dem\u00e1s endpoints. Si el siguiente paso es responder con IA, el tutorial para <a href=\"https:\/\/vectalk.com.br\/blog\/es\/chatgpt-api-oficial-whatsapp-es\/\">conectar ChatGPT a la API oficial de WhatsApp<\/a> parte de una base parecida.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Preguntas frecuentes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfNecesito una librer\u00eda espec\u00edfica de WhatsApp para Python?<\/h3>\n\n\n\n<p>No. La Cloud API es HTTP con JSON, as\u00ed que FastAPI para recibir y httpx (o requests) para enviar son suficientes. Los SDK de terceros pueden ahorrar c\u00f3digo, pero llamar a la API directamente te permite usar las funciones nuevas en cuanto Meta las lanza.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfEl bot puede iniciar la conversaci\u00f3n?<\/h3>\n\n\n\n<p>El texto libre y los botones solo funcionan dentro de la ventana de 24 horas abierta por el \u00faltimo mensaje del cliente. Para iniciar una conversaci\u00f3n, o responder despu\u00e9s de ese plazo, hay que enviar una plantilla aprobada.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfCu\u00e1nto cuesta mantener un bot?<\/h3>\n\n\n\n<p>Las respuestas dentro de la ventana de atenci\u00f3n son mensajes de servicio. Desde el 1 de octubre de 2026, cada n\u00famero tiene 1.000 mensajes de servicio gratis al mes; a partir del 1.001, la tarifa para destinatarios en Brasil es de R$ 0,0350 (reales brasile\u00f1os) por mensaje. Las tarifas de otros pa\u00edses est\u00e1n en la p\u00e1gina de precios de Meta.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfPor qu\u00e9 falla la verificaci\u00f3n del webhook en el panel?<\/h3>\n\n\n\n<p>Las causas m\u00e1s comunes: la URL no es HTTPS con certificado v\u00e1lido, el <code>VERIFY_TOKEN<\/code> del servidor no coincide con el que escribiste en el panel, o la ruta devuelve el desaf\u00edo como JSON en lugar de texto plano. Por eso el c\u00f3digo usa <code>PlainTextResponse<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfPuedo usar Flask o Django en lugar de FastAPI?<\/h3>\n\n\n\n<p>S\u00ed. La secuencia es la misma: responder al <code>GET<\/code> de verificaci\u00f3n, validar el HMAC sobre el cuerpo crudo, devolver <code>200<\/code> r\u00e1pido y llamar al endpoint de mensajes. En Flask, el cuerpo crudo se obtiene con <code>request.get_data()<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>Fuentes oficiales consultadas el 28\/10\/2026:<\/strong> <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/create-webhook-endpoint\" target=\"_blank\" rel=\"noreferrer noopener\">Create a webhook endpoint (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/overview\" target=\"_blank\" rel=\"noreferrer noopener\">Webhooks overview (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/messages\/interactive-reply-buttons-messages\" target=\"_blank\" rel=\"noreferrer noopener\">Interactive reply buttons messages (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/reference\/messages\/interactive\" target=\"_blank\" rel=\"noreferrer noopener\">Interactive messages webhook (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/typing-indicators\" target=\"_blank\" rel=\"noreferrer noopener\">Typing indicators (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/business-scoped-user-ids\" target=\"_blank\" rel=\"noreferrer noopener\">Business-scoped user IDs (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/pricing\" target=\"_blank\" rel=\"noreferrer noopener\">Pricing (Meta)<\/a>, <a href=\"https:\/\/fastapi.tiangolo.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">FastAPI<\/a>, <a href=\"https:\/\/caddyserver.com\/docs\/quick-starts\/reverse-proxy\" target=\"_blank\" rel=\"noreferrer noopener\">Caddy: reverse proxy quick-start<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/concepts\/webhooks\">Vectalk: webhooks<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/authentication\">Vectalk: autenticaci\u00f3n<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/api-reference\/messages\/send-buttons\">Vectalk: botones de respuesta<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Tutorial completo para crear un bot de WhatsApp en Python con FastAPI sobre la API oficial: webhook, firma, texto, botones y despliegue con HTTPS.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[12],"tags":[],"class_list":["post-536","post","type-post","status-publish","format-standard","hentry","category-uncategorized-es"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Bot de WhatsApp con Python y FastAPI en la API | Vectalk<\/title>\n<meta name=\"description\" content=\"Tutorial de API de WhatsApp Python con FastAPI: verificaci\u00f3n del webhook, firma X-Hub-Signature-256, respuestas, botones interactivos y despliegue.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Bot de WhatsApp con Python y FastAPI en la API | Vectalk\" \/>\n<meta property=\"og:description\" content=\"Tutorial de API de WhatsApp Python con FastAPI: verificaci\u00f3n del webhook, firma X-Hub-Signature-256, respuestas, botones interactivos y despliegue.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/\" \/>\n<meta property=\"og:site_name\" content=\"Blog Vectalk\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-28T16:06:52+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-28T16:07:03+00:00\" \/>\n<meta name=\"author\" content=\"wanderleyfcunha\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"wanderleyfcunha\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"13 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/es\\\/bot-whatsapp-python-fastapi\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/es\\\/bot-whatsapp-python-fastapi\\\/\"},\"author\":{\"name\":\"wanderleyfcunha\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\"},\"headline\":\"Bot de WhatsApp con Python en la API oficial (FastAPI, desde cero)\",\"datePublished\":\"2026-10-28T16:06:52+00:00\",\"dateModified\":\"2026-10-28T16:07:03+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/es\\\/bot-whatsapp-python-fastapi\\\/\"},\"wordCount\":1715,\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/es\\\/bot-whatsapp-python-fastapi\\\/\",\"url\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/es\\\/bot-whatsapp-python-fastapi\\\/\",\"name\":\"Bot de WhatsApp con Python y FastAPI en la API | Vectalk\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#website\"},\"datePublished\":\"2026-10-28T16:06:52+00:00\",\"dateModified\":\"2026-10-28T16:07:03+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\"},\"description\":\"Tutorial de API de WhatsApp Python con FastAPI: verificaci\u00f3n del webhook, firma X-Hub-Signature-256, respuestas, botones interactivos y despliegue.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/es\\\/bot-whatsapp-python-fastapi\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/es\\\/bot-whatsapp-python-fastapi\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/es\\\/bot-whatsapp-python-fastapi\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Bot de WhatsApp con Python en la API oficial (FastAPI, desde cero)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/\",\"name\":\"Blog Vectalk\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\",\"name\":\"wanderleyfcunha\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"caption\":\"wanderleyfcunha\"},\"sameAs\":[\"https:\\\/\\\/vectalk.com.br\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Bot de WhatsApp con Python y FastAPI en la API | Vectalk","description":"Tutorial de API de WhatsApp Python con FastAPI: verificaci\u00f3n del webhook, firma X-Hub-Signature-256, respuestas, botones interactivos y despliegue.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/","og_locale":"en_US","og_type":"article","og_title":"Bot de WhatsApp con Python y FastAPI en la API | Vectalk","og_description":"Tutorial de API de WhatsApp Python con FastAPI: verificaci\u00f3n del webhook, firma X-Hub-Signature-256, respuestas, botones interactivos y despliegue.","og_url":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/","og_site_name":"Blog Vectalk","article_published_time":"2026-10-28T16:06:52+00:00","article_modified_time":"2026-10-28T16:07:03+00:00","author":"wanderleyfcunha","twitter_card":"summary_large_image","twitter_misc":{"Written by":"wanderleyfcunha","Est. reading time":"13 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/#article","isPartOf":{"@id":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/"},"author":{"name":"wanderleyfcunha","@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b"},"headline":"Bot de WhatsApp con Python en la API oficial (FastAPI, desde cero)","datePublished":"2026-10-28T16:06:52+00:00","dateModified":"2026-10-28T16:07:03+00:00","mainEntityOfPage":{"@id":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/"},"wordCount":1715,"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/","url":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/","name":"Bot de WhatsApp con Python y FastAPI en la API | Vectalk","isPartOf":{"@id":"https:\/\/vectalk.com.br\/blog\/#website"},"datePublished":"2026-10-28T16:06:52+00:00","dateModified":"2026-10-28T16:07:03+00:00","author":{"@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b"},"description":"Tutorial de API de WhatsApp Python con FastAPI: verificaci\u00f3n del webhook, firma X-Hub-Signature-256, respuestas, botones interactivos y despliegue.","breadcrumb":{"@id":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/vectalk.com.br\/blog\/es\/bot-whatsapp-python-fastapi\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/vectalk.com.br\/blog\/"},{"@type":"ListItem","position":2,"name":"Bot de WhatsApp con Python en la API oficial (FastAPI, desde cero)"}]},{"@type":"WebSite","@id":"https:\/\/vectalk.com.br\/blog\/#website","url":"https:\/\/vectalk.com.br\/blog\/","name":"Blog Vectalk","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/vectalk.com.br\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b","name":"wanderleyfcunha","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","caption":"wanderleyfcunha"},"sameAs":["https:\/\/vectalk.com.br\/blog"]}]}},"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/536","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/comments?post=536"}],"version-history":[{"count":1,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/536\/revisions"}],"predecessor-version":[{"id":539,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/536\/revisions\/539"}],"wp:attachment":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/media?parent=536"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/categories?post=536"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/tags?post=536"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}