Skip to main content
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.
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: 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 — R5backonaR5 back on a R100 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

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:
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:
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.
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.
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.

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.
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.

Webhook

A refund emits payment.refunded, with accessRevoked saying whether the download fell:
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.

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.
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.