Developers › Payments
Refunds
Verified 6 November 2026Explain this to your team →
// everything that is left
await sp.refunds.create({ payment_intent: intent.id });
// or part of it
await sp.refunds.create({
payment_intent: intent.id,
amount: 2000,
reason: "requested_by_customer",
});Our fee comes back with it
On the standard arrangement our fee is returned in proportion, so a fully refunded sale costs you what it would have cost if it had never happened, and a sequence of partial refunds returns the fee exactly once in total rather than once per refund. Some negotiated rates keep the fee on a refund instead; yours says which, and it never changes for a payment already taken.
Reversals happen automatically
A full refund of a payment that has not settled yet is sent to the bank as a reversal, which is faster and often never appears on the cardholder’s statement at all. Everything else is an ordinary refund. You do not choose — we use whichever the bank will accept.
Amounts and currency
A refund is in the payment’s currency and minor units: refunding 2000 of a USD payment returns US$20.00, and the refund’s currency is usd. The card is credited in Belize dollars at the rate the payment was taken at, so a full refund returns exactly what was charged.
A refund can be pending
| status | What it means |
|---|---|
| pending | Sent to the bank, and its answer has not reached us. The money may already be on its way back. |
| succeeded | The money went back to the card. refund.succeeded fires. |
| failed | The bank refused it, or still had no trace of it some twenty minutes later. Nothing moved. refund.failed fires. |
| canceled | Rare. After checking with the bank: it did make this refund, but the money is already counted under another refund on the same payment. Nothing is owed and no event fires. Do not refund again. |
Most refunds come back succeeded from the request itself. One that comes back pending settles on its own: every two minutes we read the bank’s own records, and the refund becomes succeeded or failed with the webhook to say which — or, rarely, canceled (above). Do not refund it again. Its amount is held against what is left to refund, so a second refund cannot take the total past the payment — it is refused rather than sent. A refund can stay pending longer while it is held: when the bank has sent word of a refund its records do not yet show, or in the rare case the bank’s records and ours disagree, it waits — still holding its amount — until that is resolved, rather than being failed and inviting a second refund of money that may already have gone back.
Every refund also fires refund.created the moment it is accepted, before the bank has answered. Fulfil a refund on refund.succeeded, not on that.
When a refund is refused
| Response | Why |
|---|---|
400 parameter_invalid, param: amount | More than is left to refund — counting refunds still pending. The message says how much remains, and how much of that is pending. |
400 parameter_invalid, param: payment_intent | Nothing has been captured. A hold that was never captured is released by cancelling the payment, not refunded. |
400 refund_not_yet_possible | A partial refund on a payment the bank has not settled yet. The bank refunds only settled payments, and a partial amount cannot be reversed. Retry once it has settled, with a new Idempotency-Key — nothing was refunded, so a new key cannot refund twice. A new key is safe for this code only: branch on the code, never on the status. If nothing has been refunded yet, a full refund can be made now. |
402 card_declined, decline_code: refund_failed | The bank answered and said no. Nothing was returned, and the refund is recorded as failed. |
503 gateway_unavailable | The refund was never sent to the bank, so nothing was refunded. It is safe to try again. |
With the SDK (0.2.1 or later), refund_not_yet_possible has its own check. Calling the method again later without an idempotencyKey sends a fresh key on its own:
import { SecureProcessingError } from "@secureprocessing/node";
try {
await sp.refunds.create({ payment_intent: intent.id, amount: 2000 });
} catch (err) {
if (err instanceof SecureProcessingError && err.isRefundNotYetPossible) {
// Nothing was refunded. Try again once the bank has settled the payment.
scheduleRetryAfterSettlement(intent.id);
} else {
throw err;
}
}There is no error for “the bank did not answer”: that is a pending refund, for the reason the errors page gives.
succeeded, and amount_refunded goes up. That is deliberate: a payment you refunded half of is not the same thing as one that never succeeded, and treating them alike makes your own reporting wrong.Refunds made at the bank
If a refund is made directly at the bank rather than through us, we find it and record it with origin: "bank_portal". You get a refund.succeeded webhook for it like any other, so your records and the bank’s do not silently diverge.