Pular para o conteúdo

Começar

Reembolso

Devolver o dinheiro — e o que acontece com o acesso do comprador, que é a parte que ninguém pergunta antes.

Devolver dinheiro é uma chamada. A pergunta difícil vem depois: o comprador ainda tem o produto? Num produto digital ele já baixou, e o link continua no e-mail dele.

Esta página responde as duas.

O reembolso é contra o pagamento, não contra a fatura

Uma fatura pode ter várias tentativas de cobrança e só uma pegou o dinheiro. É essa que você estorna.

const [pagamento] = await infi.payments.listForInvoice(invoiceId);
await infi.payments.refund(pagamento.id, { reason: "cliente desistiu" });

Só um pagamento confirmed pode ser estornado, e de onde o dinheiro sai depende do seu modelo de coleta. Em BYOP o estorno roda na conta do provedor que você conectou: é o seu dinheiro voltando da sua conta. Em Infi Managed a cobrança foi recebida na nossa estrutura, então o estorno sai de lá e é descontado do seu repasse — inclusive de repasses futuros, se o valor já tiver sido repassado.

Total ou parcial: é o valor que decide o acesso

Omitir amount estorna tudo. E aqui está a regra que importa:

Você estornaO download do comprador
tudo (amount omitido)para de funcionar
parte (amount: "5.00")continua funcionando

A lógica é essa: um estorno total desfaz a venda, então a capacidade que a venda criou tem que morrer com ela. Um estorno parcial não desfaz a venda — R$5 de volta num guia de R$100 é cortesia, não cancelamento — e cortar o arquivo ali puniria justamente o cliente que você acabou de tentar agradar.

Um valor acima do total é tratado como total, não recusado.

Quando você quer o contrário

// devolve tudo e deixa ele ficar com o arquivo
await infi.payments.refund(pagamento.id, { revokeAccess: false });
 
// corta o acesso mesmo estornando só uma parte
await infi.payments.refund(pagamento.id, { amount: "5.00", revokeAccess: true });

revokeAccess: false é a política de muito infoproduto: brigar custa mais que o arquivo. Não mande o campo se você não quer sobrescrever — a derivação acima é o comportamento certo em quase todo caso.

O que o comprador vê depois

O link antigo dele responde 410 Gone, com error_code e message no corpo da resposta:

{
  "error_code": "download_revoked",
  "message": "This download is no longer available: the purchase behind it was refunded."
}

É 410 e não 404 de propósito. Quem tem o token já provou que tinha o token, então "isso existiu" não vaza nada — e um 404 pareceria link quebrado, o que transforma um reembolso resolvido num ticket de suporte.

Na sua página, o grant volta com revokedAt e continua na lista:

const [grant] = await infi.invoices.deliverable(invoiceId);
if (grant?.revokedAt) mostrarAvisoDeEstorno();
else if (grant) mostrarBotao(grant.downloadUrl);

Ele não desaparece porque um grant que sumisse pareceria entrega que nunca rodou — dois problemas bem diferentes com a mesma cara.

Ler o que você estornou

status não responde quanto voltou: um estorno parcial também deixa o pagamento como refunded. Quem responde é refundedAmount.

const p = await infi.payments.get(pagamento.id);
p.status;          // "refunded"  — mesmo tendo voltado só R$5
p.refundedAmount;  // "5"         — R$5 devolvidos
 
await infi.payments.refunds(pagamento.id);
// [{ id, amount: "5.00", createdAt }]

Os valores são strings decimais, sem garantia de duas casas: "5" e "5.00" representam o mesmo valor. Não compare o texto bruto para decidir quanto voltou.

refunds() retorna os registros individuais, com valor, data e identificador. O campo reason é opcional no retorno.

Guarde o motivo no seu sistema

No sandbox, reason pode não voltar na listagem mesmo quando enviado no reembolso. Se você precisa dele para atendimento ou auditoria, registre o motivo no seu sistema junto do ID do pagamento. Não dependa desse campo no retorno.

A fatura continua paid

De propósito. A fatura registra que foi paga, porque foi — e contabilidade não apaga fato, ela lança o contrário dele. O estorno é um registro próprio, com seu lançamento reverso no ledger.

Consequência prática: se você somar faturas paid pro seu relatório de vendas, uma venda estornada entra inteira. Subtraia refundedAmount dos pagamentos.

Crédito pré-pago NÃO volta

Se a compra era um pacote de créditos, o estorno devolve o dinheiro e não remove os créditos — eles continuam gastáveis. A carteira só tem lançamentos de concessão e consumo, e um estorno não escreve nada nela.

O motivo de não ser automático: se o comprador já consumiu 800 de 1000 créditos, não existe resposta óbvia — e escrever a errada num saldo é pior que não escrever. Por enquanto, se você vende crédito, debite na mão o que sobrou depois de estornar.

Webhook

O estorno emite payment.refunded, com accessRevoked dizendo se o download caiu:

{ "paymentId": "…", "invoiceId": "…", "amount": "100.00",
  "currency": "BRL", "accessRevoked": true }

accessRevoked está aí porque o seu sistema quase sempre tem acesso próprio pra cortar — assinatura, feature flag, cargo no Discord. Assine em webhooks.

Chargeback é a mesma mecânica, sem você

Quando o comprador contesta no banco, o provedor manda PAYMENT_REFUNDED ou o evento de chargeback e o mesmo caminho roda: pagamento vira charged_back, lançamento reverso, e o acesso cai — a rede leva o valor inteiro, então a derivação por valor revoga. Você não precisa fazer nada, e não tem como impedir.

O evento emitido é payment.chargeback.

Estornar duas vezes é seguro

O caminho de reversão só age sobre um pagamento confirmed. Um webhook repetido do provedor, ou um retry seu, não lança no ledger de novo nem reescreve quando o acesso caiu — e a data da revogação é preservada, porque é dela que uma disputa depende.