Skip to main content
Webhooks são como a Gravity Play avisa o seu sistema quando algo acontece — um pagamento confirmado, uma assinatura renovada, um estorno. Eles são a fonte da verdade: chegam mesmo que o cliente feche o navegador.

Duas formas de receber

Endpoints do painel

Cadastre URLs fixas em Integrações → Webhooks e escolha os eventos (e, se quiser, os produtos). Recebem todos os eventos que casarem.

postbackUrl da cobrança

Ao criar uma cobrança via API, informe um postbackUrl. Ele recebe os eventos daquela transação específica — somado aos endpoints do painel.

Eventos

Formato do payload

Toda entrega é um POST com corpo JSON neste formato:
Campos do data nos eventos payment.*:

O objeto salesRep

salesRep e salesRefCode aparecem em todo evento payment.* e subscription.* — nunca ficam ausentes, mesmo sem vendedor (nesse caso salesRep vem null).
Quando salesRep é null. Venda direta (sem link de vendedor), código de referência que não resolveu para nenhum vendedor, ou cadastro do vendedor removido — nesses três casos salesRep é null, e salesRefCode mantém o valor bruto recebido (ou também null, se não houve código). Um vendedor desativado continua aparecendo normalmente: desligar não muda a atribuição já gravada. Numa renovação de assinatura, o Payment do ciclo tem salesRep: null quando não tem atribuição própria — mesmo que a assinatura tenha vendedor. O subscription.renewed correspondente sempre traz o vendedor certo, porque vem da própria assinatura.

O objeto customer

Todos os eventos payment.* e subscription.* trazem o objeto customer com os dados que o comprador preencheu no checkout. Campos não coletados naquele fluxo vêm como null. O address, quando presente, tem o formato:
Quando cada campo existe. O endereço é coletado no cartão (Asaas/Pagar.me) e no boleto — em pagamentos PIX o checkout pede só os dados pessoais, então customer.address vem null. O telefone é coletado no cartão (gateways BR) e no PIX Pagar.me. Pagamentos anteriores a esta versão não têm os campos novos preenchidos (null).
Para eventos de uma cobrança via API, o data.metadata traz a metadata que você enviou na criação — use o invoiceId (ou o que for) para dar baixa.

O objeto offer

Todo evento cujo data tenha um offerId — pagamentos de checkout de oferta e todos os eventos subscription.* — vem com o objeto offer completo embutido, para você reagir aos dados comerciais (nome, preço, ciclo) sem uma chamada extra à API.
Cobranças avulsas não têm oferta. Eventos de cobranças criadas via POST /v1/charges (sem uma oferta) não trazem o objeto offer — nesses casos offerId é null. Use o data.metadata e o chargeId para reconciliar.
Vendas de link de pagamento. Um pagamento feito por um link de pagamento do painel dispara os mesmos eventos payment.* da cobrança avulsa, com paymentLinkId preenchido e sem produto nem oferta (productId e offerId vêm null). Por isso um endpoint filtrado por produto não recebe vendas de link. Para receber, use um endpoint sem filtro de produto. Com os dados do link e as tags, assine os eventos de links de pagamento.
Os eventos subscription.* usam um data menor (id, status, offerId, customerEmail, customer, currentPeriodEnd, salesRepId, salesRep, salesRefCode) — também com os objetos customer e offer embutidos. Aqui salesRepId/salesRep/salesRefCode vêm do vendedor gravado na PRÓPRIA assinatura (preenchido uma vez, no 1º ciclo, e nunca muda depois — por isso todo subscription.* traz o vendedor certo, mesmo quando o Payment de uma renovação não tem atribuição própria):
E os headers:

Eventos da área de membros

enrollment.created, enrollment.revoked, lesson.completed, course.completed, certificate.issued, enrollment.cohort_changed e enrollment.expired usam o mesmo envelope { id, event, createdAt, data }, a mesma assinatura e os mesmos headers dos eventos de pagamento. O data traz os mesmos objetos das rotas /v1 da área de membros: student, course (resumo), enrollment, lesson, progress (resumo) e certificate. Um parser só serve para o evento e para a consulta.
  • data.product é o produto do curso. O filtro por produto do endpoint usa esse produto. No curso comprado por order bump, vale o produto do próprio curso, e não o do item principal.
  • Esses eventos não trazem salesRep, salesRefCode, customer nem offer.
  • O data é o estado do momento do disparo. O retry automático e o reprocessamento manual reenviam o mesmo corpo. Um enrollment.created reprocessado depois do reembolso continua com status: "ACTIVE".
  • O aluno de pré-visualização do seller nunca gera evento.

Exemplos do data

Regras dos eventos

  • reactivated. Vem false na matrícula nova. Vem true quando uma compra ou uma liberação manual devolve o acesso a uma matrícula revogada ou com expiresAt já passado. A matrícula é a mesma, com o mesmo id. Na reativação por compra, enrollment.cohort continua a turma que a matrícula já tinha. Renovação de assinatura, reentrega do mesmo pagamento e nova liberação de um acesso ainda vivo não disparam enrollment.created.
  • lesson.completed sai uma vez. Sai na primeira conclusão da aula pelo aluno, pelo player ou pela aprovação no quiz. Batimentos simultâneos, quiz refeito e aula revista não geram outro. Batimento de progresso que não conclui a aula nunca gera evento.
  • course.completed pode repetir. Sai quando a conclusão de uma aula deixa progress.completedLessons igual a progress.totalLessons, com o certificado do curso ligado ou desligado. Cada conclusão do curso sai uma vez, mesmo quando o aluno conclui as últimas aulas ao mesmo tempo. Se o curso ganhar aulas e o aluno concluir todas de novo, sai outro. Curso sem aula contada nunca dispara.
  • Regra de totalLessons. Conta as aulas publicadas, de módulos publicados, que a turma da matrícula vê. percent é completedLessons / totalLessons × 100 arredondado, e 0 quando totalLessons é 0. completedAt é a conclusão mais recente entre as aulas contadas quando todas estão concluídas, e null nos demais casos. É a mesma conta de GET /v1/students/{id}/progress.
  • certificate.issued sai uma vez por certificado. Com certificado ligado, a conclusão que torna o aluno elegível emite o certificado na hora. Se a emissão falhar nesse momento, ela acontece no acesso seguinte do aluno à página ou ao PDF do certificado.
  • enrollment.cohort_changed sai na troca de turma. Sai quando o seller move a matrícula para outra turma do curso. previousCohort é a turma de antes, e enrollment.cohort é a turma nova. Mover para a turma em que o aluno já está não dispara. A compra que reativa uma matrícula revogada ou vencida não muda a turma: a matrícula volta na turma que já tinha, qualquer que seja a turma da oferta, e dispara só enrollment.created, sem enrollment.cohort_changed.
  • enrollment.expired sai uma vez por vencimento. Sai quando passa o expiresAt de uma matrícula ACTIVE, numa varredura horária, em até 1 hora. O data traz status: "ACTIVE" e hasAccess: false, porque o vencimento não muda o status. Se o expiresAt for adiado antes da varredura, o evento não sai. Se o aluno voltar a ter acesso e o acesso vencer de novo, sai outro. Vencimentos anteriores à chegada deste evento não geram aviso.
  • A ordem de chegada não é garantida. Um lesson.completed em retry pode chegar depois do enrollment.revoked da mesma matrícula. Use enrollment.status, enrollment.revokedAt e as datas do data, e não a ordem das entregas (veja Ordem de chegada).
Os eventos payment_link.* saem só para vendas de um link de pagamento do painel, no mesmo momento do payment.* par, e usam o mesmo envelope, assinatura, headers e retry. A venda de link continua saindo também como payment.*: assine um dos dois grupos para não processar a venda duas vezes. O data é o mesmo do payment.* (inclusive customer e salesRep) com mais dois objetos:
  • paymentLink: o link no instante do envio, com tags, url pública, useCount já contado e salesRep do link (null quando o link não tem vendedor).
  • charge: a cobrança filha criada pelo uso do link (id, status, amountInCents, currency, description, items, parcelas, customerName, customerEmail, datas).
payment_link.expired não tem par em payment.*: sai uma vez, quando o PIX, o boleto ou o cartão pendente do link vence sem pagamento e o uso volta para o link. O data.status vem EXPIRED. Se o pagamento vencido for pago depois, sai payment_link.paid normalmente.
payment_link.paid
As tags são as do link no momento do envio: editar as tags depois muda os próximos eventos, não os já entregues. Cada link tem até 20 tags de até 40 caracteres, sem repetição (maiúsculas e minúsculas contam como a mesma tag).

Validando a assinatura

Cada entrega é assinada com HMAC-SHA256. O header tem o formato t=<unix>,v1=<hmac>, e o HMAC é calculado sobre `${t}.${body}` usando o secret.
Qual secret usar? - Endpoints do painel: o signing secret exibido ao criar o endpoint. - postbackUrl: o postbackSecret retornado na resposta do POST /v1/charges daquela cobrança. Cada cobrança tem o seu.
Use o corpo cru. Valide a assinatura sobre o corpo exatamente como recebido (raw). Se você re-serializar o JSON, a assinatura não vai bater.

postbackUrl

Quando você cria uma cobrança com postbackUrl, todos os eventos daquela transação (payment.paid, payment.failed, payment.refunded) são entregues também nessa URL — além dos endpoints do painel. É ideal para reconciliar uma fatura sem precisar manter um endpoint fixo. O postbackSecret que valida essas entregas vem uma única vez, na resposta da criação da cobrança. Guarde-o junto com o id da cobrança/fatura.

Idempotência

A mesma entrega pode chegar mais de uma vez (ex.: no retry automático ou ao reprocessar). Use o X-GravityPay-Delivery (= id do payload) como chave de idempotência e ignore duplicatas. Toda tentativa da mesma entrega, automática ou manual, usa o mesmo id.

Resposta e re-tentativas

Responda 2xx rapidamente (idealmente em até alguns segundos) para confirmar o recebimento. Qualquer outro status, timeout ou erro de conexão é tratado como falha.

Retry automático

Quando a falha é temporária, a Gravity Play tenta de novo sozinha. Entram no retry automático:
  • timeout ou erro de conexão (DNS, conexão recusada);
  • status 429;
  • qualquer status 5xx.
Um 4xx diferente de 429 não é tentado de novo. Ele indica que o seu sistema recusou o payload, e repetir o mesmo envio não resolve. Depois da primeira tentativa, são até 6 tentativas automáticas, com estas esperas: No total são até 7 tentativas, em cerca de 33 horas. Entregas para a mesma URL mantêm um intervalo mínimo entre si, então um retry pode sair um pouco depois da espera da tabela. Se a 6ª tentativa automática também falhar, a entrega fica esgotada e não é tentada de novo sozinha. Ela continua no painel (Integrações → Webhooks → Entregas), com o histórico de cada tentativa, e você pode reprocessar manualmente a qualquer momento. O reprocessamento manual também cancela o retry automático que estiver agendado.

Ordem de chegada

Não dependa da ordem de chegada. Por causa do retry automático, um evento mais antigo pode chegar depois de um mais novo. Por exemplo, um payment.paid em retry pode chegar depois do payment.refunded do mesmo pagamento. Reconcilie pelo estado do payload (data.status e os demais campos de data), e não pela ordem em que as entregas chegam.