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

# Refunds: How to Test Your Integration

> Test Normal and Instant refunds on your S2S integration in Test Mode, including failures and the Instant-to-Normal fallback.

Available in
🇮🇳 India

Refunds are always issued against an existing captured payment, so you create a test payment and then refund it. The refund amount decides the outcome. Specific amounts may fail and all others succeed. Whether a refund is Normal or Instant depends on the Refund Instantly checkbox, not the amount. Everything runs on your test keys and is fully simulated. No real money moves.

## 1. Test Mode Coverage

| Area | What you can test | Available on test mode |
| - | - | - |
| Create a Normal refund (success) | Issue a Normal refund against a captured test payment, full or partial. | Supported |
| Create a Normal refund (failure) | Simulate a Normal refund that moves to failed. | Not supported |
| Create an Instant refund (success) | Instant at optimum speed; the per-refund fee is deducted from your test balance. | Not supported |
| Instant refund failure & fallback to Normal | An instant refund that cannot be processed instantly falls back to normal speed. | Supported |
| Refund API errors | Request validation errors (invalid refund amount, speed, uncaptured payment, and more). | Supported |
| Bank movement & references | Real money transfer, actual customer credit, and real ARN / RRN / UTR. | Supported in Live mode only |

## 2. Test Kit

You need a captured test payment to refund. Payment first, then refund: a refund always acts on an existing captured payment, so your end-to-end flow is to create and capture a test payment, then refund it.

* Create a test payment on an order and capture it, see [Orders](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/orders.md) and the method pages, then capture with the Capture Payment API (`POST /v1/payments/:id/capture`).
* Keep the payment amount greater than the refund amount. Capture more than ₹24.68 so one payment covers every case, including the ₹24.68 failure.
* Refund it with `POST /v1/payments/:id/refund` (see the scenarios below).

| Use it to test | Refund amount (₹) | Notes |
| - | - | - |
| A successful Normal refund | Any amount other than ₹24.68 and ₹14.68 | Leave Refund Instantly unticked. |
| A successful Instant refund | Any amount other than ₹24.68 and ₹14.68 | Tick Refund Instantly. |
| A failed Normal refund | ₹24.68 | <ul><li>Leave Refund Instantly unticked.</li><li>The test payment must be greater than ₹24.68.</li></ul> |
| Instant refund failed and fallback to Normal | ₹14.68 | <ul><li>Tick Refund Instantly.</li><li>The test payment must be greater than ₹14.68.</li></ul> |

## 3. Scenarios to Test Your Integration

Grouped into must-have refund flows, other common refund flows, and error scenarios. Click a Scenario ID for its details.

### 3.1 Must-have refund scenarios

<table>
  <colgroup>
    <col width="10%" />

    <col width="24%" />

    <col width="24%" />

    <col width="42%" />
  </colgroup>

  <thead><tr><th>Scenario ID</th><th>Scenario</th><th>Trigger creds</th><th>Steps to follow</th></tr></thead>

  <tbody>
    <tr><td>RFND-01</td><td>Create a Normal refund (success)</td><td>A captured payment, any refund amount (e.g. `amount: 100`), at `speed: "normal"`</td><td>`POST /v1/payments/:id/refund` with test keys, `amount: 100` and `speed: "normal"`. Confirm it moves from `pending` to `processed`, with `speed_processed: "normal"`. Store the refund `id` against your payment or order.</td></tr>
    <tr><td>RFND-02</td><td>Normal refund fails (coming soon)</td><td>A captured payment above ₹24.68, `amount: 2468` (₹24.68) at `speed: "normal"`</td><td>`POST /v1/payments/:id/refund` with `amount: 2468` and `speed: "normal"`. Confirm it moves from `pending` to `failed`. Handle the `refund.failed` webhook and reconcile.</td></tr>
  </tbody>
</table>

### 3.2 Other common refund scenarios

<table>
  <colgroup>
    <col width="10%" />

    <col width="24%" />

    <col width="24%" />

    <col width="42%" />
  </colgroup>

  <thead><tr><th>Scenario ID</th><th>Scenario</th><th>Trigger creds</th><th>Steps to follow</th></tr></thead>

  <tbody>
    <tr><td>RFND-03</td><td>Create an Instant refund (success) (coming soon)</td><td>A captured payment, any refund amount (e.g. `amount: 100`), at `speed: "optimum"`</td><td>`POST /v1/payments/:id/refund` with `amount: 100` and `speed: "optimum"`. Confirm `speed_requested: "optimum"`, `speed_processed: "instant"`, and it moves from `pending` to `processed`. The per-refund fee is deducted from your test balance.</td></tr>
    <tr><td>RFND-04</td><td>Instant refund falls back to Normal</td><td>A captured payment above ₹14.68, `amount: 1468` (₹14.68) at `speed: "optimum"`</td><td>`POST /v1/payments/:id/refund` with `amount: 1468` and `speed: "optimum"`. Confirm it moves from `pending` to `processed` with `speed_requested: "optimum"` and `speed_processed: "normal"`. The instant fee is credited back to your test balance.</td></tr>
  </tbody>
</table>

> [!NOTE]
>
> **Seeing the pending state**
>
> Whether you see the `pending` refund state depends on your account. It is enabled by default for merchants onboarded on or after 28 April 2026, and for some onboarded earlier. If it isn't enabled for your account, a refund appears directly as `processed` (or `failed`), without a visible `pending` state.

### 3.3 Refund error scenarios

Trigger each with a bad request and read the error object to handle it.

| Trigger | `error.code` | `error.reason` | `error.description` |
| - | - | - | - |
| Refund a payment that belongs to a different account | BAD\_REQUEST\_ERROR | input\_validation\_failed | Payment does not belong to the merchant |
| Call the API with invalid credentials | BAD\_REQUEST\_ERROR | not returned | Authentication failed |
| Refund a payment that is not captured | BAD\_REQUEST\_ERROR | NA | The payment status should be captured for action to be taken |
| Refund an amount less than ₹1.00 | BAD\_REQUEST\_ERROR | input\_validation\_failed | The amount must be atleast INR 1.00 |
| Pass an unsupported `speed` value | BAD\_REQUEST\_ERROR | input\_validation\_failed | The selected speed is invalid. |
| Refund a payment that is already fully refunded | BAD\_REQUEST\_ERROR | NA | The payment has been fully refunded already |
| Refund more than the payment amount | BAD\_REQUEST\_ERROR | NA | The total refund amount is greater than the refund payment amount |
| Pass a non-integer `amount` | BAD\_REQUEST\_ERROR | input\_validation\_failed | The amount must be an integer. |

## 4. Scenario Details

**RFND-01: Create a Normal refund (success)**

*Before you start:* a captured test payment and your test API keys.

*How to test:* `POST /v1/payments/:id/refund` with `amount: 100` and `speed: "normal"`.

*Expected result:* HTTP 200 with a refund ID (`rfnd_…`); it moves from `pending` to `processed`, with `speed_processed: "normal"`.

*Webhooks:* `refund.created`, then `refund.processed`.

*Your integration should:* mark the refund successful on the `processed` state or the `refund.processed` webhook, and store the refund ID.

**RFND-02: Normal refund fails**

*Before you start:* a captured test payment greater than ₹24.68.

*How to test:* `POST /v1/payments/:id/refund` with `amount: 2468` (₹24.68) and `speed: "normal"`.

*Expected result:* the refund moves from `pending` to `failed`.

*Webhooks:* `refund.created`, then `refund.failed`.

*Your integration should:* handle the `failed` state / `refund.failed` webhook, and retry or reconcile.

**RFND-03: Create an Instant refund (success)**

*Before you start:* a captured test payment, with Instant Refunds enabled on your test account.

*How to test:* `POST /v1/payments/:id/refund` with `amount: 100` and `speed: "optimum"`.

*Expected result:* `speed_requested: "optimum"`, `speed_processed: "instant"`; it moves from `pending` to `processed`. The per-refund fee is deducted from your test balance.

*Webhooks:* `refund.created`, then `refund.processed`.

*Your integration should:* read `speed_processed = instant` and account for the instant-refund fee.

**RFND-04: Instant refund falls back to Normal**

*Before you start:* a captured test payment greater than ₹14.68.

*How to test:* `POST /v1/payments/:id/refund` with `amount: 1468` (₹14.68) and `speed: "optimum"`.

*Expected result:* the instant refund cannot be processed instantly, so it falls back to normal speed. It moves from `pending` to `processed` with `speed_requested: "optimum"` and `speed_processed: "normal"`; the instant fee is credited back.

*Webhooks:* `refund.created`, `refund.speed_changed`, then `refund.processed`.

*Your integration should:* not assume instant always succeeds. Listen for `refund.speed_changed`, read `speed_processed`, and show the normal 5-7 working day timeline.

## 5. Webhooks: What Fires, and What It Lets You Decide

Every scenario above delivers a specific set of events, in a specific order. This table is the one place that says which, and, the part that actually matters, what each one entitles you to do. Most integration bugs that reach production are not missing webhooks; they are acting on the wrong one.

| Scenario | What it is | Events, in order | What it lets you decide |
| - | - | - | - |
| RFND-01 | Create a Normal refund (success) | `refund.created`, `refund.processed` | The refund is accepted, not settled. Do not tell the customer the money is back yet. The money has left Razorpay. This is when you confirm the refund to the customer. |
| RFND-02 | Normal refund fails | `refund.created`, `refund.failed` | The refund is accepted, not settled. Do not tell the customer the money is back yet. The refund did not go through. It needs a retry or a manual payout, it will not resolve itself. |
| RFND-03 | Create an Instant refund (success) | `refund.created`, `refund.processed` | The refund is accepted, not settled. Do not tell the customer the money is back yet. The money has left Razorpay. This is when you confirm the refund to the customer. |
| RFND-04 | Instant refund falls back to Normal | `refund.created`, `refund.speed_changed`, `refund.processed` | The refund is accepted, not settled. Do not tell the customer the money is back yet. The refund fell back from Instant to Normal. The customer-facing ETA you quoted is now wrong. The money has left Razorpay. This is when you confirm the refund to the customer. |

## 6. Common Integration Gaps to Avoid

| Gap | What goes wrong in production | What to do | Test with |
| - | - | - | - |
| Not handling the failed refund state | A refund that fails is treated as done; the customer never gets the money back. | Listen for the `failed` state / `refund.failed` webhook and reconcile. | RFND-02 (₹24.68) |
| Assuming an instant refund is always instant | You tell the customer "refunded instantly" when it actually fell back to normal speed. | Listen for the `refund.speed_changed` webhook and read `speed_processed`, not just `speed_requested`. | RFND-04 (₹14.68) |
| Ignoring the instant-refund fee | Settlement and books don't match because the per-refund fee was not accounted for. | Account for the fee on instant refunds; expect it credited back on fallback. | RFND-03 / RFND-04 |
| Refunding more than captured, or an uncaptured payment | Live refund calls fail with validation errors your code doesn't handle. | Validate the refund amount ≤ captured amount and payment `status` = `captured` before refunding. | Refund error scenarios (3.3) |

### Related Information

* [Test Your S2S Integration](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s.md)
* [Orders](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/orders.md)
* [Best Practices](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices)
