📊 Atribuição

Como passar UTMs do site pro checkout do Hotmart e Kiwify

O maior problema de quem vende infoproduto: o checkout acontece num domínio externo (Hotmart, Kiwify, Eduzz) e os parâmetros UTM somem nesse pulo. Resultado: atribuição quebrada, ROAS errado, CAC inflado. Veja como resolver em 7 minutos.

📌 Resposta direta

Estratégia em 3 partes: (1) captura os UTMs da URL no JS da landing e persiste em localStorage; (2) ao clicar no botão de compra, anexa esses UTMs ao link do checkout (Hotmart aceita sck, Kiwify aceita utm_source/medium/campaign/content); (3) quando Hotmart/Kiwify dispara webhook de venda aprovada, seu servidor envia o evento Purchase pra Meta CAPI com os UTMs originais. Fecha o loop em 100%.

O buraco negro do checkout externo

Você roda Meta Ads pra uma landing page. O usuário clica, chega na landing com ?utm_source=meta&utm_medium=cpc&utm_campaign=produto_x. Tudo certo até aqui.

Aí ele clica no botão "Comprar agora", que leva pro checkout do Hotmart: pay.hotmart.com/A12345B?ref=.... Os UTMs somem nesse redirecionamento. O pixel da landing dispara ViewContent, mas o Purchase acontece no domínio do Hotmart — fora do escopo do pixel.

Resultado prático em uma operação típica de infoproduto:

  • Você gastou R$ 10k no Meta Ads na campanha X;
  • O Hotmart mostra que 80 vendas vieram nesse período;
  • O Meta atribui apenas 25 dessas vendas à campanha X (o resto ficou como "orgânico" ou "direto" porque os UTMs sumiram);
  • Você acha que a campanha X tem ROAS 2.5×, quando na verdade tem 8×.

Decide pausar a campanha por "performance ruim". Estava na verdade matando galinha dos ovos de ouro.

A estratégia: 3 partes que fecham o loop

  1. Capturar os UTMs na landing (no momento que o usuário chega);
  2. Propagar os UTMs pro link do checkout (no momento do clique);
  3. Atribuir via webhook quando a venda é aprovada (no servidor → Meta CAPI).

Cada parte é simples sozinha, mas você precisa das 3 funcionando juntas pra ter atribuição completa.

Parte 1: capturar UTMs na landing

JS no header da sua landing page (executa antes do usuário interagir):

// 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 dias
    const data = { ...captured, capturedAt: Date.now() };
    localStorage.setItem('trakvo_attribution', JSON.stringify(data));
    // Também cookie de fallback (caso localStorage seja limpo)
    document.cookie = `trakvo_attribution=${encodeURIComponent(JSON.stringify(data))}; max-age=${30 * 24 * 60 * 60}; path=/; SameSite=Lax`;
  }
})();

Por que persistir em localStorage E cookie? Porque alguns navegadores móveis limpam um ou outro. Dois redundantes = mais robusto.

Parte 2: propagar UTMs pro link do checkout

Pra Hotmart (usa parâmetro sck)

// quando user clica em "Comprar agora"
function buildHotmartCheckoutUrl(productCode) {
  const attribution = JSON.parse(localStorage.getItem('trakvo_attribution') || '{}');

  // sck aceita até 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 também aceita UTMs diretos:
  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');

Pra Kiwify (usa UTMs diretos)

function buildKiwifyCheckoutUrl(productSlug) {
  const attribution = JSON.parse(localStorage.getItem('trakvo_attribution') || '{}');

  const url = new URL(`https://pay.kiwify.com.br/${productSlug}`);

  // Kiwify aceita UTMs nativamente — passa direto
  ['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 guarda em metadata)
  if (attribution.fbclid) url.searchParams.set('fbclid', attribution.fbclid);

  return url.toString();
}

Parte 3: webhook server-side → Meta CAPI

Hotmart e Kiwify oferecem webhook quando a venda é aprovada. Configure isso no painel de cada um (Settings → Webhooks → Add endpoint).

Seu endpoint PHP receberá um POST tipo:

// Webhook do Hotmart (simplificado)
{
  "data": {
    "purchase": {
      "transaction": "HP12345...",
      "status": "APPROVED",
      "approved_date": 1716640000000,
      "checkout_country": { "iso": "BR" },
      "tracking": {
        "sck": "meta-campaign123-adset456-fbclid_abc",
        "utm_source": "meta",
        "utm_medium": "cpc",
        "utm_campaign": "produto_x"
      },
      "price": { "value": 297.00, "currency_value": "BRL" }
    },
    "buyer": {
      "email": "[email protected]",
      "name": "João da Silva",
      "checkout_phone": "+5511999998888"
    }
  }
}

No seu servidor, valide a assinatura HMAC do webhook, depois dispare CAPI:

// webhook-hotmart.php
$payload = json_decode(file_get_contents('php://input'), true);
$purchase = $payload['data']['purchase'];
$buyer = $payload['data']['buyer'];

// Decodifica sck pra recuperar UTMs originais
$sck_parts = explode('-', $purchase['tracking']['sck'] ?? '');
$utm_source = $sck_parts[0] ?? 'direct';
$utm_campaign = $sck_parts[1] ?? null;
$fbclid_short = $sck_parts[3] ?? null;

// Monta evento pra 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://sualanding.com.br/produto-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' => 'Produto X',
    ]
];

// Envia pra 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 pro Hotmart
http_response_code(200);
echo 'OK';
💡 Pro tip: guarde os UTMs na sua DB junto com o pedido. Assim consegue rodar relatórios próprios depois (ROAS por campanha, por anúncio, por audience) sem depender só do Meta.

Como testar antes de subir pra produção

  1. Acessa sua landing com UTMs fake: ?utm_source=teste&utm_campaign=debug;
  2. Abre DevTools → Application → Local Storage. Confere se trakvo_attribution foi salvo;
  3. Clica no botão "Comprar". Confere se a URL do checkout tem os UTMs/sck (inspeciona o href do link);
  4. Completa uma compra de teste (Hotmart e Kiwify têm modo sandbox);
  5. Confere se o webhook foi acionado (log do seu servidor);
  6. No Meta Events Manager → Test Events: confere se o Purchase apareceu com os campos certos;
  7. EMQ do evento deve estar 6+ (com email + phone hasheados).

FAQ

Qual a diferença entre sck do Hotmart e UTM normal?

sck (sale checkout key) é um parâmetro proprietário do Hotmart que aceita até 80 caracteres. UTM normal (utm_source, utm_medium, etc.) também funciona, mas o sck é mais flexível porque você pode codificar várias informações nele (ex.: sck=meta_ads-campaign123-adset456-ad789).

Kiwify suporta UTMs nativamente?

Sim. Kiwify aceita utm_source, utm_medium, utm_campaign, utm_content e utm_term direto na URL do checkout. Eles aparecem na aba "Cliente" de cada pedido e ficam disponíveis no webhook de venda aprovada.

Preciso de webhook se já passo UTM?

Não estritamente, mas webhook server-side é o que fecha o loop completo. Sem ele, você sabe os UTMs no Hotmart/Kiwify mas o Meta nunca recebe o evento Purchase via CAPI — atribuição fica quebrada do lado do anúncio.

O UTM expira? Quanto tempo dura?

Sessão do navegador. Por isso, capture o UTM imediatamente quando o usuário chega na landing (via JavaScript no header) e persista em localStorage ou cookie por 30-90 dias. Senão, se o usuário voltar amanhã, o UTM se perde.

Tem solução pra Eduzz e PerfectPay também?

Sim, a mesma lógica vale. Eduzz aceita utm_source no checkout e tem webhook. PerfectPay tem parâmetro customizado similar ao sck do Hotmart. Cartpanda e Yampi também — todos os gateways modernos suportam essa propagação.

Trakvo cuida disso automaticamente

Conecte Hotmart/Kiwify ao Trakvo via OAuth e pronto — UTMs propagados, webhooks integrados, CAPI sincronizada. Sem código.

Falar com o time
Assistente Trakvo
Respondo na hora