Cliente gerou o PIX às 14h. Não pagou. Ninguém falou com ele. Às 16h o código expirou e a venda evaporou. Os eventos do webhook Hotmart contaram essa história inteira em tempo real, e do seu lado ninguém estava ouvindo.
Cartão recusado por limite. Do lado da Hotmart isso é um estado bem definido. Do seu lado, é silêncio. O cara ia comprar, o banco travou, e ninguém mandou "tenta de novo com outro cartão ou PIX". Perdeu.
Pior: teve gente que pediu reembolso três semanas atrás e você continua mandando conteúdo de boas-vindas pra ela, porque a sua automação nunca ouviu falar de reembolso. Cada um desses momentos é um evento que a Hotmart te avisou. Você só não estava escutando o evento certo.
Só que antes de reagir a qualquer evento tem uma pergunta que vem primeiro: esse POST veio mesmo da Hotmart? Quem responde é o hottok, o token que chega no header X-HOTMART-HOTTOK. É a parte que mais gente procura e a que mais gente implementa errado, então ela vem logo depois da resposta curta, antes do mapa de eventos.
A resposta curta
A Hotmart dispara um POST (payload versão 2.0.0) pra uma URL sua toda vez que o status de uma transação muda. O corpo é um JSON com um campo event (o nome do evento, tipo PURCHASE_APPROVED) e um campo data com os detalhes da compra. No header vem o X-HOTMART-HOTTOK, um token fixo da sua conta: se ele não for igual ao que você tem guardado, o POST não é confiável e não pode disparar nada. Reagir bem é fazer quatro coisas, nessa ordem: validar o hottok, não processar o mesmo evento duas vezes, identificar o evento certo e disparar a régua daquele estado. O resto do texto é o detalhe de cada uma.
Hottok da Hotmart: o que é, onde fica e como validar
O hottok é um token de verificação. Cada conta da Hotmart tem o seu, único, e a Hotmart manda esse mesmo valor em toda notificação de webhook daquela conta. A lógica é de segredo compartilhado: só você e a Hotmart conhecem o valor, então um POST que chega com ele foi mandado por quem conhece o segredo. Ele não é assinatura do corpo (não é HMAC), não é o client id ou client secret da API de vendas, e não muda de um evento pro outro.
- Onde encontrar: na configuração de webhook da Hotmart (em Ferramentas, no card de Webhook), na parte de autenticação. O painel da Hotmart muda de lugar de tempos em tempos; se o menu não bater com o que você está vendo, procure o rótulo "Hottok" dentro da tela de webhook.
- Onde ele chega: na versão 2.0.0 do webhook, no header HTTP
X-HOTMART-HOTTOK. A documentação de developers da Hotmart recomenda validar esse header antes de processar qualquer dado do corpo. - Onde ele não chega: no JSON do evento. Se você achou tutorial mandando ler um campo
hottokdo corpo, provavelmente era a integração com o Hotmart Send, que é o caminho inverso: lá é a sua plataforma que manda dado pra Hotmart, e a central de ajuda pede o hottok no corpo da requisição. Mesmo nome, direção oposta.
A validação em si é uma comparação de string. O que derruba gente é o entorno dela. Em Node com Express:
const crypto = require('crypto')
// um ou mais hottoks válidos, separados por vírgula (um por conta Hotmart)
const HOTTOKS = (process.env.HOTMART_HOTTOKS || '')
.split(',').map(t => t.trim()).filter(Boolean)
function hottokValido(recebido) {
if (!recebido) return false
const a = Buffer.from(recebido.trim())
return HOTTOKS.some(t => {
const b = Buffer.from(t)
// timingSafeEqual lança erro se os tamanhos forem diferentes
return a.length === b.length && crypto.timingSafeEqual(a, b)
})
}
app.post('/webhook/hotmart', express.json(), async (req, res) => {
const hottok = req.get('X-HOTMART-HOTTOK') // req.get não liga pra maiúscula
if (!hottokValido(hottok)) {
await quarentena.salvar(req.body) // guarda, mas NÃO age
await alerta('webhook Hotmart: hottok ausente ou divergente')
return res.sendStatus(200)
}
await fila.gravar(req.body) // grava rápido...
res.sendStatus(200) // ...responde, e processa depois
})
Quatro detalhes que esse trecho resolve e que eu já vi quebrar em produção:
- Maiúscula e minúscula. Nome de header HTTP não diferencia caixa, mas o seu código pode diferenciar. O Node guarda tudo em minúsculo:
req.headers['X-HOTMART-HOTTOK']voltaundefined,req.headers['x-hotmart-hottok']funciona. No n8n, o nó Webhook entrega em$json.headers['x-hotmart-hottok'], minúsculo. Em PHP o header aparece como$_SERVER['HTTP_X_HOTMART_HOTTOK']. Boa parte dos "meu hottok não bate" que eu já debuguei era o código procurando a chave com a caixa errada e comparandoundefinedcom o token. - Espaço e aspas. Hottok colado no
.envcom espaço no fim, quebra de linha, ou entre aspas que o seu loader não remove. Otrimdos dois lados resolve. - Mais de uma conta. Coprodução, conta de teste, segunda conta de produtor apontando pra mesma URL: cada conta tem o próprio hottok. Por isso o código aceita uma lista em vez de um valor só.
- Comparação em tempo constante.
timingSafeEqualem vez de===. Ataque de tempo contra webhook é improvável na prática, mas custa uma linha e tira a dúvida.
E se o hottok não bater?
Primeiro descubra qual dos três casos é, porque cada um tem causa diferente:
- Header ausente. O POST provavelmente não é da Hotmart (scanner, bot, alguém testando a sua URL), ou é uma configuração antiga de webhook apontando pro mesmo endpoint. Olhe o campo
versiondo corpo: o envelope atual vem com"version": "2.0.0". - Header presente e diferente. Quase sempre é você: token de outra conta, token com espaço, ou variável de ambiente que não subiu no deploy (a lista fica vazia e nada bate). Pra comparar, logue só os 4 últimos caracteres do recebido e do esperado. Nunca logue o token inteiro.
- Bate no teste, falha em produção. Confira se o que está na frente da aplicação (Traefik, nginx, Cloudflare, gateway de API) repassa o header. Por padrão repassam; costuma quebrar quando alguém montou regra de header customizada ou um middleware que reescreve a requisição.
Depois decida o código de resposta pro POST rejeitado. Tem dois jeitos defensáveis, e eles têm consequências diferentes:
- 401. Semanticamente é o correto, e é o que o nó comunitário de Hotmart pro n8n faz quando a checagem de token está ligada. Pra POST forjado de verdade, perfeito.
- 200 com quarentena. É o que eu uso. A Hotmart diz que retenta notificação com erro até 5 vezes, e a central de ajuda avisa que, se a URL cadastrada retornar erro, a Hotmart desativa a configuração automaticamente (o texto não diz depois de quantos erros). Se o hottok divergente for culpa sua, e quase sempre é, responder 401 transforma um erro de configuração em retentativa em série e no risco de o webhook inteiro ser desligado, levando junto os eventos válidos. Com 200 e quarentena, o evento fica guardado, você recebe o alerta, corrige o token e reprocessa da quarentena. Como rede de segurança, o histórico do webhook na Hotmart guarda os eventos por até 60 dias e permite reenvio manual.
A regra que não muda em nenhum dos dois: evento sem hottok válido nunca libera acesso e nunca dispara mensagem. Quarentena é depósito, não fila de processamento. E lembre que o hottok é um segredo estático: se ele vazar (log com headers crus, print no grupo de suporte, commit no repositório), qualquer um forja um PURCHASE_APPROVED. Pra produto caro, vale uma segunda trava antes de liberar acesso: consultar aquela transação na API de vendas da Hotmart e conferir se o status bate com o que o webhook disse.
Webhook Hotmart eventos: o mapa de estados
Com o hottok validado, vem o formato. Na versão 2.0.0 do webhook, todo evento chega com o mesmo envelope. Muda só o que vem dentro de data. Um payload de compra aprovada, mascarado, é mais ou menos assim:
{
"id": "e8f9a0b1-2c3d-4e5f-6789-0abcdef12345",
"creation_date": 1737120000000,
"event": "PURCHASE_APPROVED",
"version": "2.0.0",
"data": {
"product": {
"id": 0000000,
"name": "Seu Produto"
},
"buyer": {
"email": "c****@email.com",
"name": "Nome Mascarado",
"checkout_phone": "+55XXXXXXXXXXX"
},
"purchase": {
"transaction": "HP00000000000000",
"status": "APPROVED",
"approved_date": 1737120000000,
"payment": {
"type": "PIX",
"installments_number": 1
},
"price": {
"value": 197.00,
"currency_value": "BRL"
}
},
"affiliates": [],
"commissions": []
}
}
Os campos que você vai usar sempre: event (o que aconteceu), data.purchase.transaction (o ID único da compra, sua chave), data.purchase.status (o estado atual da transação) e data.buyer (pra quem falar). O id lá em cima é o ID do evento em si, e ele importa pra idempotência, que a gente vê mais pra frente.
Um detalhe que aparece muito em busca: o "currency_value": "BRL" dentro de price é a moeda daquele valor, e o value vem como número decimal na unidade da moeda (197.00 é R$ 197,00). Se você vende pra fora ou tem oferta em dólar, agregue faturamento sempre olhando a moeda junto. Somar value de BRL com USD é o jeito mais rápido de o seu dashboard mentir.
Os eventos, um por um, e o que cada um quer dizer
Esses são os eventos de compra do webhook 2.0.0. Não invente nome, use exatamente estes:
- PURCHASE_OUT_OF_SHOPPING_CART: abandono de carrinho. O cara preencheu dados no checkout e não terminou. Ainda não é venda, é intenção quente.
- PURCHASE_BILLET_PRINTED: boleto gerado ou PIX gerado, aguardando pagamento. Dinheiro na mesa, não pago ainda. É o evento mais subestimado da lista.
- PURCHASE_APPROVED: pagamento aprovado. A grana caiu (ou o cartão passou). Aqui começa a entrega, e é nesse payload que vem o xcod usado na atribuição de venda Hotmart no Meta Ads.
- PURCHASE_COMPLETE: compra concluída, já fora do prazo de garantia/reembolso imediato. Estado "consolidado". Não confunda com APPROVED.
- PURCHASE_CANCELED: compra cancelada. Repara na grafia: o evento é CANCELED com um L só, mas o status da transação é CANCELLED com dois L. A Hotmart escreve diferente nos dois lugares, e isso já derrubou muita comparação de string.
- PURCHASE_REFUNDED: reembolso efetivado. Você já devolveu o dinheiro. Tem que cortar acesso e parar toda régua.
- PURCHASE_CHARGEBACK: chargeback. O cliente contestou no banco/operadora do cartão. Diferente de reembolso: aqui tem risco de fraude e o dinheiro volta pela força.
- PURCHASE_PROTEST: disputa/protesto aberto, um passo antes do chargeback virar definitivo.
- PURCHASE_DELAYED: pagamento atrasado (comum em assinatura que não renovou na data).
- PURCHASE_EXPIRED: o boleto ou PIX venceu sem pagamento. A intenção da PURCHASE_BILLET_PRINTED morreu aqui.
E os de assinatura, se você vende recorrência: SUBSCRIPTION_CANCELLATION (assinatura cancelada), SWITCH_PLAN (troca de plano) e UPDATE_SUBSCRIPTION_CHARGE_DATE (mudança na data de cobrança). Tem ainda os de área de membros (CLUB_FIRST_ACCESS, CLUB_MODULE_COMPLETED), que servem mais pra engajamento do que pra recuperar venda.
Status da transação x nome do evento (não confunda)
Tem gente que roteia a régua pelo event e tem gente que roteia pelo data.purchase.status. Os dois existem e não são a mesma coisa. O event é a notificação ("mudou pra isso agora"). O status é o estado atual da transação. Os valores possíveis de status, na doc atual da Hotmart, são:
APPROVED BLOCKED CANCELLED
CHARGEBACK COMPLETE EXPIRED
NO_FUNDS OVERDUE PARTIALLY_REFUNDED
PRE_ORDER PRINTED_BILLET PROCESSING_TRANSACTION
PROTESTED REFUNDED STARTED
UNDER_ANALISYS WAITING_PAYMENT
Três detalhes que economizam horas: NO_FUNDS é a recusa por saldo/limite insuficiente (a "recusa de cartão" que você quer recuperar) e WAITING_PAYMENT é o PIX/boleto aguardando. Reembolso aparece de dois jeitos: o evento PURCHASE_REFUNDED e o status REFUNDED, ou PARTIALLY_REFUNDED quando foi parcial (trate os dois como "cortar régua de venda"). E sim, UNDER_ANALISYS está escrito assim mesmo na Hotmart, com o erro de grafia. Copie exatamente, não "corrija" pra ANALYSIS, senão seu if nunca casa.
Idempotência: o mesmo evento vai chegar mais de uma vez
Webhook não é entrega garantida uma vez só. É entrega garantida pelo menos uma vez. Se sua resposta demorou, se deu um 500, se a rede engasgou, a Hotmart reenvia (até 5 tentativas, pelo que ela mesma documenta), e você ainda pode reenviar na mão pelo histórico. Se você não se proteger, o cliente recebe a mesma mensagem de "seu PIX está esperando" três vezes, e você parece um robô quebrado.
A defesa é idempotência: registre o id do evento (ou a dupla transaction + event) numa tabela antes de agir. Se já viu aquele id, ignora e responde 200.
-- pseudo-lógica
INSERT INTO eventos_vistos (evento_id) VALUES ($id)
ON CONFLICT (evento_id) DO NOTHING;
-- se não inseriu nada, é repetido: para aqui.
Sem isso, todo pico de tráfego (lançamento, dia de PIX em massa) vira disparo duplicado. É o erro que mais queima lista.
O que dá errado na prática
Os furos que eu mais vejo em operação de infoproduto na Hotmart:
- Escutar só PURCHASE_APPROVED. É o evento bonito, o do "venda!". Só que o dinheiro que você deixa na mesa está no PURCHASE_BILLET_PRINTED (PIX gerado e não pago), e a recuperação de PIX pendente tem régua própria. Quem só reage ao aprovado ignora a maior fila de recuperação que existe, o material bruto de qualquer recuperação com n8n bem montada.
- Tratar recusa igual a abandono. NO_FUNDS (cartão recusado) e PURCHASE_OUT_OF_SHOPPING_CART (nem chegou a pagar) são pessoas em momentos diferentes. Quem foi recusado já decidiu comprar e travou no meio. A mensagem certa é "seu cartão não passou, tenta PIX", não "ainda tá pensando?".
- Processar o mesmo evento duas vezes. Já falamos: sem idempotência em webhook, retentativa vira mensagem duplicada e cliente irritado.
- Não validar o hottok, ou validar errado. Sem a checagem do header, qualquer POST forjado libera acesso ou dispara régua. Já vi liberarem curso de graça por isso. E o erro simétrico também acontece: checagem com a caixa errada do header, que rejeita todo evento legítimo e faz a régua "sumir" sem ninguém entender por quê.
- Continuar a régua depois de reembolso ou chargeback. Se PURCHASE_REFUNDED e PURCHASE_CHARGEBACK não cortam o fluxo, você fica mandando upsell pra quem já pediu o dinheiro de volta. Além de ridículo, aumenta a chance de mais chargeback.
- O webhook simplesmente não chegar. URL errada, endpoint respondendo 500, firewall barrando, ou a configuração desativada pela própria Hotmart depois de erro repetido. Se a sua régua "sumiu", o primeiro lugar pra olhar é se o POST está mesmo batendo no seu servidor, e o segundo é o histórico do webhook na Hotmart. Escrevi um checklist de debug disso em por que seu webhook do WhatsApp não dispara, e a lógica de diagnóstico é a mesma pra qualquer webhook.
A régua por estado: o que disparar pra cada evento
Aqui é onde o técnico vira dinheiro. Cada evento pede uma reação comercial diferente. Um mapa que funciona:
- PURCHASE_OUT_OF_SHOPPING_CART: mensagem leve de quebra de objeção em minutos. "Vi que você começou e não terminou, ficou alguma dúvida?"
- PURCHASE_BILLET_PRINTED (WAITING_PAYMENT): lembrete de PIX/boleto com o código pronto pra pagar, escalando conforme o prazo aperta. É a régua que mais paga a si mesma.
- NO_FUNDS (recusa): oferecer outra forma de pagamento na hora. "Seu cartão não passou, quer pagar no PIX?" com o link direto.
- PURCHASE_APPROVED: acesso liberado, boas-vindas e, se fizer sentido, o upsell.
- PURCHASE_EXPIRED / PURCHASE_DELAYED: última tentativa de reativar ou, em assinatura, aviso de renovação pendente.
- PURCHASE_REFUNDED / PURCHASE_CHARGEBACK / PURCHASE_PROTEST: cortar acesso, parar toda régua ativa e, no máximo, um pós-venda de retenção educado. Nada de oferta.
Quando essa régua sai no WhatsApp, que é onde o público brasileiro de infoproduto responde, vale decidir com cuidado por onde disparar. As opções e os trade-offs (bloqueio, custo, número oficial) estão em Evolution API vs Cloud API. E se você quiser saber de qual anúncio veio a venda que você recuperou, cuidado com a atribuição no meio do caminho: o motivo de o crédito se perder está em por que o UTM se perde no WhatsApp.
Fechando
O webhook da Hotmart não é integração de dev. É a lista de estados do seu funil chegando em tempo real. A ordem que não falha é: valida o hottok (com a caixa certa do header e quarentena pro que não bate), grava o evento antes de agir, ignora repetido, e só então liga cada estado numa régua específica. Quem só ouve "aprovado" opera no escuro pra tudo que dá errado antes e depois da venda. O código é a parte fácil. A decisão de qual mensagem cada estado merece é o que separa uma operação que fatura de uma que só assiste o dashboard.
Fontes consultadas em 10/11/2026: documentação de webhook 2.0.0 em developers.hotmart.com (envio do hottok no header X-HOTMART-HOTTOK e recomendação de validar antes de processar); blog oficial da Hotmart sobre HotConnect e Webhook (hottok único por conta, 5 retentativas em caso de erro, reprocessamento pelo histórico); central de ajuda da Hotmart sobre configuração de Webhook/Postback (retentativa até 5 vezes, desativação automática da configuração quando a URL retorna erro, histórico de 60 dias, reenvio manual) e sobre integração com o Hotmart Send (hottok no corpo, no sentido inverso).