Pular para o conteúdo
LIQFYdocs
PTENENEN
Ir para o painel

Webhooks

Webhook é como a Liqfy avisa o seu backend, em tempo real, que algo mudou. Use webhook para liberar pedido — nunca dependa só de ficar consultando. Se uma entrega se perder, use a API de conciliação para recuperar.

#0. Comece por aqui (perguntas mais comuns)

#Uma URL ou uma URL por evento?

Uma URL só. No painel (Integrações → Webhooks → Novo endpoint) você cadastra um endpoint HTTPS e marca vários eventos nos checkboxes. A Liqfy envia um POST separado para cada ocorrência, sempre na mesma URL.

Modelo antigo (alguns PSPs)Liqfy
/webhook/cashin, /webhook/cashout, /webhook/refund (path por tipo)https://sua-loja.com/hooks/liqfy + lista de eventos no cadastro
Um endpoint HTTP por eventoUm endpoint, vários eventos; o campo type (ou event no legado) diferencia

URL genérica recomendada:

text
https://seu-dominio.com.br/hooks/liqfy

Evite paths do tipo /api/charge/created a menos que o seu roteador exija — o path não escolhe o evento; o checkbox no painel (ou o array events na API) escolhe.

#Como fica a estrutura com “tudo junto”?

Não chega um array com todos os eventos de uma vez. Chega um POST por mudança de status. Você faz switch/if no tipo:

js
// Express — corpo já parseado só DEPOIS de validar a assinatura no raw body
const type = body.type || body.event; // canônico usa type; legado usa event

switch (type) {
  case 'charge.paid':
    // liberar pedido
    break;
  case 'charge.failed':
  case 'charge.expired':
    // cancelar / expirar
    break;
  case 'charge.refunded':
    // estorno
    break;
  case 'med_case.created':
  case 'med_case.updated':
  case 'med.resolved':
    // disputa Pix (MED) — veja "Eventos de MED" abaixo
    break;
  default:
    // tipo novo ou desconhecido: 200 OK e ignore
}
res.sendStatus(200); // responda 2xx em poucos segundos

#Estorno (refund) — qual evento?

Precisa deEventoFamília
Cobrança pagacharge.paidcanônico
Cobrança falhou / expiroucharge.failed / charge.expiredcanônico
Estorno de cobrançacharge.refundedcanônico
Saque liquidado / falhoupayout.paid / payout.failedcanônico (não é refund)
Disputa Pix (MED) aberta / mudou / resolvidamed_case.created / med_case.updated / med.resolvedcanônico / canônico / envelope legado (não é refund)

Marque charge.refunded no mesmo endpoint se precisar de estorno — ele dispara em todo estorno: os que você (ou a equipe da Liqfy, em seu nome) faz e a devolução automática por trava de CPF. Não confunda com payout.* (saque da carteira).

MED não é estorno: quando o banco do pagador abre um MED, nenhum estorno é criado e charge.refunded não dispara — nem quando o MED abre, nem quando é ganho ou perdido. Acompanhe disputas pelos eventos de MED.

O objeto de estorno traz o valor reembolsado e a origem:

json
{
  "id": "evt_…",
  "object": "event",
  "type": "charge.refunded",
  "created_at": "2026-07-23T14:31:00.000Z",
  "data": {
    "object": {
      "id": "ch_01JABCDEF",
      "object": "charge",
      "amount": 1000,
      "currency": "BRL",
      "status": "refunded",
      "payment_method": null,
      "amount_refunded": 1000,
      "settlement": { "end_to_end_id": "E0000…" },
      "refund": {
        "amount": 1000,
        "currency": "BRL",
        "origin": "MERCHANT",
        "reason": "customer request",
        "end_to_end_id": "E0000…"
      }
    }
  }
}

refund.origin é MERCHANT para estorno iniciado pelo lojista/admin ou AUTOMATIC_PAYER_RESTRICTION para a devolução Pix automática disparada quando o CPF/CNPJ do pagador liquidado não bateu com o pagador esperado da cobrança. amount_refunded é o total já reembolsado (igual a refund.amount num estorno único; maior em estornos parciais). Neste evento, data.object.amount é o valor original da cobrança (amount_refunded é a parte dela já devolvida) — e payment_method é sempre null (o evento de estorno não traz meio de pagamento). Exceção: eventos de estorno emitidos antes de existir o valor original entregam amount: 0; aí leia a cobrança se precisar do total original de um estorno antigo.

refund.reason só aparece em estorno de origem MERCHANT — estorno automático nunca traz motivo. refund.origin pode faltar em eventos emitidos antes de o campo existir.

#Várias contas / lojas na mesma URL

Pode. Cada conta Liqfy tem suas chaves e seus endpoints, mas a URL do seu servidor pode ser a mesma. Segmente no payload (data.object.id = ch_…, metadata que você enviou na criação da cobrança, etc.).

#Pelo painel (sem API)

  1. Integrações → Webhooks → Novo endpoint
  2. URL HTTPS genérica (ex.: …/hooks/liqfy)
  3. Marque pelo menos: charge.created, charge.paid, charge.failed, charge.expired (+ charge.refunded se for estorno)
  4. Salve o secret (aparece uma vez)
  5. Clique em Testar e confira se o seu servidor respondeu 2xx

#1. O envelope canônico de evento

Integrações novas assinam os eventos canônicos (charge.*, payout.*, med_case.*). Toda entrega é um POST HTTPS na sua URL registrada, com este formato:

json
{
  "id": "evt_5f8a3c1e9b2d4a6f8e0c1b3d5f7a9c1e",
  "object": "event",
  "api_version": "2026-07-23",
  "type": "charge.paid",
  "created_at": "2026-07-23T14:31:00.000Z",
  "data": {
    "object": {
      "id": "ch_01JABCDEF",
      "object": "charge",
      "amount": 1000,
      "currency": "BRL",
      "status": "paid",
      "payment_method": "pix",
      "idempotency_key": "order-123",
      "metadata": { "order_id": "123" },
      "payer": { "name": "Maria da Silva", "document": "***.982.247-**" },
      "settlement": {}
    }
  }
}
  • id (evt_…) é o id do evento de negócio — estável em toda tentativa de entrega e em todo endpoint que receba o mesmo evento. Deduplique por ele.
  • data.object é um objeto charge/payout/med_case público e filtrado por allowlist (nome de provedor, custo, segredo ou payload cru de PSP nunca aparecem aqui), mas nem sempre é o formato REST completo:
    • med_case.* é a única paridade real — o mesmo objeto de GET /v1/med/cases/{id};
    • charge.created é o payload de charge mais completo: pode trazer customer (name, document, email, phone), pix (br_code, expires_at), metadata e created_at. Esses dados vêm de uma consulta feita no melhor esforço — se ela falhar, o evento sai no formato reduzido. Não há URL nem base64 da imagem do QR: gere o QR a partir do br_code ou leia a cobrança. payment_method só vem preenchido quando a cobrança aceita um único meio;
    • charge.paid, charge.failed e charge.expired podem trazer idempotency_key e metadata; a metadata passa pelo mesmo filtro público do GET /v1/charges/:id. Nenhum desses eventos de status acrescenta customer, pix ou created_at. Leia data.object.status para saber o que houve: paid; no charge.failed, failed, cancelled, expired ou disputed;
    • payout.paid / payout.failed terminais podem trazer idempotency_key, amount, currency, pix_key e outcome; payout.failed também pode trazer refunded e legs. fee não vem. O status público continua paid ou failed. O payout.paid pode continuar trazendo settlement: end_to_end_id em uma perna, ou end_to_end_id: null com legs[] em várias pernas.
    • charge.paid também pode trazer payer e os payout.paid / payout.failed terminais podem trazer beneficiary. Cada um é { name, document } conforme informado pelo provedor de pagamento, e qualquer um dos valores pode ser null. O document vem sempre mascarado (***.982.247-** para CPF, **.***.111/0001-** para CNPJ); o documento completo nunca é enviado. O objeto inteiro é omitido quando o provedor não informou nenhum dos dois valores. Não é o customer enviado na criação da cobrança.
  • settlement.end_to_end_id só vem preenchido quando há um end-to-end Pix válido conhecido — na prática no charge.paid e no charge.refunded. O charge.created sempre traz settlement: {}.
  • Trate type desconhecido com naturalidade (200 OK e ignore), para que adicionar evento novo nunca quebre você.

#Catálogo de eventos

GET /v1/webhooks/event-catalog devolve a lista viva e autoritativa. Ele exige a sua chave de API, como toda rota em /v1/webhooks (só as rotas de inbound do provedor são abertas):

bash
curl https://api.liqfy.com.br/v1/webhooks/event-catalog \
  -H "Authorization: Bearer $LIQFY_API_KEY"

A resposta é um array de objetos { "event", "description" } (um item pode trazer também legacy: true):

EventoQuando dispara
charge.createdCobrança criada (Pix/cartão gerado, aguardando pagamento).
charge.paidCobrança paga e confirmada (Pix/cartão liquidado).
charge.failedCobrança falhou ou foi cancelada.
charge.expiredCobrança expirou sem pagamento.
charge.refundedCobrança reembolsada (total ou parcial) — reembolso do lojista ou devolução automática (trava de CPF). Não dispara para MED (use med_case.* e med.resolved).
payout.createdSaque solicitado.
payout.approvedSaque aprovado.
payout.paidSaque liquidado com sucesso.
payout.failedSaque falhou ou foi rejeitado.
kyc.submittedKYC enviado para análise.
kyc.approvedKYC aprovado.
kyc.rejectedKYC rejeitado.
tenant.createdNova conta criada.
tenant.updatedConta atualizada.
med.resolvedMED (devolução pedida pelo pagador) resolvida — informa o destino do valor: devolvido ao lojista, encerrado sem devolução ou encerrado como perda.
med_case.createdCaso MED aberto contra uma cobrança sua — traz o valor contestado e o prazo para contestar.
med_case.updatedCaso MED mudou — contestado, aceito, encerrado, evidências pedidas.

Os eventos de KYC e de conta usam o envelope legado — veja Eventos de conta e KYC.

#Campos de saques terminais

Os exemplos mostram callbacks canônicos enriquecidos. Eventos antigos em fila, entregas de teste e falha na consulta de detalhes podem omitir os campos novos; continue aceitando o objeto reduzido. O formato legado {event,data} não muda.

Exemplo de payout.paid (settlement mantém o formato existente quando presente):

json
{
  "id": "evt_5f8a3c1e9b2d4a6f8e0c1b3d5f7a9c1e",
  "object": "event",
  "api_version": "2026-07-23",
  "type": "payout.paid",
  "created_at": "2026-07-23T14:31:00.000Z",
  "data": {
    "object": {
      "id": "po_01JABCDEF",
      "object": "payout",
      "status": "paid",
      "idempotency_key": "withdrawal-123",
      "amount": 1000,
      "currency": "BRL",
      "pix_key": "merchant@example.com",
      "outcome": "completed",
      "beneficiary": { "name": "Maria da Silva", "document": "***.982.247-**" }
    }
  }
}

Exemplo de payout.failed (perna falha sem estorno):

json
{
  "id": "evt_5f8a3c1e9b2d4a6f8e0c1b3d5f7a9c1f",
  "object": "event",
  "api_version": "2026-07-23",
  "type": "payout.failed",
  "created_at": "2026-07-23T14:31:00.000Z",
  "data": {
    "object": {
      "id": "po_01JABCDEG",
      "object": "payout",
      "status": "failed",
      "idempotency_key": "withdrawal-124",
      "amount": 1000,
      "currency": "BRL",
      "pix_key": "merchant@example.com",
      "outcome": "failed",
      "refunded": false,
      "legs": [
        {
          "index": 0,
          "amount": 1000,
          "status": "failed",
          "refunded": false
        }
      ]
    }
  }
}
Campo em data.objectSignificado
idempotency_keyChave usada na criação da cobrança ou do saque.
metadata (charge.paid, charge.failed, charge.expired)Metadata pública do lojista, incluindo order_id quando fornecido; mesmo filtro de GET /v1/charges/:id.
amount, currency (payout.*)Valor em centavos e moeda ISO 4217.
pix_key (payout.*)Chave Pix de destino; pode ser null.
payer (charge.paid)Pagador { name, document } conforme o provedor; document sempre mascarado; omitido quando não informado.
beneficiary (payout.*)Favorecido { name, document } conforme o provedor; document sempre mascarado; omitido quando não informado.
outcome (payout.*)Resultado terminal interno em minúsculas, separado do status público.
refunded (payout.failed)Se o estorno foi contabilizado no momento do envio.
legs[] (payout.failed)index, amount, status em minúsculas, refunded e end_to_end_id só para perna concluída com E2E Pix válido. Pode ser [] se falhou antes do despacho.

Pagador e favorecido são melhor esforço. Estes campos dependem do que o provedor de pagamento informa em cada operação, então podem não vir (ou vir com apenas um dos dois valores) em alguns pagamentos. Use-os só como contexto adicional: não os exija e não os use para conciliar ou aprovar nada.

outcomestatus públicoEvento
completedpaidpayout.paid
failed, rejected, cancelledfailedpayout.failed

refunded reflete o momento do envio. Se o estorno estiver retido, payout.failed pode trazer false; a liberação posterior não gera webhook novo. Consulte GET /v1/withdrawals/{id} para o estado atual. Se depois se confirmar que o saque foi pago, payout.paid chega após o payout.failed, e o estorno já lançado, se houver, é revertido.

Só falhas definitivas (configuração da conta). Quando ativada na sua conta, payout.failed só é enviado quando a falha é definitiva e o estorno já foi lançado. Enquanto o estorno está retido para confirmação com o banco, nenhum webhook é enviado; o saque termina com payout.failed (estorno lançado) ou payout.paid (o PIX saiu). Você pode reembolsar o seu usuário assim que o payout.failed chegar. Peça ao suporte para ativar.

#Por que um saque falhou

O payout.failed traz o motivo como código estável: data.object.failure_code (minúsculo, ex. destination_not_allowed, destination_unavailable, invalid_pix_key, provider_rejected) e data.object.failure_message, uma frase neutra para esse código. Os dois só aparecem quando a falha corresponde a um código da tabela pública; fora dela, o saque não traz nenhum dos dois.

Um saque em várias pernas que quita só em parte não dispara webhook. Quando um saque dividido termina parcialmente (status interno PARTIALLY_COMPLETED), nenhum evento payout.* é entregue. Consulte GET /v1/withdrawals/by-idempotency-key/{key} para conciliar.

Retenção até o end-to-end (configuração da conta). Quando a sua conta tem essa opção ativada, um charge.paid / payout.paid Pix sem E2E fica retido enquanto a Liqfy o confirma. O evento é entregue com os E2Es confirmados; eventos seguintes da mesma cobrança ou do mesmo saque esperam atrás dele. Se a confirmação não chegar em 30 minutos, a operação gera um alerta, mas as novas retenções continuam aguardando. Sem essa opção, o callback pode chegar sem E2E. Entregas de teste nunca ficam retidas.

Disputas (MED) se acompanham pelos eventos de MED, não por charge.* — a cobrança foi paga, e um MED não é recusa nem estorno: charge.refunded não é enviado. Assine os eventos de MED; relendo a cobrança você vê status: "disputed" enquanto o caso está aberto. Se uma mudança da cobrança para disputa chegar aos avisos de cobrança, ela vem como charge.failed com data.object.status: "disputed" — nunca trate isso como recusa nem cancele o pedido por causa dele. Ver Disputas e MED.

Um charge.paid tardio pode vir depois de um charge.expired. Se o adquirente confirmar o pagamento só depois de a cobrança já ter expirado (webhook atrasado / API de status defasada), a Liqfy confere o pagamento e, confirmado, entrega charge.paid para a mesma cobrança. O charge.paid posterior é o estado final — trate como paga, mesmo tendo recebido charge.expired antes. Nunca assuma que charge.expired é definitivo.

Depois de paga, falha posterior não é entregue. Depois do charge.paid, um status posterior failed / expired / cancelled da mesma cobrança é descartado; depois de um estorno total, nenhum outro status da cobrança é entregue. A reentrega do mesmo evento de origem também é descartada.

Uma expiração também chega aos assinantes de charge.failed. Quando uma cobrança expira, o evento sai nos dois nomes: um endpoint inscrito só em charge.failed recebe a expiração como um charge.failed com data.object.status: "expired". Não trate todo charge.failed como recusa — leia data.object.status para diferenciar.

#2. Registrar seu endpoint

Endpoint POST /v1/webhooks/endpoints

Cabeçalhos

text
Authorization: Bearer lq_live_...
Content-Type: application/json

O cabeçalho canônico de autenticação é Authorization: Bearer <chave>. A forma legada apikey: <chave> continua funcionando como compatibilidade, mas integrações novas devem usar Authorization: Bearer.

Corpo

json
{
  "url": "https://loja.exemplo.com.br/hooks/liqfy",
  "events": ["charge.paid", "charge.failed", "charge.expired"]
}

Resposta 201 Created

json
{
  "id": "e5f6a7b8-c9d0-4123-9ef0-123456789012",
  "url": "https://loja.exemplo.com.br/hooks/liqfy",
  "events": ["charge.expired", "charge.failed", "charge.paid"],
  "secret": "b8f3a9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
  "status": "ACTIVE"
}

Guarde o secret na hora. Ele aparece só na criação e é necessário para verificar toda entrega. Nós não mostramos de novo.

#Limites e permissões

RegraValor
Endpoints ativos por conta10 com KYC aprovado, 2 sem. Conferido na criação e na reativação (PATCH com status: ACTIVE)
Entregas de teste por hora por conta30 com KYC aprovado, 10 sem
Status da conta indisponívelquando a Liqfy não consegue confirmar o status da conta, valem os limites sem KYC
Formato da URLprecisa começar com http:// ou https:// — senão 400
Esquema da URLhttps:// fora de SANDBOX/STAGING — senão 400 WEBHOOK_HTTPS_REQUIRED. Um endpoint http:// salvo antes da regra continua recebendo
Host da URLnão pode ser o próprio domínio da plataforma — 400 WEBHOOK_OWN_DOMAIN
URL na hora do envioprecisa resolver para um endereço público e não para a plataforma; senão a tentativa falha (SSRF_BLOCKED)
eventsarray não vazio de nomes conhecidos — senão 400 com a lista de eventos válidos
URL duplicada na conta409 WEBHOOK_ENDPOINT_DUPLICATE, com details.endpointId do endpoint existente — edite os eventos dele
Acima do limite de endpoints409 WEBHOOK_ENDPOINT_LIMIT (details.limit); uma reativação acima do limite é desfeita
Acima do limite de testes429 WEBHOOK_TEST_RATE_LIMIT (details.limit)
Escritas (criar/atualizar/apagar/girar secret/testar/reenviar)exigem sessão de painel OWNER ou ADMIN; a personificação somente-leitura é recusada (403 WEBHOOK_ROLE_REQUIRED / IMPERSONATION_VIEW_ONLY). O papel é lido do token da sessão, então uma troca de papel vale quando esse token expira. Uma chave de API escreve como a própria conta.
Sessão do painel depois de troca de senhauma sessão emitida antes da troca de senha é recusada — 401 Session no longer valid
Conta suspensa ou bloqueadanão pode criar, editar nem testar endpoints — 403 TENANT_NOT_ACTIVE

#Inscrições canônicas

Novas inscrições de cobranças e saques usam apenas os nomes públicos canônicos. Adicionar nomes antigos na criação ou atualização retorna HTTP 400, código LEGACY_WEBHOOK_EVENT e o par event / replacement. Inscrições existentes continuam recebendo eventos; atualizações podem manter os nomes já inscritos. O lojista pode visualizar e remover esses nomes no painel.

json
{
  "code": "LEGACY_WEBHOOK_EVENT",
  "event": "<retired name>",
  "replacement": "charge.paid",
  "message": "Event <retired name> was retired; use charge.paid."
}

Um endpoint inscrito num nome canônico e também no nome antigo correspondente recebe uma entrega por ocorrência, no nome canônico. Os nomes brutos payment.status_changed e withdrawal.status_changed continuam aceitos na criação/atualização e são expandidos para charge.paid + charge.failed e payout.paid + payout.failed.

payout.approved informa a aprovação antes da liquidação. Seu envelope versionado contém data.object com object: "payout", um identificador po_ e status: "processing". Ele usa a assinatura HMAC com timestamp, como os demais eventos de saque. O identificador do evento é estável por saque, inclusive em republicações.

#Eventos de conta e KYC

kyc.submitted, kyc.approved, kyc.rejected e o par de ciclo de vida da conta tenant.created / tenant.updated também disparam, e usam sempre o envelope mais antigo { event, data } — não são charge.*/payout.*, então não vêm com o invólucro evt_ nem com a assinatura t=,v1=. É por isso que existem dois esquemas de assinatura, logo abaixo.

Nos kyc.*, o data traz documentId e documentType quando conhecidos, status (PENDING no kyc.submitted, APPROVED no kyc.approved, REJECTED no kyc.rejected), reason no kyc.rejected quando houver, e occurredAt.

⚠️ Lacuna de nomenclatura conhecida: os eventos de ciclo de vida da conta emitem hoje um nome de campo interno literalmente no payload — uma violação de glossário rastreada, não parte intencional do contrato público. Veja o gap de nomenclatura conhecido e os followups no guia de migração para os nomes exatos de evento e de campo. Não construa integração permanente contra esse nome de campo.

#Eventos de MED

Três eventos acompanham uma disputa Pix (MED) — o fluxo completo está em Disputas e MED:

EventoEnvelope / assinaturaQuando dispara
med_case.createdcanônico evt_ / t=,v1=Um caso MED abre contra uma cobrança sua.
med_case.updatedcanônico evt_ / t=,v1=O caso muda de status (appealed, accepted, rejected, closed) ou a plataforma te pede evidências.
med.resolvedlegado {event,data} / sha256=O caso chega ao desfecho; diz para onde foi o dinheiro.

med_case.created / med_case.updated trazem em data.object o mesmo objeto med_case que GET /v1/med/cases/{id} devolve:

json
{
  "id": "evt_9c2f4e1a7b3d5c8e0f1a2b3c4d5e6f70",
  "object": "event",
  "api_version": "2026-07-23",
  "type": "med_case.created",
  "created_at": "2026-09-23T14:02:12.000Z",
  "data": {
    "object": {
      "id": "med_3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10",
      "object": "med_case",
      "type": "med",
      "status": "open",
      "amount": 15000,
      "currency": "BRL",
      "charge_id": "ch_6b0f4c1e-2a8d-4c1b-9d0e-1f2a3b4c5d6e",
      "charge_metadata": { "order_id": "PED-10442" },
      "customer_name": "Maria Silva",
      "reason": "Pagador relata golpe",
      "response_deadline": "2026-09-30T23:59:59.000Z",
      "evidence_requested_at": null,
      "appeal_reason": null,
      "appealed_at": null,
      "evidence_count": 0,
      "money_outcome": null,
      "opened_at": "2026-09-23T14:02:11.000Z",
      "closed_at": null,
      "created_at": "2026-09-23T14:02:12.000Z",
      "updated_at": "2026-09-23T14:02:12.000Z"
    }
  }
}

O med_case.updated tem o mesmo formato, com type: "med_case.updated" e o caso como ficou depois da mudança (por exemplo status: "closed" e money_outcome: "refunded_to_merchant").

No objeto med_case, status é um de open, appealed, accepted, rejected, closed; type é med, chargeback, dispute ou fraud; money_outcome é null ou um de refunded_to_merchant, closed_without_refund, lost.

med.resolved usa o corpo legado {event, data} e a assinatura sha256=:

json
{
  "event": "med.resolved",
  "data": {
    "infractionId": "3f1c2b9e-0d7a-4a55-9a51-6f0e3c1d2b10",
    "transactionId": "6b0f4c1e-2a8d-4c1b-9d0e-1f2a3b4c5d6e",
    "amount": 15000,
    "currency": "BRL",
    "outcome": "closed_without_refund",
    "occurredAt": "2026-09-11T14:32:07.000Z"
  }
}
  • infractionId é o id do caso sem o prefixo med_ e transactionId é o id da cobrança sem o prefixo ch_: med_ + infractionId é o id que você usa em /v1/med/cases/{id}.
  • outcome é refunded_to_merchant (o valor retido voltou para o seu saldo), closed_without_refund (encerrado, o valor não voltou) ou lost (o valor segue debitado). É o mesmo valor do money_outcome do caso.
  • O mesmo desfecho nunca é avisado duas vezes; um desfecho diferente (uma correção) é.
  • Aceitar um caso você mesmo não envia med.resolved — o med_case.updated com status: "accepted" avisa.

Os avisos de desfecho (med.resolved, e med_case.updated para accepted / rejected / closed) podem não chegar: um evento de MED cuja publicação falha não é reenviado (a entrega para o seu endpoint, uma vez publicado o evento, segue a agenda de retentativa normal). Trate estes eventos como sinal e reconcilie com GET /v1/med/cases — a API é a fonte da verdade.

#3. Verificar a assinatura

Toda entrega é assinada com HMAC-SHA256 sobre o corpo bruto da requisição, usando o secret do seu endpoint. Há dois esquemas, escolhidos automaticamente pela família do evento.

CabeçalhoEsquemaAplica-se a
X-Liqfy-Signaturet=<unix-segundos>,v1=<hex> — o payload assinado é "<t>.<corpoBruto>"Eventos canônicos (charge.*, payout.*, med_case.*)
X-Liqfy-Signaturesha256=<hex> — o payload assinado é só o corpo brutoEventos legados (kyc.*, med.resolved e os eventos de ciclo de vida da conta acima)
X-Liqfy-Delivery-IdId opaco, estável em toda retentativa da mesma entregaAmbos
X-Liqfy-Event-TypeEspelha o nome do evento entregueAmbos
X-Liqfy-Testtrue, só numa entrega de teste; ausente em toda entrega realAmbos
User-AgentLiqfy-Webhooks/1.0 (espaços do nome viram hífen)Ambos

O timestamp canônico é gerado na hora do envio: o t (e a assinatura) muda a cada retentativa, enquanto o corpo continua idêntico byte a byte. Durante uma migração de marca, nomes antigos de cabeçalho também podem ser enviados em paralelo, com os mesmos valores.

O esquema canônico embute um timestamp no material assinado justamente para você recusar replay fora de uma janela de tolerância (recomendado: 5 minutos). O esquema antigo não tem timestamp e não consegue fazer isso.

Verifique sobre o corpo BRUTO. Não faça parse do JSON e re-serialize antes de verificar: qualquer diferença de espaço ou de ordem de chave muda o HMAC e a assinatura falha. É o erro mais comum em integração de webhook, em qualquer linguagem.

#Node.js (Express) — esquema canônico

js
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SEGREDO = process.env.LIQFY_WEBHOOK_SECRET;
const TOLERANCIA_SEGUNDOS = 300;

function verificaCanonica(corpoBruto, cabecalho, segredo) {
  const [parteT, parteV1] = cabecalho.split(',');
  const t = Number(parteT?.split('=')[1]);
  const v1 = parteV1?.split('=')[1];
  if (!Number.isFinite(t) || !v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCIA_SEGUNDOS) return false;

  const esperado = crypto
    .createHmac('sha256', segredo)
    .update(`${t}.${corpoBruto}`)
    .digest('hex');
  const a = Buffer.from(esperado);
  const b = Buffer.from(v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post(
  '/hooks/liqfy',
  // captura o corpo bruto — express.json() o destrói
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const assinatura = req.header('X-Liqfy-Signature') || '';
    if (!verificaCanonica(req.body.toString('utf8'), assinatura, SEGREDO)) {
      return res.status(401).send('assinatura invalida');
    }

    const { id: eventoId, type, data } = JSON.parse(req.body.toString('utf8'));

    // Responda 200 RÁPIDO. O trabalho pesado vai para uma fila.
    res.status(200).end();
    fila.enfileirar({ eventoId, type, data, deliveryId: req.header('X-Liqfy-Delivery-Id') });
  },
);

Não quer escrever isso à mão? Os SDKs oficiais verificam os dois esquemas (canônico e legado) para você. O ponto de entrada recomendado passa o mapa de cabeçalhos: o SDK escolhe o cabeçalho de assinatura pelo nome (uma opção), ou detecta automaticamente o único cabeçalho cujo nome termina em -signature — que é exatamente o cabeçalho das entregas da sua marca. Veja packages/sdk-node/README.md.

js
// node — @liqfy/node
import { LiqfyClient } from '@liqfy/node';

const client = new LiqfyClient({ apiKey: process.env.LIQFY_API_KEY!, baseUrl: 'https://api.liqfy.com.br' });

// forma com mapa de cabeçalhos: detecta o cabeçalho `*-signature` desta marca
if (!client.webhooks.verify(corpoBruto, cabecalhos, segredo)) return res.status(401).end();

// ou fixe o nome do cabeçalho explicitamente (também configurável uma vez como `webhookSignatureHeader`)
client.webhooks.verify(corpoBruto, cabecalhos, segredo, { signatureHeader: 'X-Liqfy-Signature' });
client.webhooks.parse(corpoBruto, cabecalhos, segredo);

// superfície de gestão 0.8.0
await client.webhooks.endpoints.test(endpointId);      // POST /v1/webhooks/endpoints/:id/test
await client.webhooks.events({ since, limit: 50 });    // GET /v1/webhooks/events (pull)
python
# python — liqfy
ok = client.webhooks.verify(corpo_bruto, cabecalhos, segredo)                       # auto-detecção
ok = client.webhooks.verify(corpo_bruto, cabecalhos, segredo,
                            signature_header='X-Liqfy-Signature')  # nome explícito
evento = client.webhooks.parse(corpo_bruto, cabecalhos, segredo)
client.webhooks.endpoints.test(endpoint_id)
client.webhooks.events(since=..., limit=50)
php
// php — liqfy/sdk-php
$ok = $client->webhooks->verify($corpoBruto, $cabecalhos, $segredo); // array = mapa de cabeçalhos, auto-detecção
$ok = $client->webhooks->verify($corpoBruto, $cabecalhos, $segredo, 300, null, 'X-Liqfy-Signature');
$evento = $client->webhooks->parse($corpoBruto, $cabecalhos, $segredo);
$client->webhooks->endpoints->test($endpointId);
$client->webhooks->events(['since' => $since, 'limit' => 50]);
csharp
// dotnet — Liqfy
bool ok = client.Webhooks.Verify(corpoBruto, cabecalhos, segredo); // dicionário = mapa de cabeçalhos
bool ok2 = client.Webhooks.Verify(corpoBruto, cabecalhos, segredo, 300, "X-Liqfy-Signature");
var evt = client.Webhooks.Parse(corpoBruto, cabecalhos, segredo);
await client.Webhooks.Endpoints.TestAsync(endpointId);
await client.Webhooks.EventsAsync(); // ListWebhookEventsQuery opcional

A resolução é idêntica nas quatro: um signatureHeader explícito (correspondência de nome, sem diferenciar maiúsculas) vence; sem ele, o SDK usa o único cabeçalho cujo nome em minúsculas termina em -signature, e falha quando há zero ou dois — ele nunca adivinha. Uma string de assinatura avulsa continua funcionando e não precisa de nome de cabeçalho.

#PHP

php
$segredo = getenv('LIQFY_WEBHOOK_SECRET');
$bruto   = file_get_contents('php://input');
$sig     = $_SERVER['HTTP_X_LIQFY_SIGNATURE'] ?? '';
$espera  = 'sha256=' . hash_hmac('sha256', $bruto, $segredo);

if (!hash_equals($espera, $sig)) {
    http_response_code(401);
    exit('assinatura invalida');
}

$corpo = json_decode($bruto, true);
http_response_code(200);
// enfileire $corpo para processar

#Python (Flask)

python
import hmac, hashlib, os
from flask import request, abort

SEGREDO = os.environ['LIQFY_WEBHOOK_SECRET'].encode()

@app.post('/hooks/liqfy')
def liqfy_hook():
    bruto = request.get_data()  # bytes, intocados
    sig = request.headers.get('X-Liqfy-Signature', '')
    esperado = 'sha256=' + hmac.new(SEGREDO, bruto, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, esperado):
        abort(401)
    payload = request.get_json()
    # responda 200 rápido, processe depois
    return '', 200

#4. Garantias de entrega, retentativa e DLQ

  • Sucesso Qualquer resposta 2xx confirma a entrega e para as retentativas.
  • Timeout 30 segundos (10 numa entrega de teste). Resposta mais lenta conta como falha.
  • Agenda de retentativa Backoff exponencial (1s × 2^tentativa), com teto de 6 horas entre tentativas. As entregas vencidas são apanhadas a cada 10 segundos.
  • Máximo de tentativas 15. Depois da última falha a entrega vai para CANCELLED (dead-letter) — ela não é apagada, e pode ser reenviada manualmente.
  • Endpoint pausado ou parado Se as entregas para o seu servidor falharem repetidamente, o endpoint pode ser pausado. O painel mostra o endpoint afetado, o último erro e quantos eventos aguardam envio. Os novos eventos ficam guardados para reenvio. Se as tentativas de restaurar a entrega continuarem falhando, o endpoint pode ser parado e não volta sozinho.
  • Restaurar as entregas Corrija o seu servidor e clique em Reenviar na página de webhooks ou no aviso do painel. Isso reativa o endpoint e reenvia os eventos em espera. Pela API, use POST /v1/webhooks/endpoints/{id}/resume. Uma nova falha pode pausar as entregas outra vez. Confira o status do endpoint e concilie os pagamentos pela API enquanto a entrega estiver interrompida. Se substituir o endpoint, inscreva o novo endpoint ativo nos mesmos eventos para receber as entregas em espera.
  • Trocar o seu endpoint As entregas em falha seguem o endpoint que você tem cadastrado agora. Crie um endpoint (ou reative um) e as entregas que estão falhando nos seus outros endpoints migram para ele, quando ele assina o mesmo nome de evento. Desative ou apague um endpoint, ou pare de assinar um evento, e tudo que ainda estava na fila dele migra para os seus outros endpoints ativos inscritos no mesmo evento — ou é cancelado se não restar nenhum. Trocar a url de um endpoint reenfileira as entregas em falha dele na hora, com orçamento de tentativas novo. Só entregas criadas nos últimos 7 dias são movidas ou reiniciadas. Uma entrega movida ganha um X-Liqfy-Delivery-Id novo; o id do evento canônico (evt_…) permanece o mesmo.
  • Tratamento de 429 Se o seu endpoint devolver 429 com cabeçalho Retry-After (em segundos) ou corpo JSON { "retry_after": <segundos> }, esse valor (o maior dos dois) é respeitado (limitado a 5 minutos) no lugar do backoff cego.
  • Estabilidade do id de entrega O X-Liqfy-Delivery-Id é idêntico em todas as tentativas da mesma entrega — use-o como chave primária de deduplicação.

⚠️ Uma entrega pode chegar mais de uma vez. Faça o seu handler idempotente, deduplicando por X-Liqfy-Delivery-Id (ou pelo id do evento canônico, evt_…, que se mantém quando a entrega muda de endpoint).

#Padrão de handler idempotente

js
async function tratar({ deliveryId, eventoId, type, data }) {
  // Insert atômico — falha se já vimos esta entrega
  const inserido = await db.webhooksProcessados.insertIgnore({
    id: deliveryId ?? eventoId,
    recebidoEm: new Date(),
  });
  if (!inserido) return; // já tratado

  if (type === 'charge.paid') {
    await pedidos.marcarPago(data.object.id, data.object);
  }
}

#5. API de conciliação (pull)

Se um push se perdeu (seu receptor ficou fora do ar por horas), puxe os eventos perdidos em vez de perdê-los.

bash
curl "https://api.liqfy.com.br/v1/webhooks/events?since=2026-07-23T00:00:00Z&limit=50" \
  -H "Authorization: Bearer $LIQFY_API_KEY"
json
{
  "data": [
    { "eventType": "charge.paid", "payload": { "...": "..." }, "status": "DELIVERED", "lastStatusCode": 200, "deliveredAt": "2026-07-23T14:31:05.000Z", "createdAt": "2026-07-23T14:31:00.000Z" }
  ],
  "nextCursor": "MjAyNi0wNy0yM1QxNDozMTowMC4wMDBafGRlbF8xMjM="
}
  • Paginação por cursor em (createdAt desc, id desc) — devolva o nextCursor como cursor na próxima página, e trate-o como token opaco. Na última página, nextCursor é null.
  • limit padrão 50, máximo 100.
  • Filtros: since, until (ISO 8601), status, eventType.
  • Cada item é uma entrega: um evento enviado a dois endpoints seus aparece duas vezes. Nos canônicos, deduplique pelo id (evt_…) do payload.
  • Restrito aos seus próprios endpoints — nunca devolve entrega interna da plataforma.

#6. Testar um endpoint

POST /v1/webhooks/endpoints/:id/test manda uma entrega de amostra síncrona — sem registro de entrega e sem retentativa — para você conferir status, latência e tratamento de assinatura. Exige a mesma permissão das outras escritas e conta no limite de testes por hora (ver Limites e permissões).

bash
curl -X POST "https://api.liqfy.com.br/v1/webhooks/endpoints/<ENDPOINT_ID>/test" \
  -H "Authorization: Bearer $LIQFY_API_KEY"

A amostra segue o que o endpoint assina:

  • charge.paid (padrão, e sempre que o endpoint assina charge.paid) — envelope evt_ e assinatura t=,v1=. O objeto é uma charge com id ch_test_…, amount: 10000, status: "paid", payment_method: "pix" e metadata.test: true.
  • nome antigo — só quando o endpoint assina o nome antigo de charge.paid e não assina charge.paid: corpo {event,data} e assinatura sha256=, com data.test: true.

A resposta diz o que aconteceu e o que foi enviado:

json
{
  "ok": true,
  "statusCode": 200,
  "durationMs": 182,
  "errorKind": null,
  "error": null,
  "responseSnippet": "ok",
  "eventType": "charge.paid",
  "url": "https://loja.exemplo.com.br/hooks/liqfy",
  "sentHeaders": { "...": "..." },
  "sentPayload": { "...": "..." }
}
  • errorKind: timeout (sem resposta em 10 segundos), connection (inalcançável, recusado ou URL bloqueada), http (resposta fora de 2xx) ou null.
  • responseSnippet: os primeiros 500 caracteres do corpo da sua resposta, só em 2xx; num erro você recebe só o status e a mensagem.

#Entregas de teste

Algumas entregas são teste, não evento de negócio:

  • o botão Testar, acima;
  • o pagamento de uma cobrança de teste (criada com uma chave lq_test_ ou na tela Ambiente de teste do painel);
  • o saque simulado do Ambiente de teste, que manda o webhook de um saque concluído que nunca existiu (payout.paid com um id po_… novo). Ele tem limite por hora próprio, do mesmo tamanho do limite de testes, contado por entrega (cada endpoint inscrito conta uma).

Elas levam X-Liqfy-Test: true — e uma entrega real nunca leva esse cabeçalho. O sample do botão Testar é o único que também difere no corpo: ele carimba metadata.test: true no sample canônico (ou data.test: true no legado). O sample legado também traz providerFee e netAmount ilustrativos, que nenhuma entrega de produção emite — não programe contra nenhum dos dois campos. A cobrança de teste paga de verdade e o saque simulado do ambiente de teste entregam corpo idêntico ao do evento de negócio correspondente. O charge.created de uma cobrança de teste só sai marcado quando a plataforma conseguiu ler os detalhes da cobrança — se essa consulta falhar, ele sai sem o cabeçalho. Assinatura e os demais cabeçalhos são sempre os de um evento real, então o seu verificador e o seu parser rodam sem mudança — trate o cabeçalho como a fonte da verdade e nunca libere pedido, entregue mercadoria ou credite cliente a partir de uma entrega com ele.

#7. Checklist de produção

  • Endpoint em HTTPS com certificado TLS válido.
  • Assinatura verificada sobre o corpo bruto, antes do parse do JSON.
  • Entregas com X-Liqfy-Test: true nunca liberam pedido.
  • Comparação em tempo constante (timingSafeEqual / hash_equals / hmac.compare_digest).
  • Seu verificador suporta os dois esquemas se algum endpoint continua inscrito num evento legado.
  • O handler responde 2xx em menos de 5 segundos. Trabalho pesado vai para fila.
  • Idempotência por X-Liqfy-Delivery-Id (ou evt_… nos canônicos).
  • type desconhecido é ignorado sem erro (200 OK).
  • O segredo vem de um cofre de segredos — nunca commitado.
  • Existe alerta se nenhum webhook chegar numa janela esperada; use a API de conciliação como rede de segurança.

#8. Operação

#Listar endpoints

bash
curl "https://api.liqfy.com.br/v1/webhooks/endpoints" \
  -H "Authorization: Bearer $LIQFY_API_KEY"
json
{
  "data": [
    {
      "id": "e5f6a7b8-c9d0-4123-9ef0-123456789012",
      "url": "https://loja.exemplo.com.br/hooks/liqfy",
      "events": ["charge.paid", "charge.failed"],
      "status": "ACTIVE",
      "createdAt": "2026-07-23T14:31:00.000Z",
      "updatedAt": "2026-07-23T14:31:00.000Z",
      "consecutiveFailures": 0,
      "lastSuccessAt": "2026-07-23T14:31:05.000Z",
      "lastStatusCode": 200,
      "failingSince": null,
      "failing": false
    }
  ],
  "access": { "canManage": true },
  "httpsOnly": true
}

Os itens nunca trazem o secret. access.canManage diz se você pode escrever (criar, atualizar, apagar, girar secret, testar, reenviar) a partir deste chamador; httpsOnly diz se o ambiente exige https:// nas URLs de endpoint.

#Saúde do endpoint

Cada endpoint também traz a saúde das entregas:

CampoSignificado
consecutiveFailuresTentativas de entrega com falha em sequência, sem sucesso no meio
lastSuccessAtÚltima tentativa respondida com 2xx
lastStatusCodeStatus HTTP da última tentativa (null em erro de transporte)
failingSinceQuando o endpoint passou a ser considerado em falha; null enquanto saudável
failingtrue enquanto failingSince estiver preenchido

Um endpoint entra em falha quando chega a 20 tentativas com falha em sequência (padrão), ou quando recebeu tentativas na última hora e nenhuma teve sucesso. Ele continua em falha — mesmo que o tráfego pare — até uma tentativa dar certo. Enquanto estiver em falha, a conta é avisada por e-mail e no painel no máximo uma vez a cada 6 horas. A saúde só observa: nunca para, atrasa nem muda uma entrega.

#Listar entregas recentes

bash
curl "https://api.liqfy.com.br/v1/webhooks/deliveries?limit=25" \
  -H "Authorization: Bearer $LIQFY_API_KEY"

A resposta é { data, total, limit, offset }. Cada item traz a contagem de tentativas (attempts / maxAttempts), o último status code, o payload entregue, o texto de lastError, nextRetryAt, deliveredAt e, numa entrega movida para outro endpoint, cancelReason / supersededById. O último corpo de resposta não está aqui — ele só aparece no detalhe da entrega. Filtros opcionais: status, eventType, endpointId, limit (padrão 25, máximo 100), offset.

#Inspecionar uma entrega

bash
curl "https://api.liqfy.com.br/v1/webhooks/deliveries/<DELIVERY_ID>" \
  -H "Authorization: Bearer $LIQFY_API_KEY"

Devolve o registro completo da entrega — incluindo lastResponseBody, paymentMethod e test — mais o log de tentativas e o endpoint a que ela pertence:

json
{
  "id": "…",
  "eventType": "charge.paid",
  "status": "FAILED",
  "attempts": 3,
  "lastStatusCode": 500,
  "lastError": "HTTP 500",
  "lastResponseBody": "{\"error\":\"…\"}",
  "logs": [
    {
      "id": "…",
      "deliveryId": "…",
      "attempt": 1,
      "statusCode": 500,
      "error": "HTTP 500",
      "responseBody": "{\"error\":\"…\"}",
      "duration": 120,
      "url": "https://loja.exemplo.com.br/hooks/liqfy",
      "createdAt": "2026-07-23T14:31:00.000Z"
    }
  ],
  "endpoint": { "url": "https://loja.exemplo.com.br/hooks/liqfy", "status": "ACTIVE" }
}

logs lista cada tentativa em ordem, cada uma com a URL usada naquele momento. Uma entrega que foi movida para outro endpoint traz cancelReason (ENDPOINT_REPLACED, ENDPOINT_INACTIVE ou ENDPOINT_DELETED) e, quando foi substituída, supersededById. Um id desconhecido devolve 404.

#Reenviar uma entrega que falhou ou foi cancelada

bash
curl -X POST "https://api.liqfy.com.br/v1/webhooks/deliveries/<DELIVERY_ID>/replay" \
  -H "Authorization: Bearer $LIQFY_API_KEY"

Zera o contador de tentativas e reenfileira na hora; responde { "id", "status": "PENDING", "attempts": 0, "nextRetryAt" }. Reenvio em massa fica em POST /v1/webhooks/deliveries/replay-bulk, com filtro opcional { status, endpointId, limit } (limit padrão 50, máximo 500; o status padrão é FAILED + CANCELLED), e responde { "requeued": <quantidade> }.

A semântica de filtros muda entre leitura e reenvio em massa. GET /deliveries e GET /events ignoram em silêncio um status inválido, uma data inválida ou um cursor malformado (você recebe uma página sem filtro). O POST /deliveries/replay-bulk, em vez disso, responde 400 num status desconhecido. Problema conhecido (documentado como está): o reenvio em massa também aceita status: DELIVERED ou PROCESSING e reenfileira essas entregas, enquanto o reenvio único acima recusa os dois com 400.

#Girar o segredo

bash
curl -X POST "https://api.liqfy.com.br/v1/webhooks/endpoints/<ENDPOINT_ID>/rotate-secret" \
  -H "Authorization: Bearer $LIQFY_API_KEY"

O segredo novo é devolvido uma vez, como { "id": "…", "secret": "…" }.

A rotação é uma troca instantânea no servidor — todo webhook que a Liqfy assinar depois de um rotate-secret bem-sucedido usa o segredo novo. Não existe janela de sobreposição do nosso lado.

Para girar sem perder evento, seu verificador precisa aceitar temporariamente os dois segredos durante o deploy:

js
// Tenta o novo primeiro, cai no antigo. Remova SEGREDO_ANTIGO depois que o deploy assentar.
const ok = verifica(req, SEGREDO_NOVO) || verifica(req, SEGREDO_ANTIGO);
if (!ok) return res.status(401).end();

Ordem das operações:

  1. Chame rotate-secret → guarde o novo ao lado do antigo.
  2. Faça deploy do verificador com os dois ativos.
  3. Deixe assentar por pelo menos um minuto (as retentativas em voo se resolvem).
  4. Remova o antigo no deploy seguinte.

#Atualizar ou apagar um endpoint

bash
curl -X PATCH "https://api.liqfy.com.br/v1/webhooks/endpoints/<ENDPOINT_ID>" \
  -H "Authorization: Bearer $LIQFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://loja.exemplo.com.br/hooks/liqfy-v2", "status": "ACTIVE"}'

curl -X DELETE "https://api.liqfy.com.br/v1/webhooks/endpoints/<ENDPOINT_ID>" \
  -H "Authorization: Bearer $LIQFY_API_KEY"

O PATCH atualiza só os campos que você mandar (url, events, status: ACTIVE/INACTIVE), responde { id, url, events, status } e nunca devolve o segredo. Ele responde 404 para endpoint desconhecido, 400 quando nenhum campo é enviado ou o status não é ACTIVE/INACTIVE, e 409 WEBHOOK_ENDPOINT_LIMIT quando uma reativação passa do limite (a edição é desfeita). O DELETE responde { "deleted": true } e desativa o endpoint em definitivo: ele sai da sua lista e não recebe mais nada, enquanto o histórico de entregas é mantido. As entregas ainda na fila dele migram para os seus outros endpoints ativos inscritos no mesmo evento — ver Garantias de entrega.

#Estatísticas

bash
curl "https://api.liqfy.com.br/v1/webhooks/stats" \
  -H "Authorization: Bearer $LIQFY_API_KEY"

Devolve a contagem de entregas por estado (PENDING, PROCESSING, DELIVERED, FAILED, CANCELLED) e o total.

#Dúvidas

P: Posso ter vários endpoints? R: Pode, até o limite de endpoints ativos da sua conta (10 com KYC aprovado, 2 sem — ver Limites e permissões) — é útil para separar homologação, produção e um destino de observabilidade. Cada URL só pode ser cadastrada uma vez por conta.

P: Um endpoint pode misturar eventos canônicos e legados? R: Pode. events num mesmo endpoint pode incluir as duas famílias (ex.: ["charge.paid", "kyc.approved"]); cada evento entregue usa o envelope/esquema de assinatura do próprio nome.

Inscrições antigas de webhook Nomes antigos de cobranças e saques não são aceitos em novas inscrições. Inscrições existentes continuam recebendo o mesmo formato de mensagem e assinatura. O painel mostra esses nomes com a indicação de remoção e seu substituto; o lojista pode removê-los e selecionar o nome canônico. O nome canônico usa o envelope versionado e a assinatura com timestamp.

P: O que acontece se meu endpoint ficar fora do ar por horas? R: A entrega continua sendo retentada por até 15 tentativas, com backoff de até 6 horas. Depois disso ela vai para CANCELLED, sem ser apagada — dá para reenviar manualmente ou puxar tudo pela API de conciliação.

Veja identificadores de liquidação e consulta para as garantias de E2E e retenção.