📊 Atribución

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.

📌 Respuesta directa

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

  1. Capturar los UTMs en la landing (en el momento que el usuario llega);
  2. Propagar los UTMs al link del checkout (en el momento del clic);
  3. 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';
💡 Pro tip: guarda los UTMs en tu DB junto con el pedido. Así puedes correr reportes propios después (ROAS por campaña, por anuncio, por audience) sin depender solo de Meta.

Cómo testear antes de subir a producción

  1. Accede a tu landing con UTMs fake: ?utm_source=test&utm_campaign=debug;
  2. Abre DevTools → Application → Local Storage. Verifica si trakvo_attribution fue guardado;
  3. Haz clic en el botón "Comprar". Verifica si la URL del checkout tiene los UTMs/sck (inspecciona el href del link);
  4. Completa una compra de prueba (Hotmart y Kiwify tienen modo sandbox);
  5. Verifica si el webhook fue accionado (log de tu servidor);
  6. En Meta Events Manager → Test Events: verifica si el Purchase apareció con los campos correctos;
  7. 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
Asistente Trakvo
Respuesta al instante