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ê estorna | O 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.