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
httpse 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::/7e afins); - sem usuário e senha embutidos na URL;
- sem redirecionar: um
3xxna 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
- Ler o corpo cru antes de qualquer parser. A assinatura cobre os bytes exatos que chegaram.
- Verificar a assinatura e recusar com
401se não bater. - Responder
2xxem 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. - Deduplicar por
X-Event-Id. O mesmo evento pode chegar mais de uma vez. - 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.