Handle Refunds
Learn how to process full and partial refunds programmatically via the API or through the merchant dashboard.
Refund Types
Full Refund
Refund the entire transaction amount back to the customer. The transaction status changes to REFUND_SUCCESS.
Partial Refund
Refund a portion of the transaction. Multiple partial refunds can be issued until the full amount is refunded.
Refund Processing Flow
Create Refund
Call POST /api/v1/refund/create with transaction ID and amount
Validation
System validates transaction status, refund amount, and merchant balance
Async Processing
Refund queued for processing at PSP (status: REFUND_INIT)
Webhook Notification
Webhook sent with the result: refund.completed or refund.failed. A failed refund can be retried by our team (you then get refund.retried and a new result for the same refund id).
Refunds are Asynchronous
REFUND_INIT). This does not mean the refund is complete: a 202 answer means the provider has not confirmed it yet. Always listen for webhook events to confirm the result, and key your records on the refund id. Create a Refund
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
transactionId | string | Required | ULID of the original transaction |
refundAmount | number | Required | Amount to refund (min: $0.10, max: remaining refundable) |
reason | string | Optional | Reason for refund (max 255 characters) |
API Request
curl -X POST https://gateway.epayse.com/api/v1/refund/create \
-H "Content-Type: application/json" \
-H "X-Api-Key-Id: your_api_key" \
-H "X-Signature: generated_hmac_signature" \
-H "X-Timestamp: 1700000000" \
-H "X-Nonce: unique_nonce_value" \
-H "X-Signature-Version: 3" \
-d '{
"transactionId": "01kbkzs3pdcdkjvsq2xb2j0ej3",
"refundAmount": 50.00,
"reason": "Customer request"
}'Response
{
"status": "SUCCESS",
"message": "Refund created successfully",
"data": {
"id": "01kbm1234abcd5678efgh9012",
"transaction_id": "01kbkzs3pdcdkjvsq2xb2j0ej3",
"amount": 50.00,
"currency": "USD",
"status": "processing",
"reason": "Customer request",
"created_at": "2024-01-15T10:30:00Z",
"total_fee": 3.50,
"completed_at": null,
"provider_created_at": "2024-01-15T10:30:00Z"
}
}Check Refund Status
Poll the refund status using the GET endpoint (prefer webhooks for real-time updates):
curl -X GET https://gateway.epayse.com/api/v1/refund/01kbm1234abcd5678efgh9012 \
-H "X-Api-Key-Id: your_api_key" \
-H "X-Signature: generated_hmac_signature" \
-H "X-Timestamp: 1700000000" \
-H "X-Nonce: unique_nonce_value" \
-H "X-Signature-Version: 3"PHP Integration Example
<?php
$apiKey = 'your_api_key';
$secretKey = 'your_secret_key';
$timestamp = time();
$nonce = bin2hex(random_bytes(16));
// Full refund
$payload = json_encode([
'transactionId' => '01kbkzs3pdcdkjvsq2xb2j0ej3',
'refundAmount' => 100.00, // Full transaction amount
'reason' => 'Customer request',
]);
// Generate the HMAC-SHA256 signature (version 3): METHOD|PATH|TIMESTAMP|NONCE|BODY
$path = 'api/v1/refund/create'; // no leading slash
$signature = hash_hmac('sha256', implode('|', ['POST', $path, $timestamp, $nonce, $payload]), $secretKey);
$ch = curl_init('https://gateway.epayse.com/api/v1/refund/create');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Api-Key-Id: ' . $apiKey,
'X-Signature: ' . $signature,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature-Version: 3',
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($response['status'] === 'SUCCESS') {
$refundId = $response['data']['id'];
// Store refund ID - wait for webhook to confirm completion
echo "Refund initiated: $refundId (status: processing)";
}Refund Status Lifecycle
| Status | Description | Final? |
|---|---|---|
REFUND_INIT | Refund created, queued for processing at PSP | No |
REFUND_SUCCESS | Full refund completed successfully | Yes |
REFUND_PARTIAL_SUCCESS | Partial refund completed (more refunds possible) | Yes |
REFUND_FAIL | Refund rejected by PSP | Yes |
Webhook Events
Listen for refund webhook events to track the final result:
// Webhook event: refund.completed
{
"id": "evt_01hqkz7n3b5v9x2c4d6f8g0h1j",
"object": "event",
"type": "refund.completed",
"created": "2024-01-15T10:31:00Z",
"livemode": true,
"api_key_id": "01hqkz7n3b5v9x2c4d6f8g0h1j",
"data": {
"object": {
"id": "01kbm1234abcd5678efgh9012",
"object": "refund",
"transaction_id": "01kbkzs3pdcdkjvsq2xb2j0ej3",
"amount": 50.00,
"currency": "USD",
"status": "REFUND_SUCCESS",
"reason": "Customer request"
}
},
"api_version": "2024-01-01"
}
// Webhook event: refund.failed (same envelope, type "refund.failed", status "REFUND_FAIL")Validation Rules
Minimum 0.01, maximum is the remaining refundable amount of the transaction. Max 2 decimal places. Only one refund per transaction can be in progress at a time. Refunds are possible for 90 days after the payment and are refused while the transaction has an active dispute.
Must exist in your merchant account and have status SUCCESS. Cannot refund PENDING, FAIL, CANCEL, or EXPIRE transactions.
The refund and its fee are held from your available balance when it starts. If your balance does not cover them, the request is still accepted (200) with approvalStatus: PENDING_BALANCE and waits for an administrator; nothing is sent to the provider meanwhile. At most 50 refunds can wait at once. Send an X-Idempotency-Key header so a repeated request does not create a second refund.
Testing Refunds
Use these sandbox test cards to simulate refund scenarios:
| Card Number | Result | Use Case |
|---|---|---|
4000 0000 0000 5126 | Success | Full refund processed successfully |
4000 0000 0000 5423 | Partial | Partial refund processed successfully |
Dashboard Refunds
Related Resources
Explore the full API documentation for refund endpoint specifications.