Reenvio
Todo POST que sai daqui vira uma linha consultável: o que foi enviado, para onde, em que
tentativa, com que resposta. É por aí que você descobre se o evento que faltou nunca saiu ou
esbarrou no seu servidor, e é de lá que você reenvia.
Inspecionar entregas
Em Lista as entregas de webhook do seller estão as tentativas, da mais recente para a mais antiga,
com paginação por cursor. Os filtros que resolvem quase tudo:
status:success,failedoudead. Vale a distinção:failedé tentativa isolada que ainda vai ser retentada,deadé a que esgotou as 6 e parou.event_typepara olhar um tipo só,event_idpara seguir um evento específico pelas tentativas,endpoint_idpara isolar um destino.frometopara a janela de tempo.
Cada item traz target (endpoint para o webhook da conta, postback para o postback_url da
cobrança), target_host, attempt, status_code, response_ms e error.
O detalhe de uma tentativa (Detalha uma entrega de webhook) acrescenta três coisas que a
listagem não tem: o payload que foi enviado, os request_headers daquele POST e o
response_body_excerpt, o primeiro 1 KB do que o seu servidor respondeu. Quando o status_code é
500 e você não acha o log do seu lado, o trecho da resposta costuma ter a mensagem de erro
inteira.
Uma ressalva sobre request_headers: o X-Signature guardado ali tem só o t=. A assinatura
completa nunca é persistida, nem para nós. Você consegue reproduzir a verificação, porque tem o
payload, o t e o segredo, mas não consegue copiar a assinatura de uma entrega antiga para
reencenar a requisição.
No painel a mesma coisa está na tela de webhooks, com o histórico por endpoint e o detalhe de cada tentativa.
Ping
POST /v1/me/webhook-endpoints/{id}/ping
(na referência)
dispara na hora uma entrega de teste para aquele endpoint e responde 202 com o delivery_id da
tentativa. É o jeito de conferir a rota, o TLS e a verificação de assinatura sem esperar uma
cobrança de verdade.
O ping chega assinado como qualquer outro evento, com type: "ping" e um data.object mínimo
(endpoint_id e url). O seu receptor precisa responder 2xx a ele: um ping que volta 400
porque o tipo é desconhecido é o mesmo bug que vai descartar todo evento novo depois.
ping não é assinável em events, e a entrega dele vai só para o endpoint que você pediu, nunca
para os outros.
Reenviar uma entrega
O reenvio (Reenvia uma entrega manualmente () cria uma tentativa nova, imediata, a partir de
qualquer entrega que não tenha dado certo, e responde attempt = last + 1)202 com o delivery_id novo.
O que o reenvio manual faz de diferente:
- o
X-Attemptcontinua de onde parou (última + 1), então pode passar de 6; - ele nunca vira
dead, mesmo falhando: você reenvia quantas vezes quiser; - ele não agenda a próxima tentativa. Uma falha no reenvio manual para ali.
O payload enviado é o mesmo de sempre, com o mesmo X-Event-Id: do ponto de vista do seu receptor
é uma duplicata, e a deduplicação tem que estar funcionando antes de você
sair reenviando em lote.
Um 409 invalid_state quer dizer que não há o que reenviar: ou já existe uma entrega bem-sucedida
para aquele destino, ou uma tentativa automática com o mesmo número já está agendada. Nos dois
casos a resposta certa é seguir em frente.
O exemplo abaixo varre as entregas que morreram nas últimas 24 h e reenvia cada uma, tratando o
409 como caso normal:
// Reenvio manual de entregas de webhook que morreram, em Node 22.
//
// Zero dependência: `fetch` global. `LM_API_URL` e `LM_API_KEY` vêm do ambiente.
// LM_API_KEY=sk_test_... node retry-delivery.mjs
const API_URL = process.env.LM_API_URL ?? 'https://api.leztypay.com';
const API_KEY = process.env.LM_API_KEY ?? '';
async function api(path, init = {}) {
const response = await fetch(new URL(path, API_URL), {
...init,
headers: { Authorization: `Bearer ${API_KEY}`, ...init.headers },
});
const body = await response.json();
return { status: response.status, body };
}
// 1. As entregas que esgotaram as 6 tentativas nas últimas 24 h. `status=dead` é o que
// ficou mesmo sem chegar; `failed` é tentativa isolada que ainda tem retentativa pela
// frente e não precisa de reenvio manual.
const desde = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const query = new URLSearchParams({ status: 'dead', from: desde, limit: '100' });
const lista = await api(`/v1/webhook-deliveries?${query}`);
if (lista.status !== 200) {
throw new Error(`listagem falhou: ${lista.status} ${lista.body.error?.code}`);
}
// 2. Uma tentativa nova por entrega. Ela nasce com `attempt = última + 1`, não conta para
// o limite de 6 e nunca vira `dead`: se falhar, você reenvia de novo.
for (const entrega of lista.body.data) {
const { status, body } = await api(`/v1/webhook-deliveries/${entrega.id}/retry`, {
method: 'POST',
});
if (status === 202) {
console.log(`${entrega.event_type} ${entrega.event_id}: tentativa ${body.delivery_id}`);
} else if (status === 409) {
// `invalid_state`: ou já existe uma entrega bem-sucedida para esse alvo, ou uma
// tentativa com o mesmo número já está agendada. Nos dois casos não há o que fazer.
console.log(`${entrega.event_type} ${entrega.event_id}: ${body.error.message}`);
} else {
throw new Error(`${entrega.id}: ${status} ${body.error?.code} (${body.error?.request_id})`);
}
}
console.log(`${lista.body.data.length} entrega(s) processada(s)`);
Quando o reenvio não resolve
Reenvio manual só existe para entregas que alguma vez foram criadas. Ele não cobre:
- o período em que o endpoint esteve desativado, porque as entregas nesse intervalo falharam
com
endpoint_disablede não têm payload novo a oferecer; - eventos anteriores ao cadastro do endpoint;
- o gatilho de teste que paga uma cobrança sem entregar webhook nenhum.
Para esses casos o caminho é reconciliar pela consulta: o feed de eventos
(Lista o feed de eventos do seller) para varrer o período e Consulta uma cobrança para conferir o
estado atual de cada cobrança que você ainda tem como pendente.