---
id: docs.refunds
title: "Refunds"
canonical: https://docs.secureprocessing.app/refunds
last_verified: 2026-11-06
---

# Refunds

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

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

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

```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](https://docs.secureprocessing.app/errors.md) page gives.

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