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.
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
- Capturar os UTMs na landing (no momento que o usuário chega);
- Propagar os UTMs pro link do checkout (no momento do clique);
- 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';
Como testar antes de subir pra produção
- Acessa sua landing com UTMs fake:
?utm_source=teste&utm_campaign=debug; - Abre DevTools → Application → Local Storage. Confere se
trakvo_attributionfoi salvo; - Clica no botão "Comprar". Confere se a URL do checkout tem os UTMs/sck (inspeciona o href do link);
- Completa uma compra de teste (Hotmart e Kiwify têm modo sandbox);
- Confere se o webhook foi acionado (log do seu servidor);
- No Meta Events Manager → Test Events: confere se o Purchase apareceu com os campos certos;
- 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