Refunds
Process refunds for completed transactions. This section covers how to create and manage refunds through the ZoPay API.
Overview
Refunds allow you to return money to customers for various reasons such as:
- Customer request
- Duplicate payment
- Service not delivered
- Wrong amount charged
- Technical error
Refund Methods
ZoPay supports two refund methods:
- Reversal: Instant refund by reversing the original transaction through the gateway (preferred method)
- Payout: Manual refund by creating a new payout to the customer (used when reversal is not available)
Refund Flow
- Identify the original transaction that needs to be refunded
- Create a refund request via
POST /api/v1/refunds - Specify refund amount (full or partial)
- Select refund method (reversal or payout)
- Provide reason for refund
- ZoPay processes the refund
- Webhook notification sent when refund completes
- Check refund status via
GET /api/v1/refunds/:id
Refund Statuses
- PENDING: Refund initiated, awaiting processing
- PROCESSING: Refund is being processed
- SUCCESS: Refund completed successfully
- FAILED: Refund failed (check error details)
- CANCELLED: Refund was cancelled
Create Refund
Create a refund for a completed transaction. You can refund the full amount or a partial amount.
Endpoint
1POST /api/v1/refundsRequest Body
1{
2 "transaction_id": "txn-uuid",
3 "amount": "5000.00",
4 "reason": "Customer request",
5 "method": "REVERSAL",
6 "idempotency_key": "unique-refund-123",
7 "additional_details": "Order #123 was cancelled by customer"
8}Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
transaction_id | string | Yes | ID of the original transaction to refund |
amount | string | Yes | Refund amount (must be less than or equal to original transaction amount) |
reason | string | Yes | Reason for refund (e.g., "Customer request", "Duplicate payment", "Service not delivered") |
method | string | No | Refund method: "REVERSAL" (default, instant) or "PAYOUT" (manual) |
idempotency_key | string | Yes | Unique identifier to prevent duplicate refunds |
additional_details | string | No | Additional information about the refund |
Response
1{
2 "refund_id": "refund-uuid",
3 "transaction_id": "txn-uuid",
4 "amount": "5000.00",
5 "status": "PROCESSING",
6 "method": "REVERSAL",
7 "reason": "Customer request",
8 "created_at": "2024-01-15T10:00:00.000Z"
9}Example Request
1const response = await fetch('https://api.zopay.com/api/v1/refunds', {
2 method: 'POST',
3 headers: {
4 'Content-Type': 'application/json',
5 'x-zo-key': 'your-api-key',
6 'x-zo-timestamp': Math.floor(Date.now() / 1000).toString(),
7 'x-zo-nonce': crypto.randomBytes(16).toString('hex'),
8 'x-zo-origin': 'https://yourdomain.com',
9 'x-zo-signature': signature,
10 'x-zo-version': '1.0'
11 },
12 body: JSON.stringify({
13 transaction_id: 'txn-uuid',
14 amount: '5000.00',
15 reason: 'Customer request',
16 method: 'REVERSAL',
17 idempotency_key: 'unique-refund-123',
18 additional_details: 'Order #123 was cancelled by customer'
19 })
20});
21
22const refund = await response.json();Partial Refunds
You can create multiple partial refunds for a single transaction, as long as the total refunded amount does not exceed the original transaction amount.
Get Refund Status
Check the status of a refund to see if it has been completed, failed, or is still processing.
Endpoint
1GET /api/v1/refunds/:idPath Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Refund ID from the create refund response |
Response
1{
2 "refund": {
3 "id": "refund-uuid",
4 "transaction_id": "txn-uuid",
5 "merchantId": "merchant-uuid",
6 "amount": "5000.00",
7 "currency": "XAF",
8 "status": "SUCCESS",
9 "method": "REVERSAL",
10 "reason": "Customer request",
11 "gatewayReference": "MTN_REF_123456789",
12 "createdAt": "2024-01-15T10:00:00.000Z",
13 "completedAt": "2024-01-15T10:01:00.000Z"
14 }
15}Response Fields
| Field | Type | Description |
|---|---|---|
refund.id | string | Unique refund identifier |
refund.transaction_id | string | Original transaction ID |
refund.amount | string | Refund amount |
refund.status | string | Refund status (PENDING, PROCESSING, SUCCESS, FAILED, CANCELLED) |
refund.method | string | Refund method used (REVERSAL or PAYOUT) |
refund.reason | string | Reason for the refund |
refund.completedAt | string | Refund completion timestamp (null if not completed) |
List Refunds
Retrieve a list of all your refunds with optional filtering and pagination.
Endpoint
1GET /api/v1/refundsQuery Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status (PENDING, PROCESSING, SUCCESS, FAILED, CANCELLED) |
transaction_id | string | Filter by original transaction ID |
limit | number | Number of results per page (default: 20, max: 100) |
offset | number | Number of results to skip (for pagination) |
Response
1{
2 "refunds": [
3 {
4 "id": "refund-uuid",
5 "transaction_id": "txn-uuid",
6 "amount": "5000.00",
7 "status": "SUCCESS",
8 "method": "REVERSAL",
9 "reason": "Customer request",
10 "createdAt": "2024-01-15T10:00:00.000Z"
11 }
12 ],
13 "total": 45,
14 "limit": 20,
15 "offset": 0
16}Example Request
1// Get all refunds for a specific transaction
2const response = await fetch('https://api.zopay.com/api/v1/refunds?transaction_id=txn-uuid', {
3 method: 'GET',
4 headers: {
5 'x-zo-key': 'your-api-key',
6 'x-zo-timestamp': Math.floor(Date.now() / 1000).toString(),
7 'x-zo-nonce': crypto.randomBytes(16).toString('hex'),
8 'x-zo-origin': 'https://yourdomain.com',
9 'x-zo-signature': signature,
10 'x-zo-version': '1.0'
11 }
12});
13
14const data = await response.json();
15console.log('Total refunds:', data.total);
16console.log('Refunds:', data.refunds);Best Practices
- Always use idempotency keys to prevent duplicate refunds
- Prefer REVERSAL method when available (faster and more reliable)
- Provide clear reasons for refunds for audit purposes
- Set up webhooks to receive real-time refund status updates
- Keep track of partial refunds to ensure total doesn't exceed original amount
- Monitor refund success rates and handle failures appropriately