Cómo pasar UTMs del sitio al checkout de Hotmart y Kiwify
El mayor problema de quien vende infoproducto: el checkout ocurre en un dominio externo (Hotmart, Kiwify, Eduzz) y los parámetros UTM desaparecen en ese salto. Resultado: atribución rota, ROAS equivocado, CAC inflado. Ve cómo resolverlo en 7 minutos.
Estrategia en 3 partes: (1) captura los UTMs de la URL en el JS de la landing y persístelos en localStorage; (2) al hacer clic en el botón de compra, anexa esos UTMs al link del checkout (Hotmart acepta sck, Kiwify acepta utm_source/medium/campaign/content); (3) cuando Hotmart/Kiwify dispara el webhook de venta aprobada, tu servidor envía el evento Purchase a Meta CAPI con los UTMs originales. Cierra el loop al 100%.
El agujero negro del checkout externo
Corres Meta Ads hacia una landing page. El usuario hace clic, llega a la landing con ?utm_source=meta&utm_medium=cpc&utm_campaign=producto_x. Todo bien hasta aquí.
Ahora hace clic en el botón "Comprar ahora", que lleva al checkout de Hotmart: pay.hotmart.com/A12345B?ref=.... Los UTMs desaparecen en ese redireccionamiento. El pixel de la landing dispara ViewContent, pero el Purchase ocurre en el dominio de Hotmart — fuera del scope del pixel.
Resultado práctico en una operación típica de infoproducto:
- Gastaste USD 2.000 en Meta Ads en la campaña X;
- Hotmart muestra que 80 ventas vinieron en ese período;
- Meta atribuye solo 25 de esas ventas a la campaña X (el resto quedó como "orgánico" o "directo" porque los UTMs desaparecieron);
- Crees que la campaña X tiene ROAS 2.5×, cuando en realidad tiene 8×.
Decides pausar la campaña por "performance malo". En realidad estabas matando a la gallina de los huevos de oro.
La estrategia: 3 partes que cierran el loop
- Capturar los UTMs en la landing (en el momento que el usuario llega);
- Propagar los UTMs al link del checkout (en el momento del clic);
- Atribuir vía webhook cuando la venta es aprobada (en el servidor → Meta CAPI).
Cada parte es simple por separado, pero necesitas las 3 funcionando juntas para tener atribución completa.
Parte 1: capturar UTMs en la landing
JS en el header de tu landing page (se ejecuta antes de que el usuario interactúe):
// landing-utm-capture.js
(function() {
const params = new URLSearchParams(window.location.search);
const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'fbclid', 'gclid', 'ttclid'];
const captured = {};
let hasNew = false;
utmKeys.forEach(key => {
const value = params.get(key);
if (value) { captured[key] = value; hasNew = true; }
});
if (hasNew) {
// Persiste por 30 días
const data = { ...captured, capturedAt: Date.now() };
localStorage.setItem('trakvo_attribution', JSON.stringify(data));
// También cookie de fallback (en caso de que localStorage sea limpiado)
document.cookie = `trakvo_attribution=${encodeURIComponent(JSON.stringify(data))}; max-age=${30 * 24 * 60 * 60}; path=/; SameSite=Lax`;
}
})();
¿Por qué persistir en localStorage Y cookie? Porque algunos navegadores móviles limpian uno u otro. Dos redundantes = más robusto.
Parte 2: propagar UTMs al link del checkout
Para Hotmart (usa el parámetro sck)
// cuando user hace clic en "Comprar ahora"
function buildHotmartCheckoutUrl(productCode) {
const attribution = JSON.parse(localStorage.getItem('trakvo_attribution') || '{}');
// sck acepta hasta 80 caracteres. Codifica info principal:
const sck = [
attribution.utm_source || 'direct',
attribution.utm_campaign || 'none',
attribution.utm_content || 'none',
attribution.fbclid ? attribution.fbclid.slice(0, 16) : ''
].join('-');
const url = new URL(`https://pay.hotmart.com/${productCode}`);
url.searchParams.set('sck', sck);
url.searchParams.set('off', 'meta_ads'); // offer override opcional
// Hotmart también acepta UTMs directos:
Object.entries(attribution).forEach(([key, value]) => {
if (key.startsWith('utm_')) url.searchParams.set(key, value);
});
return url.toString();
}
document.querySelector('.btn-buy').href = buildHotmartCheckoutUrl('A12345B');
Para Kiwify (usa UTMs directos)
function buildKiwifyCheckoutUrl(productSlug) {
const attribution = JSON.parse(localStorage.getItem('trakvo_attribution') || '{}');
const url = new URL(`https://pay.kiwify.com/${productSlug}`);
// Kiwify acepta UTMs nativamente — los pasa directo
['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(key => {
if (attribution[key]) url.searchParams.set(key, attribution[key]);
});
// Click IDs como query custom (Kiwify los guarda en metadata)
if (attribution.fbclid) url.searchParams.set('fbclid', attribution.fbclid);
return url.toString();
}
Parte 3: webhook server-side → Meta CAPI
Hotmart y Kiwify ofrecen webhook cuando la venta es aprobada. Configúralo en el panel de cada uno (Settings → Webhooks → Add endpoint).
Tu endpoint PHP recibirá un POST tipo:
// Webhook de Hotmart (simplificado)
{
"data": {
"purchase": {
"transaction": "HP12345...",
"status": "APPROVED",
"approved_date": 1716640000000,
"checkout_country": { "iso": "AR" },
"tracking": {
"sck": "meta-campaign123-adset456-fbclid_abc",
"utm_source": "meta",
"utm_medium": "cpc",
"utm_campaign": "producto_x"
},
"price": { "value": 297.00, "currency_value": "USD" }
},
"buyer": {
"email": "[email protected]",
"name": "Juan Pérez",
"checkout_phone": "+5491199998888"
}
}
}
En tu servidor, valida la firma HMAC del webhook, después dispara CAPI:
// webhook-hotmart.php
$payload = json_decode(file_get_contents('php://input'), true);
$purchase = $payload['data']['purchase'];
$buyer = $payload['data']['buyer'];
// Decodifica sck para recuperar UTMs originales
$sck_parts = explode('-', $purchase['tracking']['sck'] ?? '');
$utm_source = $sck_parts[0] ?? 'direct';
$utm_campaign = $sck_parts[1] ?? null;
$fbclid_short = $sck_parts[3] ?? null;
// Arma evento para Meta CAPI
$event = [
'event_name' => 'Purchase',
'event_time' => intval($purchase['approved_date'] / 1000),
'event_id' => $purchase['transaction'], // hotmart transaction = event_id
'action_source' => 'website',
'event_source_url' => 'https://tulanding.com/producto-x',
'user_data' => [
'em' => [hash('sha256', strtolower(trim($buyer['email'])))],
'ph' => [hash('sha256', preg_replace('/[^0-9]/', '', $buyer['checkout_phone']))],
'fn' => [hash('sha256', strtolower(explode(' ', $buyer['name'])[0]))],
],
'custom_data' => [
'value' => $purchase['price']['value'],
'currency' => $purchase['price']['currency_value'],
'content_name' => 'Producto X',
]
];
// Envía a Meta CAPI
$ch = curl_init("https://graph.facebook.com/v19.0/{$pixel_id}/events");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'data' => [$event],
'access_token' => META_ACCESS_TOKEN
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
// Responde 200 a Hotmart
http_response_code(200);
echo 'OK';
Cómo testear antes de subir a producción
- Accede a tu landing con UTMs fake:
?utm_source=test&utm_campaign=debug; - Abre DevTools → Application → Local Storage. Verifica si
trakvo_attributionfue guardado; - Haz clic en el botón "Comprar". Verifica si la URL del checkout tiene los UTMs/sck (inspecciona el href del link);
- Completa una compra de prueba (Hotmart y Kiwify tienen modo sandbox);
- Verifica si el webhook fue accionado (log de tu servidor);
- En Meta Events Manager → Test Events: verifica si el Purchase apareció con los campos correctos;
- El EMQ del evento debería estar 6+ (con email + phone hasheados).
FAQ
¿Cuál es la diferencia entre sck de Hotmart y UTM normal?
sck (sale checkout key) es un parámetro propietario de Hotmart que acepta hasta 80 caracteres. UTM normal (utm_source, utm_medium, etc.) también funciona, pero sck es más flexible porque puedes codificar varias informaciones en él (ej.: sck=meta_ads-campaign123-adset456-ad789).
¿Kiwify soporta UTMs nativamente?
Sí. Kiwify acepta utm_source, utm_medium, utm_campaign, utm_content y utm_term directamente en la URL del checkout. Aparecen en la pestaña "Cliente" de cada pedido y quedan disponibles en el webhook de venta aprobada.
¿Necesito webhook si ya paso UTM?
No estrictamente, pero el webhook server-side es lo que cierra el loop completo. Sin él, sabes los UTMs en Hotmart/Kiwify pero Meta nunca recibe el evento Purchase vía CAPI — la atribución queda rota del lado del anuncio.
¿El UTM expira? ¿Cuánto tiempo dura?
Sesión del navegador. Por eso, captura el UTM inmediatamente cuando el usuario llega a la landing (vía JavaScript en el header) y persístelo en localStorage o cookie por 30-90 días. Si no, si el usuario vuelve mañana, el UTM se pierde.
¿Hay solución para Eduzz y PerfectPay también?
Sí, la misma lógica vale. Eduzz acepta utm_source en el checkout y tiene webhook. PerfectPay tiene parámetro custom similar al sck de Hotmart. Cartpanda y Yampi también — todos los gateways modernos soportan esta propagación.
Trakvo se encarga de esto automáticamente
Conecta Hotmart/Kiwify a Trakvo vía OAuth y listo — UTMs propagados, webhooks integrados, CAPI sincronizada. Sin código.
Hablar con el equipo