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

# Set Refund Status (Sandbox)

> Force a refund to any status on demand and fire the matching webhook, so one refund can exercise every branch of your handler.

<Warning>
  **Sandbox only.** This endpoint returns `400` in production. It operates only on refunds created through the **Simulator** payment gateway — a refund on a real gateway is rejected.
</Warning>

## Overview

A real refund walks `INITIATED → REFUNDED` or `INITIATED → FAILED`, driven by the bank on its own timetable. Statuses like `REVERSED` or `BANK_REJECTED_REFUND` are rare enough that you may never see one before it happens in production.

This endpoint forces a refund straight to any status and fires the same webhook a real change would, so you can exercise every branch of your handler deliberately.

## Statuses

| Value                  | Default message            | Meaning                          |
| ---------------------- | -------------------------- | -------------------------------- |
| `INITIATED`            | `Refund initiated`         | Accepted, not yet processing     |
| `PROCESSING`           | `Refund processing`        | In flight at the bank            |
| `REFUNDED`             | `Simulated refund settled` | Successfully refunded            |
| `FAILED`               | `Simulated refund failed`  | Refund failed                    |
| `REVERSED`             | `Refund reversed`          | Reversed after being sent        |
| `ON_HOLD`              | `Refund on hold`           | Held for review                  |
| `BANK_REJECTED_REFUND` | `Refund rejected by bank`  | Rejected by the beneficiary bank |

Setting `REFUNDED` also stamps `refunded_at` with the current time, if it is not already set.

<Note>
  **Terminal statuses are reversible here.** You may move a refund out of `REFUNDED` or `FAILED` and back again. This is deliberate — it lets one refund cover the whole matrix instead of needing a fresh one per branch.
</Note>

## Which webhook fires

| Target status          | Webhook                                                                   |
| ---------------------- | ------------------------------------------------------------------------- |
| `REFUNDED`             | [`PAYMENT_REFUNDED`](/api-reference/v3/webhooks/payment-refunded)         |
| `FAILED`               | [`REFUND_FAILED`](/api-reference/v3/webhooks/refund-failed)               |
| `PROCESSING`           | [`REFUND_STATUS_UPDATE`](/api-reference/v3/webhooks/refund-status-update) |
| `REVERSED`             | [`REFUND_STATUS_UPDATE`](/api-reference/v3/webhooks/refund-status-update) |
| `ON_HOLD`              | [`REFUND_STATUS_UPDATE`](/api-reference/v3/webhooks/refund-status-update) |
| `BANK_REJECTED_REFUND` | [`REFUND_STATUS_UPDATE`](/api-reference/v3/webhooks/refund-status-update) |
| `INITIATED`            | **None** — it is the starting state, not a transition worth notifying     |

<Warning>
  **Webhooks fire only on a genuine transition.** Setting a refund to the status it already holds returns `200` and sends nothing. To re-trigger an event, move the refund to a different status first.
</Warning>

Your `status_message` is carried into the webhook, so you can assert on it end to end.

## Case asymmetry

You send `"status": "FAILED"` and receive `"refund_status": "failed"`.

Requests take **uppercase**; responses return **lowercase**. Sending `"refunded"` is rejected as an invalid choice — the most common cause of a `400` on this endpoint. Uppercase the response value before comparing it to what you sent.

## Exercising every branch

Walk one refund through each status, checking your handler at each step:

```
INITIATED → PROCESSING → ON_HOLD → BANK_REJECTED_REFUND → FAILED → REFUNDED
```

Each transition fires the corresponding event from the table above.

### Testing a failed refund

```bash theme={null}
curl -X POST "https://<your-sandbox-host>/pg/refunds/RF7019490071/simulate-status/" \
  -H "X-Client-ID: <client-id>" \
  -H "X-Client-Secret: <client-secret>" \
  -H "Content-Type: application/json" \
  -d '{
        "status": "FAILED",
        "status_message": "Beneficiary account closed"
      }'
```

Your endpoint receives `REFUND_FAILED` carrying your custom message.

## Errors

`400` responses use `error.code` = `ERR_REFUND_002`:

| `details`                                                        | Cause                            |
| ---------------------------------------------------------------- | -------------------------------- |
| `status: "…" is not a valid choice.`                             | Unknown status, or lowercase     |
| `status: This field is required.`                                | `status` omitted                 |
| `refund_uid: RF… is not on the simulator gateway`                | Refund created on a real gateway |
| `error: Refund status simulation is not available in production` | Called in production             |

<Warning>
  **`404` does not use the standard envelope.** A refund that does not exist — or belongs to another merchant — returns a bare `{"detail": "..."}` rather than the `success`/`error` shape. Handle that separately.

  Note this is a `404`, not a `403`: refunds are scoped to your account and its sub-merchants, and someone else's refund is simply not found.
</Warning>

A `500` on this endpoint uses `ERR_SERVICE_ERROR_000`, unlike its `400`s.

## Related

<CardGroup cols={2}>
  <Card title="Mark as Settled" icon="money-check-dollar" href="/api-reference/v3/settlement/mark-settled">
    Settle payments on demand, refunds and chargebacks included.
  </Card>

  <Card title="Create Refund" icon="rotate-left" href="/api-reference/v3/refunds/create">
    Create the refund you are going to drive.
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /pg/refunds/{refund_id}/simulate-status/
openapi: 3.0.0
info:
  title: Eximpe Payment Gateway API
  description: >-
    API for payment processing and order management through Eximpe payment
    gateway. This specification is for v3 API version.
  license:
    name: Proprietary
  version: 3.0.0
servers:
  - url: https://api-pacb-uat.eximpe.com
    description: Payment Gateway Sandbox URL
security:
  - clientAuth: []
    clientSecretAuth: []
    apiVersionHeader: []
tags:
  - name: Card Tokens
  - name: Merchants
  - name: Orders
  - name: Payment Links
  - name: Payments
  - name: Refunds
  - name: Settlements
  - name: Subscriptions
  - name: Virtual Bank Accounts
    description: Create and manage ICICI virtual accounts and read collected payments.
paths:
  /pg/refunds/{refund_id}/simulate-status/:
    post:
      tags:
        - Refund
      summary: Set refund status (sandbox)
      description: >-
        Forces a refund straight to any status, bypassing the normal `INITIATED
        → REFUNDED/FAILED` progression, and fires the same webhook a real status
        change would. Lets one refund exercise every branch of your refund
        handler, including statuses a real rail reaches rarely.


        **Sandbox only.** This endpoint returns `400` in production, and
        operates only on transactions created through the **Simulator** payment
        gateway — a real Cashfree or ICICI transaction is rejected.


        Unlike a real rail, a refund may be moved **out of** a terminal status
        (`REFUNDED` / `FAILED`) and back again — deliberately, so you do not
        need a fresh refund per branch.


        **PSP callers must send `X-Merchant-ID`.**
      parameters:
        - name: refund_id
          in: path
          required: true
          schema:
            type: string
          description: UID of the refund.
          example: RF7019490071
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - status
              properties:
                status:
                  type: string
                  enum:
                    - INITIATED
                    - PROCESSING
                    - REFUNDED
                    - FAILED
                    - REVERSED
                    - ON_HOLD
                    - BANK_REJECTED_REFUND
                  description: >-
                    Target status. **Uppercase** — `"refunded"` is rejected,
                    `"REFUNDED"` is accepted.
                  example: FAILED
                status_message:
                  type: string
                  nullable: true
                  description: >-
                    Custom message carried into the webhook. Omit for the
                    default for that status.
                  example: Beneficiary account closed
                bank_ref_num:
                  type: string
                  nullable: true
                  description: >-
                    Bank reference to stamp on the refund. Left unchanged if
                    omitted.
                  example: BANKREF123
            example:
              status: FAILED
              status_message: Beneficiary account closed
              bank_ref_num: BANKREF123
      responses:
        '200':
          description: >-
            Refund updated. A transition to a status the refund already held
            still returns 200, but sends no webhook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Refund status updated successfully
                  data:
                    type: object
                    properties:
                      refund_id:
                        type: string
                        example: RF7019490071
                      payment_id:
                        type: string
                        example: PR1195868571
                      refund_amount:
                        type: string
                        description: In **rupees** with 2 decimals.
                        example: '250.00'
                      refund_status:
                        type: string
                        description: >-
                          Current status, **lowercase** — you send `FAILED` and
                          receive `failed`.
                        example: failed
                      refunded_at:
                        type: string
                        format: date-time
                        nullable: true
                        description: Stamped when the refund first reaches `REFUNDED`.
                        example: '2026-08-12T11:32:25.256618Z'
                      message:
                        type: string
                        example: Beneficiary account closed
                      settlement_details:
                        type: object
                        description: >-
                          Settlement breakdown once the refund is settled; `{}`
                          otherwise.
                      reference_id:
                        type: string
                        nullable: true
                        description: Your reference, if supplied at refund creation.
                      created_at:
                        type: string
                        format: date-time
                        example: '2026-08-12T11:32:25.255318Z'
              example:
                success: true
                message: Refund status updated successfully
                data:
                  refund_id: RF7019490071
                  payment_id: PR1195868571
                  refund_amount: '250.00'
                  refund_status: failed
                  refunded_at: '2026-08-12T11:32:25.256618Z'
                  message: Beneficiary account closed
                  settlement_details: {}
                  reference_id: null
                  created_at: '2026-08-12T11:32:25.255318Z'
        '400':
          description: >-
            Invalid or missing `status`, a refund not on the Simulator gateway,
            or the endpoint called in production. `error.code` is
            `ERR_REFUND_002`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/v3_ErrorResponse'
              example:
                success: false
                error:
                  code: ERR_REFUND_002
                  message: Validation error
                  details:
                    status: '"NOT_A_STATUS" is not a valid choice.'
        '404':
          description: >-
            No such refund for this merchant. **This response does not use the
            standard envelope** — it returns a bare `detail` string.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
              example:
                detail: No DomesticPaymentRefund matches the given query.
        '500':
          description: >-
            Unexpected processing failure. `error.code` is
            `ERR_SERVICE_ERROR_000` here, unlike the 400s on this endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/v3_ErrorResponse'
      security:
        - clientAuth: []
          clientSecretAuth: []
          merchantAuth: []
          apiVersionHeader: []
components:
  schemas:
    v3_ErrorResponse:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          description: Indicates if the request was successful
        error:
          $ref: '#/components/schemas/v3_ErrorDetails'
    v3_ErrorDetails:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Error code (e.g., ERR_ORDER_002)
        message:
          type: string
          description: Error message
        details:
          type: object
          description: Detailed validation error information with field-specific errors
          additionalProperties:
            type: string
            description: Error message for the specific field
  securitySchemes:
    clientAuth:
      type: apiKey
      name: X-Client-ID
      in: header
      description: >-
        **Client Application ID** - Your unique application identifier used to
        authenticate API requests. You can find your Client ID in the Developer
        Settings section of the merchant dashboard.
      x-displayName: Client ID
      x-example: your-client-id
    clientSecretAuth:
      type: apiKey
      name: X-Client-Secret
      in: header
      description: >-
        **Client Secret Key** - Your secret key used alongside the Client ID for
        secure authentication. Keep this confidential and never expose it in
        client-side code. Available in the Developer Settings section of the
        merchant dashboard.
      x-displayName: Client Secret
      x-example: your-client-secret
    apiVersionHeader:
      type: apiKey
      name: X-API-Version
      in: header
      description: >-
        **API Version** - Specifies which version of the API to use (e.g.,
        '1.X.X', '2.X.X', or '3.X.X'). This header allows you to control which
        API version your integration uses. Default version information is
        available in the Developer Settings.
      x-displayName: API Version
      x-example: 3.0.0
    merchantAuth:
      type: apiKey
      name: X-Merchant-ID
      in: header
      description: >-
        **Merchant Identifier** - The unique ID for the merchant account. This
        is required for PSP (Payment Service Provider) merchants who manage
        multiple merchant accounts. You can find merchant IDs in the Merchant
        Management section of the dashboard.
      x-displayName: Merchant ID
      x-example: your-merchant-id

````