Cliente gerou o PIX às 14h. Não pagou. Ninguém falou com ele. Às 16h o código expirou e a venda evaporou. Você nem soube que ela existiu.
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 em tempo real. Você só não estava escutando o evento certo.
O webhook da Hotmart é o cano por onde essas informações chegam. Reagir bem a cada evento é a diferença entre uma operação que recupera venda no automático e uma que só olha o dashboard no fim do dia pra ver o estrago.
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. Reagir bem é fazer quatro coisas: identificar o evento certo, confiar só se o hottok bater, não processar o mesmo evento duas vezes, e disparar a régua do estado. O resto do texto é o detalhe de cada uma.
Webhook Hotmart eventos: o mapa de estados
Antes dos nomes, entenda 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.
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.
- 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
Dois 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. 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.
Como saber que o webhook é mesmo da Hotmart
Sua URL de webhook é pública. Qualquer um que descobrir o endereço pode mandar um POST falso dizendo "PURCHASE_APPROVED, libera o acesso". A Hotmart resolve isso com o hottok: um token único da sua conta. Na versão 2.0, ele chega no header X-HOTMART-HOTTOK. A regra é uma linha: se o hottok do header não for exatamente igual ao da sua configuração, você responde 200 e descarta, sem processar nada.
if request.header("X-HOTMART-HOTTOK") != MEU_HOTTOK:
return 200 # responde ok, mas ignora
processa(payload)
Guarde o hottok como segredo de ambiente, nunca no código versionado. E responda 200 rápido mesmo pro que você descarta: webhook que recebe erro ou timeout entra em fila de retentativa, e aí o mesmo evento volta várias vezes (o que nos leva ao próximo problema).
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. 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). Quem só reage ao aprovado ignora a maior fila de recuperação que existe.
- 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, retentativa vira mensagem duplicada e cliente irritado.
- Não validar o hottok. Sem a checagem do header, qualquer POST forjado libera acesso ou dispara régua. Já vi liberarem curso de graça por isso.
- 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. Se a sua régua "sumiu", o primeiro lugar pra olhar é se o POST está mesmo batendo no seu servidor. 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. Quem só ouve "aprovado" opera no escuro pra tudo que dá errado antes e depois da venda. Quem mapeia os dez eventos, valida o hottok, garante idempotência e liga cada estado numa régua específica recupera venda enquanto dorme. 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.