curl --request POST \
--url https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/ \
--header 'Content-Type: application/json' \
--header 'X-API-Version: <api-key>' \
--header 'X-Client-ID: <api-key>' \
--header 'X-Client-Secret: <api-key>' \
--header 'X-Merchant-ID: <api-key>' \
--data '
{
"status": "FAILED",
"status_message": "Beneficiary account closed",
"bank_ref_num": "BANKREF123"
}
'import requests
url = "https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/"
payload = {
"status": "FAILED",
"status_message": "Beneficiary account closed",
"bank_ref_num": "BANKREF123"
}
headers = {
"X-Client-ID": "<api-key>",
"X-Client-Secret": "<api-key>",
"X-Merchant-ID": "<api-key>",
"X-API-Version": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'X-Client-ID': '<api-key>',
'X-Client-Secret': '<api-key>',
'X-Merchant-ID': '<api-key>',
'X-API-Version': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'FAILED',
status_message: 'Beneficiary account closed',
bank_ref_num: 'BANKREF123'
})
};
fetch('https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'status' => 'FAILED',
'status_message' => 'Beneficiary account closed',
'bank_ref_num' => 'BANKREF123'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Version: <api-key>",
"X-Client-ID: <api-key>",
"X-Client-Secret: <api-key>",
"X-Merchant-ID: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/"
payload := strings.NewReader("{\n \"status\": \"FAILED\",\n \"status_message\": \"Beneficiary account closed\",\n \"bank_ref_num\": \"BANKREF123\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Client-ID", "<api-key>")
req.Header.Add("X-Client-Secret", "<api-key>")
req.Header.Add("X-Merchant-ID", "<api-key>")
req.Header.Add("X-API-Version", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/")
.header("X-Client-ID", "<api-key>")
.header("X-Client-Secret", "<api-key>")
.header("X-Merchant-ID", "<api-key>")
.header("X-API-Version", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"status\": \"FAILED\",\n \"status_message\": \"Beneficiary account closed\",\n \"bank_ref_num\": \"BANKREF123\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Client-ID"] = '<api-key>'
request["X-Client-Secret"] = '<api-key>'
request["X-Merchant-ID"] = '<api-key>'
request["X-API-Version"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"status\": \"FAILED\",\n \"status_message\": \"Beneficiary account closed\",\n \"bank_ref_num\": \"BANKREF123\"\n}"
response = http.request(request)
puts response.read_body{
"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"
}
}{
"success": false,
"error": {
"code": "ERR_REFUND_002",
"message": "Validation error",
"details": {
"status": "\"NOT_A_STATUS\" is not a valid choice."
}
}
}{
"detail": "No DomesticPaymentRefund matches the given query."
}{
"success": true,
"error": {
"code": "<string>",
"message": "<string>",
"details": {}
}
}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.
curl --request POST \
--url https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/ \
--header 'Content-Type: application/json' \
--header 'X-API-Version: <api-key>' \
--header 'X-Client-ID: <api-key>' \
--header 'X-Client-Secret: <api-key>' \
--header 'X-Merchant-ID: <api-key>' \
--data '
{
"status": "FAILED",
"status_message": "Beneficiary account closed",
"bank_ref_num": "BANKREF123"
}
'import requests
url = "https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/"
payload = {
"status": "FAILED",
"status_message": "Beneficiary account closed",
"bank_ref_num": "BANKREF123"
}
headers = {
"X-Client-ID": "<api-key>",
"X-Client-Secret": "<api-key>",
"X-Merchant-ID": "<api-key>",
"X-API-Version": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'X-Client-ID': '<api-key>',
'X-Client-Secret': '<api-key>',
'X-Merchant-ID': '<api-key>',
'X-API-Version': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'FAILED',
status_message: 'Beneficiary account closed',
bank_ref_num: 'BANKREF123'
})
};
fetch('https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'status' => 'FAILED',
'status_message' => 'Beneficiary account closed',
'bank_ref_num' => 'BANKREF123'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Version: <api-key>",
"X-Client-ID: <api-key>",
"X-Client-Secret: <api-key>",
"X-Merchant-ID: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/"
payload := strings.NewReader("{\n \"status\": \"FAILED\",\n \"status_message\": \"Beneficiary account closed\",\n \"bank_ref_num\": \"BANKREF123\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Client-ID", "<api-key>")
req.Header.Add("X-Client-Secret", "<api-key>")
req.Header.Add("X-Merchant-ID", "<api-key>")
req.Header.Add("X-API-Version", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/")
.header("X-Client-ID", "<api-key>")
.header("X-Client-Secret", "<api-key>")
.header("X-Merchant-ID", "<api-key>")
.header("X-API-Version", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"status\": \"FAILED\",\n \"status_message\": \"Beneficiary account closed\",\n \"bank_ref_num\": \"BANKREF123\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-pacb-uat.eximpe.com/pg/refunds/{refund_id}/simulate-status/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Client-ID"] = '<api-key>'
request["X-Client-Secret"] = '<api-key>'
request["X-Merchant-ID"] = '<api-key>'
request["X-API-Version"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"status\": \"FAILED\",\n \"status_message\": \"Beneficiary account closed\",\n \"bank_ref_num\": \"BANKREF123\"\n}"
response = http.request(request)
puts response.read_body{
"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"
}
}{
"success": false,
"error": {
"code": "ERR_REFUND_002",
"message": "Validation error",
"details": {
"status": "\"NOT_A_STATUS\" is not a valid choice."
}
}
}{
"detail": "No DomesticPaymentRefund matches the given query."
}{
"success": true,
"error": {
"code": "<string>",
"message": "<string>",
"details": {}
}
}400 in production. It operates only on refunds created through the Simulator payment gateway — a refund on a real gateway is rejected.Overview
A real refund walksINITIATED → 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 |
REFUNDED also stamps refunded_at with the current time, if it is not already set.
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.Which webhook fires
| Target status | Webhook |
|---|---|
REFUNDED | PAYMENT_REFUNDED |
FAILED | REFUND_FAILED |
PROCESSING | REFUND_STATUS_UPDATE |
REVERSED | REFUND_STATUS_UPDATE |
ON_HOLD | REFUND_STATUS_UPDATE |
BANK_REJECTED_REFUND | REFUND_STATUS_UPDATE |
INITIATED | None — it is the starting state, not a transition worth notifying |
200 and sends nothing. To re-trigger an event, move the refund to a different status first.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
Testing a failed refund
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"
}'
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 |
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.500 on this endpoint uses ERR_SERVICE_ERROR_000, unlike its 400s.
Related
Mark as Settled
Create Refund
Authorizations
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.
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.
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.
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.
Path Parameters
UID of the refund.
Body
Target status. Uppercase — "refunded" is rejected, "REFUNDED" is accepted.
INITIATED, PROCESSING, REFUNDED, FAILED, REVERSED, ON_HOLD, BANK_REJECTED_REFUND "FAILED"
Custom message carried into the webhook. Omit for the default for that status.
"Beneficiary account closed"
Bank reference to stamp on the refund. Left unchanged if omitted.
"BANKREF123"