Pular para o conteúdo
LeztyPay docs
Painel

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, failed ou dead. Vale a distinção: failed é tentativa isolada que ainda vai ser retentada, dead é a que esgotou as 6 e parou.
  • event_type para olhar um tipo só, event_id para seguir um evento específico pelas tentativas, endpoint_id para isolar um destino.
  • from e to para 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 (attempt = last + 1)) cria uma tentativa nova, imediata, a partir de qualquer entrega que não tenha dado certo, e responde 202 com o delivery_id novo.

O que o reenvio manual faz de diferente:

  • o X-Attempt continua 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_disabled e 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.