Integraciones

Verificación en tu receptor Python

Funciones descargables para GET y HMAC, sin dependencias externas.

Revisado el 28 de septiembre de 2026 · WhatsAut

Qué hace el ejemplo

El archivo utiliza la biblioteca estándar de Python. No inicia un servidor, no guarda mensajes y no envía respuestas a clientes. Integralo en las rutas HTTP de tu aplicación.

Para la prueba inicial usá verify_test_ping con tu token de verificación y la URL exacta guardada en el panel. Para mensajes/estados reales usá la API de verificación de WhatsAut. verify_signature requiere administrar la app Meta y disponer de su App Secret; el verify token no sustituye ese secreto.

Funciones de verificación

Pasá los bytes originales del cuerpo a verify_signature antes de convertirlos a JSON. Una nueva serialización, aunque represente el mismo objeto, puede cambiar la firma.

verify_challenge devuelve el texto exacto cuando la validación es correcta y None cuando hay que rechazar. No confundas None con una respuesta HTTP satisfactoria.

"""Helpers para tu receptor; no inicia un servidor ni procesa mensajes.

verify_test_ping autentica exclusivamente pruebas sintéticas con tu verify token.
Para mensajes/estados usá la API de verificación de WhatsAut, o verify_signature
sólo cuando administrás la app Meta y disponés de su App Secret.
"""

import hashlib
import hmac
import json
import re
from datetime import UTC, datetime


def verify_challenge(
    mode: str | None,
    token: str | None,
    challenge: str | None,
    expected_token: str,
) -> str | None:
    """Retorna texto para HTTP 200, o None para rechazar con HTTP 403."""
    if mode != "subscribe" or not token or not challenge or not expected_token:
        return None
    if not hmac.compare_digest(token.encode("utf-8"), expected_token.encode("utf-8")):
        return None
    return challenge


def verify_signature(raw_body: bytes, signature: str | None, app_secret: str) -> bool:
    """Valida bytes originales. Firma ausente, inválida o secreto vacío: rechazo."""
    if not app_secret or signature is None:
        return False
    if re.fullmatch(r"sha256=[0-9a-fA-F]{64}", signature) is None:
        return False
    expected = hmac.new(app_secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature[7:].lower())


def verify_test_ping(
    raw_body: bytes,
    signature: str | None,
    verify_token: str,
    callback_url: str,
    *,
    now: datetime | None = None,
) -> bool:
    """Sólo pruebas recientes para la URL exacta del panel; nunca autoriza mensajes.

    Usá X-WhatsAut-Test-Signature-256. Deduplificá request_id durante cinco
    minutos antes de cualquier efecto adicional; una prueba sólo recibe HTTP 2xx.
    """
    if not verify_token or not callback_url or not signature or len(raw_body) > 65536:
        return False
    if re.fullmatch(r"v1=[0-9a-f]{64}", signature) is None:
        return False
    material = b"whatsaut.webhook_test.v1\n" + callback_url.encode("utf-8") + b"\n" + raw_body
    expected = hmac.new(verify_token.encode("utf-8"), material, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature[3:]):
        return False
    try:
        body: object = json.loads(raw_body)
        if not isinstance(body, dict) or set(body) != {"object", "event", "request_id", "sent_at", "test"}:
            return False
        if body["object"] != "whatsaut_webhook_test" or body["event"] != "whatsaut.webhook_test" or body["test"] is not True:
            return False
        if not isinstance(body["request_id"], str) or re.fullmatch(r"[A-Za-z0-9_-]{8,80}", body["request_id"]) is None:
            return False
        if not isinstance(body["sent_at"], str):
            return False
        sent_at = datetime.fromisoformat(body["sent_at"])
        current = now or datetime.now(UTC)
        if sent_at.tzinfo is None or current.tzinfo is None:
            return False
        age = (current - sent_at).total_seconds()
        return -30 <= age <= 300
    except (ValueError, UnicodeDecodeError, RecursionError, OverflowError):
        return False

Cómo conectarlo a tu framework

Ruta GET: leé hub.mode, hub.verify_token y hub.challenge. Si la función devuelve None, respondé 403. En caso contrario, devolvé el texto con 200 y text/plain, sin salto de línea adicional.

Ruta POST: limitá el tamaño durante la lectura, obtené el cuerpo original y rechazá firmas inválidas con 403. Después validá JSON y estructura. Distinguí el ping sintético de eventos reales.

Guardá eventos reales en una cola durable antes del 2xx; procesá y deduplicá en segundo plano. Estas funciones no implementan cola, límites de carga ni protección contra reenvío de un evento ya firmado.

Casos que debe superar

GET con token incorrecto o parámetro faltante: rechazo. Firma ausente, secreto vacío o cuerpo modificado: rechazo. Firma válida sobre los mismos bytes: aceptación.

Los tests del archivo verifican estos casos con secretos ficticios. Eso comprueba el algoritmo, no la conectividad de un endpoint ni la disponibilidad de credenciales para tu integración.

¿Necesitás ayuda? Prepará tu consulta para soporte →