> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beinfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Refunds

> Giving the money back — and what happens to the buyer's access, which is the part nobody asks about first.

Giving money back is one call. The hard question comes after: **does the buyer
still have the product?** With a digital product they already downloaded it, and
the link is still in their inbox.

This page answers both.

## The refund is against the payment, not the invoice

An invoice can have several charge attempts and only one took the money. That is
the one you refund.

```ts theme={null}
const [payment] = await infi.payments.listForInvoice(invoiceId);
await infi.payments.refund(payment.id, { reason: "customer changed their mind" });
```

Only a **`confirmed`** payment can be refunded, and where the money comes from
depends on your collection model. Under **BYOP** the refund runs on the provider
account you connected: it is your money leaving your account. Under **Infi
Managed** the charge was received on our structure, so the refund leaves from
there and is deducted from your payout — including future payouts, if the amount
was already paid out.

## Full or partial: the amount decides the access

Omitting `amount` refunds everything. And here is the rule that matters:

| You refund                    | The buyer's download |
| ----------------------------- | -------------------- |
| everything (`amount` omitted) | **stops working**    |
| part of it (`amount: "5.00"`) | keeps working        |

The logic: a full refund undoes the sale, so the capability the sale created has
to die with it. A partial refund does **not** undo the sale — R$5 back on a
R$100 guide is goodwill, not a cancellation — and cutting the file there would
punish exactly the customer you just tried to please.

An amount above the total is treated as full, not refused.

### When you want the opposite

```ts theme={null}
// refund everything and let them keep the file
await infi.payments.refund(payment.id, { revokeAccess: false });

// cut access even when refunding only part
await infi.payments.refund(payment.id, { amount: "5.00", revokeAccess: true });
```

`revokeAccess: false` is the policy of plenty of info-products: fighting costs
more than the file. **Do not send the field** if you do not mean to override —
the derivation above is the right behaviour in almost every case.

## What the buyer sees afterwards

Their old link answers **`410 Gone`**, with `error_code` and `message` in the
body:

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

410 and not 404, on purpose. Whoever holds the token already proved they hold
it, so "this existed" leaks nothing — and a 404 would look like a broken link,
which turns a settled refund into a support ticket.

On your page, the grant comes back with `revokedAt` and **stays in the list**:

```ts theme={null}
const [grant] = await infi.invoices.deliverable(invoiceId);
if (grant?.revokedAt) showRefundNotice();
else if (grant) showButton(grant.downloadUrl);
```

It does not disappear, because a grant that vanished would look like fulfilment
that never ran — two very different problems wearing the same face.

## Reading what you refunded

`status` becomes `refunded` only once the whole amount is back. A partial
refund leaves the payment `confirmed`; what says how much came back is
`refundedAmount`.

```ts theme={null}
const p = await infi.payments.get(payment.id);
p.status;          // "confirmed" — only R$5 of R$100 returned
p.refundedAmount;  // "5"         — R$5 given back

await infi.payments.refunds(payment.id);
// [{ id, amount: "5.00", createdAt }]
```

The values are decimal strings with no guarantee of two places: `"5"` and
`"5.00"` are the same amount. Do not compare the raw text to decide how much
came back.

`refunds()` returns the individual records, with amount, date and identifier.
The `reason` field is optional in the response.

<Warning>
  **Keep the reason in your own system.** In sandbox, `reason` may not come back in the listing even when it was sent with
  the refund. If you need it for support or audit, record the reason in your own
  system alongside the payment id. Do not depend on that field in the response.
</Warning>

## The invoice stays `paid`

On purpose. The invoice records that it was paid, because it **was** — and
accounting does not erase a fact, it posts the opposite of it. The refund is its
own record, with its own reversing entry in the ledger.

Practical consequence: if you sum `paid` invoices for your sales report, a
refunded sale counts in full. Subtract `refundedAmount` from the payments.

<Warning>
  **Prepaid credit does NOT come back.** If the purchase was a credit package, the refund returns the money and does
  **not** remove the credits — they stay spendable. The wallet only has grant and
  consume entries, and a refund writes nothing in it.

  Why it is not automatic: if the buyer already consumed 800 of 1000 credits there
  is no obvious answer — and writing the wrong one into a balance is worse than
  writing nothing. For now, if you sell credit, debit what is left by hand after
  refunding.
</Warning>

## Webhook

A refund emits `payment.refunded`, with `accessRevoked` saying whether the
download fell:

```json theme={null}
{ "paymentId": "…", "invoiceId": "…", "amount": "100.00",
  "currency": "BRL", "accessRevoked": true }
```

`accessRevoked` is there because your own system almost always has access of its
own to cut — a subscription, a feature flag, a Discord role. Subscribe in
[webhooks](https://beinfi.com/en/webhooks).

## A chargeback is the same mechanic, without you

When the buyer disputes at their bank, the provider sends `PAYMENT_REFUNDED` or
the chargeback event and the same path runs: the payment becomes
`charged_back`, a reversing entry is posted, and access falls — the network takes
the full amount, so the amount-based derivation revokes. You do not have to do
anything, and you cannot prevent it.

The emitted event is `payment.chargeback`.

<Info>
  **Refunding twice is safe.** The reversal path only acts on a `confirmed` payment. A repeated provider
  webhook, or a retry of your own, does not post to the ledger again and does not
  rewrite when access fell — and the revocation date is preserved, because that is
  what a dispute turns on.
</Info>
