<!-- ZenPays documentation · https://docs.zenpayz.com/docs/rest-api/endpoints/refunds/create-refund -->

# Create Refund

Create a refund for a completed INR transaction. Supports full and partial refunds. Refunds are processed as payouts to the customer's bank account via IMPS.

<Diagram
  slug="refund-flow"
  alt="A refund is created and funds are immediately held. It is either auto-approved when auto-refund is enabled, or waits for ZenPays to approve it. Once approved it is sent to the provider, and on completion the held funds are debited. A rejected refund, or one that fails at the provider, releases the hold back to your balance."
  caption="How a refund is approved, sent and settled"
/>

<EndpointHeader verb="POST" path="/merchant/api/v1/refund-intents" />

## Request

### Headers

<ParamTable
  label="Header"
  rows={[
    { name: "Authorization", required: true, desc: "`Bearer {api_key}`" },
    { name: "X-Signature", required: true, desc: "HMAC-SHA256 signature" },
    { name: "X-Timestamp", required: true, desc: "ISO 8601 timestamp" },
    { name: "X-Secret-Salt", required: true, desc: "Secret salt for HMAC validation" },
    { name: "Content-Type", required: true, desc: "`application/json`" },
    { name: "X-Idempotency-Key", desc: "Unique key to prevent duplicate refunds" },
  ]}
/>

### Body Parameters

<ParamTable
  rows={[
    { name: "transactionId", type: "string", desc: "ID of the original transaction to refund" },
    { name: "reason", type: "string", desc: "Reason for the refund" },
    { name: "amount", type: "number", desc: "Refund amount. Omit for a full refund. Must be > 0 and ≤ the remaining refundable amount." },
    { name: "customerId", type: "string", desc: "Customer ID. Resolved from the transaction if omitted." },
    { name: "beneficiaryEmail", type: "string", desc: "Beneficiary email address" },
    { name: "beneficiaryAccount", type: "string", desc: "Bank account number" },
    { name: "beneficiaryIfsc", type: "string", desc: "IFSC code (e.g. `HDFC0001234`)" },
    { name: "beneficiaryName", type: "string", desc: "Full name of the beneficiary" },
    { name: "beneficiaryMobile", type: "string", desc: "Beneficiary mobile number with country code" },
    { name: "beneficiaryCity", type: "string", desc: "Beneficiary city" },
  ]}
/>

## Beneficiary Fields

INR refunds are processed as IMPS payouts to the customer's bank account. You must provide:

| Field | Required | Example |
|-------|----------|---------|
| `beneficiaryEmail` | Yes | `"rahul@example.com"` |
| `beneficiaryAccount` | Yes | `"50100012345678"` |
| `beneficiaryIfsc` | Yes | `"HDFC0001234"` |

### Optional fields

| Field | Description | Example |
|-------|-------------|---------|
| `beneficiaryName` | Account holder name | `"Rahul Sharma"` |
| `beneficiaryMobile` | Mobile with country code | `"+919876543210"` |
| `beneficiaryCity` | City | `"Mumbai"` |

:::warning
If `beneficiaryAccount` or `beneficiaryIfsc` is missing, the API will return an error.
:::

:::tip Try it out
Test refund creation interactively using the [API Simulator](https://merchant-sample.zenpayz.com/api-simulator/refund-intents/create).
:::

## Response

### Success (201 Created)

```json
{
  "success": true,
  "data": {
    "refundId": "refund_1774683026458_oann5ibjf",
    "merchantId": "ZP_FIN_1774592804_0781001264",
    "customerId": "cust_mn8yfewm_37ntht",
    "transactionId": "txn_1774619159530_1_mn8yfmph_5iwb9b",
    "paymentIntentId": "pi_1774619159530_66d8s8v25",
    "amount": 100,
    "originalAmount": 100,
    "currency": "INR",
    "refundType": "full",
    "status": "pending_approval",
    "tspProvider": "sulifu_pay",
    "externalRefundId": null,
    "reason": "Customer requested refund",
    "createdAt": "2026-04-10T14:22:01.000Z",
    "completedAt": null,
    "failureReason": null,
    "processingTimeMs": null
  }
}
```

### Error Responses

<ErrorTable
  rows={[
    { code: "REFUND_NOT_ALLOWED", status: "400", message: "Transaction cannot be refunded (must be `success` or `partial_refund` status)" },
    { code: "REFUND_AMOUNT_EXCEEDED", status: "400", message: "Requested amount exceeds the remaining refundable amount" },
    { code: "MISSING_BENEFICIARY_FIELDS", status: "400", message: "`beneficiaryEmail`, `beneficiaryAccount`, or `beneficiaryIfsc` is missing" },
    { code: "UNAUTHORIZED", status: "401", message: "Invalid or missing API key" },
    { code: "TRANSACTION_NOT_FOUND", status: "404", message: "Transaction does not exist or does not belong to this merchant" },
    { code: "DUPLICATE_REQUEST", status: "409", message: "A refund with this idempotency key already exists" },
    { code: "ALREADY_FULLY_REFUNDED", status: "422", message: "Transaction has already been fully refunded" },
  ]}
/>

## Request Examples

### Full refund

Refund the entire transaction amount to the customer's bank account:

```json
{
  "transactionId": "txn_1774619159530_1_mn8yfmph_5iwb9b",
  "reason": "Customer requested full refund",
  "beneficiaryEmail": "rahul@example.com",
  "beneficiaryAccount": "50100012345678",
  "beneficiaryIfsc": "HDFC0001234"
}
```

Response:

```json
{
  "success": true,
  "data": {
    "refundId": "refund_1774683026458_oann5ibjf",
    "amount": 100,
    "originalAmount": 100,
    "currency": "INR",
    "refundType": "full",
    "status": "pending_approval",
    "tspProvider": "sulifu_pay",
    "reason": "Customer requested full refund",
    "createdAt": "2026-04-10T14:22:01.000Z"
  }
}
```

### Partial refund

Refund 500 INR out of a 2000 INR transaction:

```json
{
  "transactionId": "txn_1774700012000_1_abc12345_9z8y7x",
  "amount": 500,
  "reason": "Partial item return",
  "beneficiaryEmail": "priya@example.com",
  "beneficiaryAccount": "91020034567890",
  "beneficiaryIfsc": "SBIN0001234",
  "beneficiaryName": "Priya Sharma"
}
```

Response:

```json
{
  "success": true,
  "data": {
    "refundId": "refund_1774700099000_kp3m7dxw2",
    "amount": 500,
    "originalAmount": 2000,
    "currency": "INR",
    "refundType": "partial",
    "status": "pending_approval",
    "tspProvider": "sulifu_pay",
    "reason": "Partial item return",
    "createdAt": "2026-04-10T14:35:00.000Z"
  }
}
```

### Amount exceeds refundable balance

If you try to refund more than the remaining balance:

```json
{
  "success": false,
  "error": "Requested refund amount 1800 exceeds refundable amount 1250.00 INR"
}
```

## Refund Lifecycle

After a refund is created, it moves through these statuses:

| Status | What happens |
|--------|-------------|
| `pending_approval` | Refund created. The refund amount is **blocked (escrowed)** in the merchant wallet. Waiting for admin to approve or reject. |
| `approved` | Admin approved. The refund is being sent to the payment provider. |
| `processing` | Payment provider is executing the payout to the customer. |
| `completed` | Payout succeeded. Blocked funds are **debited** from the merchant wallet. Transaction `refundedAmount` is updated. |
| `failed` | Payment provider failed to process. Blocked funds are **released back** to the merchant wallet. |
| `rejected` | Admin rejected the refund. Blocked funds are **released back**. |
| `cancelled` | Merchant cancelled the refund before processing. Blocked funds are **released back**. |

### Flow

```
                                    +---> rejected (funds released)
                                    |
pending_approval ---> approved ---> processing ---> completed (funds debited)
       |                                    |
       +---> cancelled (funds released)     +---> failed (funds released)
```

### Auto-approval

If `auto_refund_enabled` is `true` in the merchant configuration, refunds skip `pending_approval` and go directly to `processing`.

### Auto-cancellation

If a refund stays in `pending_approval` for **2 days** without admin action, it is automatically cancelled and the blocked funds are released.

## Partial Refunds

You can issue multiple partial refunds against the same transaction.

- Specify `amount` to refund less than the full transaction amount.
- The system tracks cumulative `refundedAmount` on the transaction.
- Each completed refund adds to the running total.
- Validation: `amount` must be ≤ `originalAmount - refundedAmount`.
- When cumulative refunds equal the transaction amount, the transaction becomes `refunded` and no more refunds are accepted.

**Example: three partial refunds on a 1000 INR transaction**

| Refund | Amount | Transaction `refundedAmount` | Transaction Status |
|--------|--------|-----------------------------|--------------------|
| 1st | 300 | 300 | `partial_refund` |
| 2nd | 200 | 500 | `partial_refund` |
| 3rd | 500 | 1000 | `refunded` |
| 4th (rejected) | 1 | -- | Error: already fully refunded |

:::info
Transactions in `partial_refund` status still accept new refund requests. You do not need to wait for a pending refund to complete before creating the next one -- the system validates against cumulative **completed** refunds only.
:::

## Code Examples

<Tabs groupId="language">
  <TabItem value="curl" label="cURL" default>

```bash
# Full refund
curl -X POST https://api.zenpayz.com/merchant/api/v1/refund-intents \
  -H "Authorization: Bearer zp_live_abc123def456" \
  -H "X-Timestamp: 2026-04-10T14:22:01.000Z" \
  -H "X-Signature: a1b2c3d4e5f6..." \
  -H "X-Secret-Salt: your_secret_salt" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: refund-order-8842" \
  -d '{
    "transactionId": "txn_1774619159530_1_mn8yfmph_5iwb9b",
    "reason": "Customer requested full refund",
    "beneficiaryEmail": "rahul@example.com",
    "beneficiaryAccount": "50100012345678",
    "beneficiaryIfsc": "HDFC0001234"
  }'

# Partial refund
curl -X POST https://api.zenpayz.com/merchant/api/v1/refund-intents \
  -H "Authorization: Bearer zp_live_abc123def456" \
  -H "X-Timestamp: 2026-04-10T14:35:00.000Z" \
  -H "X-Signature: e5f6a7b8c9d0..." \
  -H "X-Secret-Salt: your_secret_salt" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionId": "txn_1774700012000_1_abc12345_9z8y7x",
    "amount": 500,
    "reason": "Partial item return",
    "beneficiaryEmail": "priya@example.com",
    "beneficiaryAccount": "91020034567890",
    "beneficiaryIfsc": "SBIN0001234",
    "beneficiaryName": "Priya Sharma"
  }'
```

  </TabItem>
  <TabItem value="javascript" label="JavaScript">

```javascript
const crypto = require('crypto');

const apiKey = process.env.ZENPAYS_API_KEY;
const secretSalt = process.env.ZENPAYS_SECRET_SALT;
const timestamp = new Date().toISOString();

// Full refund
const body = {
  transactionId: 'txn_1774619159530_1_mn8yfmph_5iwb9b',
  reason: 'Customer requested full refund',
  beneficiaryEmail: 'rahul@example.com',
  beneficiaryAccount: '50100012345678',
  beneficiaryIfsc: 'HDFC0001234',
};

const signature = crypto
  .createHmac('sha256', secretSalt)
  .update(timestamp + JSON.stringify(body))
  .digest('hex');

const response = await fetch('https://api.zenpayz.com/merchant/api/v1/refund-intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'X-Timestamp': timestamp,
    'X-Signature': signature,
    'X-Secret-Salt': secretSalt,
    'Content-Type': 'application/json',
    'X-Idempotency-Key': 'refund-order-8842',
  },
  body: JSON.stringify(body),
});

const result = await response.json();
console.log(result.data.refundId);  // refund_1774683026458_oann5ibjf
console.log(result.data.status);    // pending_approval
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import hmac
import hashlib
import json
import os
from datetime import datetime, timezone
import requests

api_key = os.environ["ZENPAYS_API_KEY"]
secret_salt = os.environ["ZENPAYS_SECRET_SALT"]
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")

# Full refund
body = {
    "transactionId": "txn_1774619159530_1_mn8yfmph_5iwb9b",
    "reason": "Customer requested full refund",
    "beneficiaryEmail": "rahul@example.com",
    "beneficiaryAccount": "50100012345678",
    "beneficiaryIfsc": "HDFC0001234",
}

data = timestamp + json.dumps(body, separators=(",", ":"))
signature = hmac.new(
    secret_salt.encode(),
    data.encode(),
    hashlib.sha256
).hexdigest()

response = requests.post(
    "https://api.zenpayz.com/merchant/api/v1/refund-intents",
    headers={
        "Authorization": f"Bearer {api_key}",
        "X-Timestamp": timestamp,
        "X-Signature": signature,
        "X-Secret-Salt": secret_salt,
        "Content-Type": "application/json",
        "X-Idempotency-Key": "refund-order-8842",
    },
    json=body,
)

result = response.json()
print(result["data"]["refundId"])  # refund_1774683026458_oann5ibjf
print(result["data"]["status"])    # pending_approval
```

  </TabItem>
  <TabItem value="sdk" label="SDK (Recommended)">

```javascript
import { ZenPays } from '@zenxdigitalholdings/zenpays'

const zenpays = new ZenPays({ apiKey: process.env.ZENPAYS_API_KEY })

// Full refund
const refund = await zenpays.refunds.create({
  transactionId: 'txn_1774619159530_1_mn8yfmph_5iwb9b',
  reason: 'Customer requested full refund',
  beneficiaryEmail: 'rahul@example.com',
  beneficiaryAccount: '50100012345678',
  beneficiaryIfsc: 'HDFC0001234',
})
console.log(refund.refundId) // refund_xxxxx

// Partial refund
const partialRefund = await zenpays.refunds.create({
  transactionId: 'txn_1774700012000_1_abc12345_9z8y7x',
  amount: 500,
  reason: 'Partial item return',
  beneficiaryEmail: 'priya@example.com',
  beneficiaryAccount: '91020034567890',
  beneficiaryIfsc: 'SBIN0001234',
  beneficiaryName: 'Priya Sharma',
})
```

  </TabItem>
</Tabs>

## Related Endpoints

- [Get Refund](/docs/rest-api/endpoints/refunds/get-refund) -- Check refund status
- [List Refunds](/docs/rest-api/endpoints/refunds/list-refunds) -- List all refunds with filters
- [Cancel Refund](/docs/rest-api/endpoints/refunds/cancel-refund) -- Cancel a pending refund
- [Refund Stats](/docs/rest-api/endpoints/refunds/refund-stats) -- Aggregate refund statistics
- [Bulk Upload](/docs/rest-api/endpoints/refunds/bulk-upload) -- Create multiple refunds at once
