Skip to main content
O club cadastra um endpoint — uma URL do seu sistema — e escolhe os eventos que quer receber. Cada vez que um deles acontece, a API faz um POST assinado para essa URL e insiste até receber um 2xx. É a volta do webhook de entrada: lá a plataforma de venda avisa o club; aqui o club avisa você. O formato segue o Standard Webhooks, então as bibliotecas oficiais do padrão conferem a assinatura sem código nosso.

A requisição

O envelope

Todo evento tem a mesma casca:
event_types é uma lista explícita, sem coringa: um endpoint recebe só os tipos que escolheu, e um tipo novo que entrar no catálogo nunca começa a sair para quem não o pediu.
O documento do aluno (CPF) não sai em evento nenhum.

Verificar a assinatura

A assinatura é um HMAC-SHA256 sobre {webhook-id}.{webhook-timestamp}.{corpo}. A chave é o segredo sem o prefixo whsec_, decodificado de base64; o resultado vai em base64 depois de v1,. Três cuidados que valem para qualquer linguagem:
  • Assine o corpo cru, os bytes exatos que chegaram, antes de qualquer parse. Um JSON lido e serializado de novo muda espaço, ordem ou escape de acento, e a assinatura deixa de conferir.
  • Aceite qualquer uma das assinaturas do cabeçalho: na troca de segredo vêm duas.
  • Recuse o que chega velho: timestamp com mais de 5 minutos de diferença do seu relógio. Como o timestamp entra na assinatura, uma requisição capturada não pode ser repetida depois.
Compare a assinatura em tempo constante (hash_equals, hmac.Equal, hmac.compare_digest). Uma comparação comum de string para no primeiro byte diferente, e o tempo da resposta vaza quanto da assinatura estava certo.

O segredo

O segredo (whsec_…) aparece uma única vez: na resposta de Cadastrar endpoint e na de Trocar segredo. Perdeu, troque. Na troca, o segredo anterior continua assinando por 24 horas: nesse intervalo cada entrega leva as duas assinaturas, e o seu lado aceita a que conferir. Atualize o segredo do seu lado dentro desse prazo e nenhuma entrega fica sem conferir no meio do caminho.

Responder

Responda 2xx rápido e processe depois. Cada tentativa tem 15 segundos de prazo; grave o evento numa fila sua e responda, em vez de fazer o trabalho dentro da requisição.
  • Qualquer resposta fora de 2xx conta como falha, inclusive 4xx: um 401 depois de trocar o segredo ou um 404 de um deploy pela metade se consertam do seu lado, e a entrega volta a tentar.
  • Redirecionamento (3xx) não é seguido. Cadastre a URL final.
  • Retry-After em 429 e 503 é respeitado, até 10 horas: ele alonga a espera até a próxima tentativa, nunca a encurta.

Novas tentativas

Uma entrega tem 8 tentativas no total. Depois de cada falha, a próxima espera: Cada espera varia ±10%, para mil entregas que falharam juntas não voltarem juntas. São cerca de 27 horas entre a primeira e a última tentativa; esgotadas as oito, a entrega fica failed. Reenviar entrega tenta agora, com o mesmo corpo e o mesmo webhook-id, e o efeito depende do estado da entrega:
  • Fechada (succeeded, failed ou canceled): ganha uma tentativa manual. Falhando, volta a failed, sem voltar para a agenda.
  • pending: a próxima tentativa só é antecipada, e a entrega continua na agenda.

Entrega pelo menos uma vez, e sem ordem

A mesma mensagem pode chegar mais de uma vez — a sua resposta se perdeu na rede, alguém reenviou pelo painel. Deduplique pelo webhook-id: guarde os ids já processados e ignore o repetido. Não há garantia de ordem. Uma falha numa tentativa faz o evento seguinte chegar antes. Não decida pelo tipo da última mensagem que chegou: use o occurred_at e o estado do recurso que vem no data. O corpo é montado segundos depois do fato e traz o recurso como está naquele momento — num access.revoked, data.enrollment.status já é revoked.

O disjuntor

Um endpoint que só falha não deve receber rajadas:
  • A partir de 5 falhas seguidas, o endpoint passa a receber uma tentativa por vez, com intervalos que começam em 1 minuto e dobram até 1 hora (hold_until).
  • Qualquer 2xx zera o disjuntor.
  • Cinco dias sem nenhuma entrega aceita desligam o endpoint (status: disabled, disabled_reason: failing). A fila dele é cancelada e quem administra o club recebe um e-mail.
O que uma pessoa pede não espera o disjuntor: Enviar teste, Reenviar entrega e Recuperar entregas tentam na hora, e a tentativa serve de sonda. Deu certo, o disjuntor zera; falhou, ele arma de novo. Consertado o seu lado, religue o endpoint (status: enabled), o que zera o disjuntor, e chame Recuperar entregas: o que falhou, ou foi cancelado porque o endpoint estava desligado, desde a data informada — até 30 dias atrás — volta para a fila, com uma tentativa cada. O que foi cancelado pela remoção de um aluno ou por expirar não volta: o corpo dele não existe mais.

Estados do endpoint

Pausar é para uma manutenção do seu lado, em que nada pode se perder. Desligar é para parar de receber.

O endereço

  • Precisa ser https e resolver para a internet pública. Rede privada, localhost e IPs reservados são recusados no cadastro — e de novo a cada conexão, contra o IP que o DNS devolveu naquela hora.
  • Não há IP fixo de saída: não dá para liberar a entrega por lista de IPs. A assinatura é o que prova que a mensagem veio do club.
  • Até 20 endpoints por club.
  • Cadastrar e gerir endpoints exige permissão de administração do club: o histórico de entregas leva nome e e-mail de alunos.

Testar

Enviar teste cria uma entrega webhook.test que passa pelo mesmo caminho dos eventos reais: assinatura, conferência do endereço, novas tentativas. O teste que passa prova o que vai acontecer com os eventos de verdade. Só funciona com o endpoint enabled, e não espera o disjuntor: é o jeito de conferir, na hora, que o seu lado voltou.

Retenção

Depois que o corpo é apagado, a entrega continua no histórico com payload_purged, e não há mais o que reenviar. O prazo do corpo não tem exceção. Uma entrega que fica pendente mais de 30 dias — o caso é o endpoint pausado e esquecido — é cancelada com cancel_reason: expired, para o corpo sair no prazo como todos os outros.

Remoção de aluno

Quando um aluno é removido do club (LGPD):
  • sai student.removed, só com o id — o sinal para apagar a pessoa do seu sistema também;
  • os corpos das mensagens anteriores que falavam dela são apagados;
  • o que ainda não tinha saído é cancelado (cancel_reason: student_removed);
  • eventos daquele aluno gravados depois da remoção não saem.