API de Conversiones de Meta en un motor de reservas hotelero: cuando te aparecen 1.300 compras que no existen
Implementé la API de Conversiones de Meta en un motor de reservas hotelero y empezaron a aparecer más de 40 compras diarias falsas. Esto es lo que pasó.
Antonio Soler
imorillas — Consultoría Tecnológica, Almería
Vamos por partes, que esto tiene chicha.
Llevo un tiempo implementando la API de Conversiones de Meta (CAPI) en clientes del sector hotelero y, aunque la teoría es bastante clara, la práctica tiene sus cosillas. Este artículo lo escribo a raíz de un caso concreto que me hizo rascar la cabeza bastante más de lo que esperaba: dos hoteles boutique, un motor de reservas de terceros, y más de 1.300 eventos Purchase al mes que no correspondían a ninguna reserva real.
Spoiler: al final lo dejamos fino, pero el camino hasta ahí tuvo su punto.
Por qué la CAPI ya no es opcional
Si llevas un tiempo con campañas en Meta Ads, sabrás que el píxel de navegador solo funciona a medias desde hace años. Los bloqueadores de anuncios, las restricciones de cookies de terceros y lo que hizo Apple con iOS se han cargado buena parte de la señal. Para muchos clientes, el píxel de toda la vida ya solo ve la mitad de las conversiones reales, como mucho.
El tracking server-side es la respuesta. En lugar de depender del navegador del usuario para disparar el evento, lo mandas desde tu servidor directamente a Meta. Así te libras de los bloqueadores y de las restricciones del dispositivo.
No es opcional ya. Es lo mínimo si quieres que tus campañas tengan datos decentes con los que optimizar.
El entorno técnico del caso
El hotel en cuestión tenía esta pinta:
- Motor de reservas de plataforma SaaS de terceros (Next.js), en un subdominio propio:
reservas.[dominio].es - Pasarela de pago Redsys — TPV virtual con redirección externa y vuelta al motor
- GTM web propio del hotel para el tracking
- Un VPS con Plesk en Ubuntu 22.04 que ya existía y que íbamos a aprovechar
El objetivo era montar CAPI para mejorar la señal de conversión en Meta Ads. Hasta aquí todo normal.
Cómo montamos GTM Server-Side en el VPS propio
Lo primero que te planteas cuando vas a montar GTM SS es dónde lo alojas. Hay servicios como Stape.io o Google Cloud Run que lo hacen fácil, pero tienen un coste mensual. Como el cliente ya tenía un VPS con recursos de sobra, lo montamos ahí directamente. Cero coste adicional de infraestructura.
El proceso fue así:
Docker en Ubuntu 22.04
apt update
apt install -y docker.io
systemctl enable docker
Lanzar el contenedor de GTM Server-Side
docker run -d --name gtm-ss \
-p 8080:8080 \
-e CONTAINER_CONFIG='[CONTAINER_CONFIG_STRING]' \
-e PORT=8080 \
--restart always \
gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable
Comprobar que responde
curl http://localhost:8080/healthy
# Si todo va bien: ok
Proxy inverso con Apache en Plesk
El entorno usaba Plesk con Apache, sin Nginx de por medio. Para el proxy inverso del subdominio gtm.[dominio].es añadimos estas directivas Apache:
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
Con el SSL de Let’s Encrypt que ya gestiona Plesk, el servidor quedó accesible en https://gtm.[dominio].es. Limpio.
Cómo fluyen los datos
Navegador del usuario
│
├─► Píxel Meta (navegador) ──────────────────► Meta
│
└─► GA4 Web Container (GTM-WEB)
│
└─► GTM Server-Side (gtm.[dominio].es)
│
└─► Meta CAPI ───────────────► Meta (deduplicado)
El truco está en añadir server_container_url = https://gtm.[dominio].es al tag de configuración GA4 del contenedor web. Así, el cliente GA4 del contenedor server-side recibe los eventos y los reenvía a Meta. La deduplicación la hace Meta automáticamente gracias al event_id.
El problema: 1.300 compras mensuales que no son compras
Una vez operativa la CAPI, reviso Meta Events Manager y me encuentro con más de 40 eventos Purchase diarios. En dos hoteles boutique pequeños. Que se yo, eso no cuadra ni de coña.
El primer impulso es pensar que hay algo mal con la configuración, pero no, los eventos llegan bien, con sus event_id, todo correcto. El problema es que hay demasiados.
Lo que encontré mirando el código
El motor de reservas cargaba el píxel de Meta por dos vías al mismo tiempo:
- El contenedor GTM del hotel (
GTM-XXXXXXX) — el nuestro, el que controlamos - Un contenedor GTM del proveedor del motor (
GTM-YYYYYYY) — inyectado automáticamente por la plataforma SaaS
Esto lo confirmé inspeccionando el objeto staticTrackingParams del JSON de la página:
"engineProviderGTMCode": "GTM-YYYYYYY",
"gtmCodes": ["GTM-XXXXXXX"]
O sea, la plataforma mete su propio GTM sin que el hotel lo sepa. Y ese GTM del proveedor usa el Pixel ID del hotel para disparar sus propios eventos. Bien, ya tenemos un primer problema.
Pero lo gordo no estaba ahí.
Dónde se disparaba realmente el Purchase
Capturé archivos HAR con DevTools para ver exactamente cuándo llegaba el evento ev=Purchase a Meta. Y lo que vi fue que la URL de página en el momento del disparo era:
/es/hotel/[hotel-id]/checkout
Sin ningún parámetro paymentStatus. Es decir, el Purchase se disparaba en el checkout, cuando el usuario hacía clic en “Pagar”, antes de que Redsys siquiera procesara nada.
Busqué en el bundle principal del motor (checkout-[hash].js) y encontré esto:
// Simplificado
useEffect(() => {
s.gs.addEvent(s.MK.CHECKOUT, s.J7.CHECKOUT)
m.q.setFunnel(E.o.CHECKOUT)
}, [])
useEffect(() => {
N.isEmpty() || s.gs.executeEvents() // Aquí manda el Wit_Purchase
}, [N])
La plataforma dispara Wit_Purchase en el useEffect de la página de checkout, no en la de confirmación. Cualquier usuario que llega al checkout con el carrito lleno genera un evento Purchase. Se complete el pago o no.
Ahí estaba el problema principal.
El segundo factor: Meta adivinando conversiones
Por si fuera poco, había un segundo origen de Purchase falsos que me encontré de rebote: el sistema de estimación automática de conversiones de Meta. Se identifica por estos parámetros en las peticiones a facebook.com/tr:
cs_est=true
es=automatic
est_source=[ID_INTERNO_META]
Meta infiere conversiones basándose en señales de comportamiento en la página, sin depender del píxel. Y en un motor de reservas con pasarela externa, hace agua por todos los lados.
Las soluciones
1. Filtrar el trigger en GTM
La solución más directa: no dejar que el tag de conversión se dispare si no estamos en la página de confirmación con pago exitoso.
Añadí dos condiciones al trigger de Wit_Purchase:
Page URLcontieneconfirmationPage URLcontienepaymentStatus=SUCCESS
Las URLs de confirmación del motor siguen este patrón:
/[idioma]/hotel/[hotel-id]/confirmation?token=[JWT]&paymentStatus=SUCCESS
Y funciona tanto para reservas con TPV como para reservas con cancelación gratuita (pago en el hotel). El filtro es válido para los dos casos.
2. Desactivar las coincidencias automáticas de Meta
En Meta Events Manager, dentro de la configuración del dataset, desactivé:
- Coincidencias de sitio web automáticas
- Incluir automáticamente información más detallada de la página y los productos
Estas opciones son las que permiten a Meta inferir conversiones por su cuenta. En un motor de reservas con redirección externa, generan falsos positivos de forma sistemática. Fuera.
3. Aprovechar el dato de abandono
No quería perder la información de cuántos usuarios llegaban al checkout y no completaban el pago. Eso también tiene valor analítico.
Monté un evento payment_failed en GA4:
- Variable GTM:
dl-paymentStatus— tipo URL, componente de consulta, parámetropaymentStatus - Trigger: evento personalizado
Wit_Purchasecon condición{{dl-paymentStatus}}igual aFAILED - Tag GA4: evento
payment_failed
Así queda separado en GA4 lo que es abandono de pago de lo que es conversión real, y las métricas de Meta Ads no se contaminan.
Lo que aprendes de esto si trabajas con motores de terceros
Este tipo de problema es más habitual de lo que parece en el sector hotelero. Hay una casuística estructural que conviene tener clara:
El motor no es tuyo, pero el Pixel ID sí. Cuando el proveedor inyecta su GTM y usa el Pixel ID del hotel para sus propios eventos, el hotel pierde el control sobre sus propios datos de conversión. Y muchas veces ni sabe que esto pasa.
Antes de montar CAPI en un motor de reservas de terceros, yo siempre hago estas preguntas:
- ¿El proveedor tiene su propio contenedor GTM activo en el motor?
- ¿En qué momento del flujo de reserva dispara el Purchase?
- ¿El pago pasa por pasarela externa (Redsys, Stripe, PayPal)?
- ¿Están activadas las coincidencias automáticas de Meta?
Si el proveedor puede mover el evento de Purchase a la página de confirmación con validación del paymentStatus, esa es la solución limpia. Si no puede o no quiere, los filtros de trigger que he descrito aquí funcionan bien como solución alternativa.
Resultado final
Después de aplicar las tres correcciones:
- Los eventos Purchase en Meta reflejan solo reservas confirmadas con pago completado
- La deduplicación entre píxel y CAPI funciona correctamente (en Meta Events Manager aparecen los eventos del servidor marcados como “Deduplicados”)
- Los abandonos de pago quedan registrados en GA4 como
payment_failed, sin tocar las métricas de conversión de Meta - El GTM Server-Side corre en el VPS existente sin coste adicional
No es el caso más raro que me ha tocado, pero sí uno de los que más me ha gustado resolver. Cuando tienes datos rotos que no entiendes de dónde vienen, el proceso de ir tirando del hilo hasta dar con la causa tiene su punto.
Caso real de implementación. Identificadores, dominios y datos específicos del cliente anonimizados.
¿Quieres que esto funcione en tu negocio?
Diagnosticamos qué necesitas y lo ponemos en marcha. Sin tecnicismos, sin perder tu tiempo.
Solicitar diagnóstico gratuito