Skip to content
On this page

Developers › Payments

Refunds

All of a payment, or part of it, as many times as you need.

Verified 6 November 2026Explain this to your team →

View as Markdown
ts
// 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

statusWhat it means
pendingSent to the bank, and its answer has not reached us. The money may already be on its way back.
succeededThe money went back to the card. refund.succeeded fires.
failedThe bank refused it, or still had no trace of it some twenty minutes later. Nothing moved. refund.failed fires.
canceledRare. 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

ResponseWhy
400 parameter_invalid, param: amountMore 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_intentNothing has been captured. A hold that was never captured is released by cancelling the payment, not refunded.
400 refund_not_yet_possibleA 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_failedThe bank answered and said no. Nothing was returned, and the refund is recorded as failed.
503 gateway_unavailableThe 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:

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

A refund does not change the payment’s status. It stays 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.