Pular para o conteúdo
LeztyPay docs
Painel

Webhooks

Uma cobrança PIX nasce pending e muda de estado sozinha, minutos ou dias depois, quando o cliente paga ou o prazo acaba. Quem decide isso é o banco do pagador, não a sua aplicação e nem a nossa API. O webhook é o jeito de você ficar sabendo no instante em que acontece.

Ficar consultando a cobrança em laço funciona mal em todos os aspectos que importam: gasta o seu rate limit, atrasa a confirmação para o cliente na proporção do intervalo de polling e fica mais caro exatamente quando você vende mais. Polling é bom como rede de segurança (ver idempotência e ordem), não como caminho principal.

Dois destinos possíveis

Endpoint da conta postback_url da cobrança
Onde se configura uma vez, no painel ou pela API campo na criação de cada cobrança
O que recebe os tipos de evento que ele assina só os eventos daquela cobrança
Segredo um por endpoint, rotacionável o segredo de postback da store
Quantos até 5 ativos por ambiente um por cobrança

Os dois recebem o mesmo envelope, assinado do mesmo jeito. Se a cobrança tem postback_url e a conta tem endpoint assinando o tipo, os dois recebem: são duas entregas independentes, cada uma com o seu próprio ciclo de retentativas.

Use o endpoint da conta como regra. O postback_url existe para o caso em que cada cobrança pertence a um destino diferente (um marketplace com um webhook por lojista, por exemplo).

Cadastrar um endpoint

POST /v1/me/webhook-endpoints (na referência), com sessão do painel e verificação em duas etapas recente. Chave de API não cria endpoint: é configuração de conta, não operação do dia a dia.

A url precisa passar na mesma checagem que roda de novo em toda entrega:

  • esquema https e porta 443;
  • host que resolve para endereço público, nunca IP literal, nunca localhost, nunca faixa privada (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, 100.64/10, fc00::/7 e afins);
  • sem usuário e senha embutidos na URL;
  • sem redirecionar: um 3xx na entrega conta como falha, não é seguido.

Em events vai a lista de tipos que aquele endpoint assina, do catálogo de eventos, ou ["*"] para todos. Tipo que não existe no catálogo é recusado com invalid_event_type. O ping não é assinável: ele só é entregue quando você pede o teste, e chega em qualquer endpoint.

São no máximo 5 endpoints ativos por ambiente (live e test contam separado). O sexto volta 409 invalid_state. Remover um endpoint é soft delete: ele some da listagem e do fan-out, e o id passa a devolver 404.

O segredo aparece uma vez

A criação devolve secret, no formato whsec_ seguido de 43 caracteres. É a única resposta em que ele trafega. Depois disso a API só mostra secret_prefix, 6 caracteres que servem para você saber qual segredo está em uso, e nunca o valor inteiro. Guarde no seu gerenciador de segredos na hora, como faria com uma chave de API.

Perdeu, vazou ou o funcionário que tinha acesso saiu: rotacione em POST /v1/me/webhook-endpoints/{id}/rotate-secret (na referência). A resposta traz o segredo novo e previous_secret_expires_at, 24 h à frente. Nessa janela o antigo continua valendo e toda entrega sai com duas assinaturas, para você trocar a configuração do seu receptor sem derrubar nenhum evento. Como escrever a verificação para aceitar as duas está em assinatura.

O segredo do postback_url é da store inteira e só reaparece em POST /v1/me/postback-secret/rotate, também uma vez só. Ele não tem janela de graça: rotacionar invalida o anterior na hora, então troque o seu lado primeiro se puder.

O que chega no seu servidor

Um POST com Content-Type: application/json, User-Agent: LeztyPay-Webhooks/1.0 e estes headers:

Header Para que serve
X-Signature t=<unix>,v1=<hex>, prova de que o corpo veio de nós. Ver assinatura
X-Event-Id evt_…, identidade do evento. É por ele que você deduplica
X-Event-Type o tipo, igual ao type do corpo
X-Delivery-Id whd_…, identidade desta tentativa, diferente a cada retentativa
X-Attempt número da tentativa, começando em 1
Idempotency-Key mesmo valor de X-Event-Id, para quem já tem middleware de idempotência

O corpo é sempre o mesmo envelope, qualquer que seja o tipo:

{
  "id": "evt_01j9x5k8p2q3r4s5t6u7v8w9xy",
  "object": "event",
  "type": "transaction.paid",
  "mode": "live",
  "created_at": "2026-09-10T12:04:31Z",
  "data": {
    "object": { "id": "txn_01j9x5k8p2q3r4s5t6u7v8w9xy", "object": "transaction", "status": "paid" }
  }
}

O data.object acima está encurtado: ele traz o recurso inteiro, com todos os campos que a consulta dele devolve.

data.object é o recurso do momento em que o evento aconteceu, no mesmo formato que a consulta daquele recurso devolve: para transaction.* é o objeto de Consulta uma cobrança, para withdrawal.* o de Consulta um saque e para seller.* o de Perfil do seller da sessão (documento sempre mascarado). Nunca vem dado de outro seller.

mode diz de qual ambiente veio. Um endpoint cadastrado em test só recebe evento de test.

O que o seu receptor tem que fazer

  1. Ler o corpo cru antes de qualquer parser. A assinatura cobre os bytes exatos que chegaram.
  2. Verificar a assinatura e recusar com 401 se não bater.
  3. Responder 2xx em menos de 10 s. Depois disso a conexão é cortada e a entrega conta como falha. Grave o evento numa fila e processe fora do ciclo da requisição.
  4. Deduplicar por X-Event-Id. O mesmo evento pode chegar mais de uma vez.
  5. Ignorar tipo de evento que você não conhece respondendo 2xx. Tipo novo é mudança aditiva e pode aparecer a qualquer momento.

Qualquer 2xx encerra a entrega com sucesso. 3xx não: ver retries.