This guide provides comprehensive instructions for implementing refunds. You'll learn how to initiate refunds, handle multiple refund scenarios, manage refund statuses, and handle webhook notifications.
Before implementing refunds, ensure you have:
https://gate.reviopay.com/api/v1
Endpoint
POST /purchases/{purchase_id}/refund/
Headers
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
Request Body
JSON
{
"amount": 100
}
Parameters
Success Response (200 OK)
JSON
{
"id": "9c4fa29b-ee35-430c-a412-60e5de77483e",
"type": "payment",
"status": "success",
"payment": {
"is_outgoing": true,
"payment_type": "refund",
"amount": 100,
"currency": "ZAR",
"net_amount": 100,
"fee_amount": 0,
"description": "Refund: RV238",
"paid_on": 1770212782
},
"related_to": {
"type": "purchase",
"id": "347472e8-21ad-42fc-a9b5-241963fb0cb1",
"reference": "RV238"
},
"reference_generated": "46",
"refund_availability": "none",
"created_on": 1770212782,
"updated_on": 1770212782
}
Key Response Fields
Error Response (400 Bad Request)
JSON
{
"__all__": [
{
"message": "Could not refund the payment! Consult `.transaction_data` for possible reason.",
"code": "purchase_refund_error"
}
]
}
Common error codes:
purchase_refund_error: General refund failure (check transaction_data for details)purchase_refund_amount_too_large: Refund amount exceeds available refundable amountpurchase_already_fully_refunded: No refundable amount remainingpurchase_refund_wrong_status: Purchase is not in a refundable status (must be: settled, cleared, paid, retrieved, or refunded)purchase_refund_timeout: Cannot refund same purchase more than once every 60 seconds
Your system will receive webhook notifications for refund status changes. Configure your webhook endpoint to handle these events.
Payload Structure
JSON
{
"id": "9c4fa29b-ee35-430c-a412-60e5de77483e",
"type": "payment",
"event_type": "payment.refunded",
"status": "success",
"payment": {
"amount": 100,
"currency": "ZAR",
"payment_type": "refund",
"is_outgoing": true,
"description": "Refund: RV238",
"paid_on": 1770212782
},
"related_to": {
"id": "347472e8-21ad-42fc-a9b5-241963fb0cb1",
"type": "purchase",
"reference": "RV238"
},
"transaction_data": {
"payment_method": "mastercard",
"extra": {
"response_code": "00",
"masked_pan": "541282******2835"
}
},
"created_on": 1770212782,
"updated_on": 1770212782
}
Payload Structure
JSON
{
"id": "347472e8-21ad-42fc-a9b5-241963fb0cb1",
"type": "purchase",
"event_type": "purchase.pending_refund",
"status": "pending_refund",
"refundable_amount": 230,
"transaction_data": {
"attempts": [
{
"type": "refund",
"amount": 150,
"successful": null,
"processing_time": 1770213226
}
]
}
}
Payload Structure
JSON
{
"id": "347472e8-21ad-42fc-a9b5-241963fb0cb1",
"type": "purchase",
"event_type": "purchase.refund_failure",
"status": "refunded",
"refundable_amount": 230,
"transaction_data": {
"attempts": [
{
"type": "refund",
"amount": 150,
"successful": false,
"processing_time": 1770212876,
"error": {
"message": "Refund limit exceeded",
"code": "REFUND_LIMIT_EXCEEDED"
}
}
]
}
}
Failed refund details are found in the transaction_data.attempts array. Look for entries where type is 'refund' and successful is false.
Error Response
JSON
{
"__all__": [
{
"message": "You can only refund this Purchase for 80 (check `.refundable_amount`).",
"code": "purchase_refund_amount_too_large"
}
]
}
How to Handle: Query the purchase details to get the current refundable_amount before attempting the refund.
Error Response
JSON
{
"__all__": [
{
"message": "Could not refund the payment! Consult `.transaction_data` for possible reason.",
"code": "purchase_refund_error"
}
]
}
How to Handle:
transaction_data field in the webhook for specific failure reasonspurchase.refund_failure webhook which will contain detailed error information
How to Detect: When refundable_amount equals 0 in the purchase details.
Prevention: Always check refundable_amount before initiating a refund.
Before initiating a refund, query the purchase to get the current refundable amount:
BASH
curl --location 'https://gate.reviopay.com/api/v1/purchases/{purchase_id}' \
--header 'Authorization: Bearer YOUR_API_TOKEN'
Look for the refundable_amount field in the response.
Maintain a mapping between your internal refund requests and the platform's refund transaction IDs:
JSON
{
"your_refund_id": "REF-12345",
"platform_refund_id": "9c4fa29b-ee35-430c-a412-60e5de77483e",
"status": "pending",
"amount": 100,
"initiated_at": "2024-01-15T10:30:00Z"
}
Verify webhook authenticity by:
company_id matches your account
The platform does not support idempotency keys for refunds. To prevent duplicate refunds:
related_to.id field to link refunds to purchases
When a refund fails:
purchase.refund_failure webhooktransaction_data.attempts array (look for entries where type is "refund" and successful is false)
Important: You cannot make refunds for the same purchase more than once every 60 seconds. Track the processing_time from failed attempts and wait at least 60 seconds before retrying. If you attempt too soon, you'll receive error code purchase_refund_timeout.
Create a polling mechanism as a fallback:
JAVASCRIPT
async function monitorRefundStatus(refundId, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
const refund = await getRefundDetails(refundId);
if (refund.status === 'success' || refund.status === 'failed') {
return refund;
}
await sleep(5000);
}
throw new Error('Refund status check timeout');
}
This example demonstrates handling a purchase of 330 ZAR with multiple refund attempts.
Request
BASH
curl --location 'https://gate.reviopay.com/api/v1/purchases/347472e8-21ad-42fc-a9b5-241963fb0cb1/refund/' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_TOKEN' \
--data '{"amount": 100}'
Response (200 OK)
JSON
{
"id": "9c4fa29b-ee35-430c-a412-60e5de77483e",
"status": "success",
"payment": {
"amount": 100,
"payment_type": "refund"
},
"related_to": {
"id": "347472e8-21ad-42fc-a9b5-241963fb0cb1"
}
}
Result: Refundable amount is now 230 ZAR (330 - 100)
Request
BASH
curl --location 'https://gate.reviopay.com/api/v1/purchases/347472e8-21ad-42fc-a9b5-241963fb0cb1/refund/' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_TOKEN' \
--data '{"amount": 150}'
Response (400 Bad Request)
JSON
{
"__all__": [
{
"message": "Could not refund the payment! Consult `.transaction_data` for possible reason.",
"code": "purchase_refund_error"
}
]
}
Result: Refund failed due to the percentage limit. Refundable amount remains 230 ZAR.
Following the same pattern, a 200 ZAR refund attempt also fails due to limits.
Refundable amount remains: 230 ZAR
BASH
curl --location 'https://gate.reviopay.com/api/v1/purchases/347472e8-21ad-42fc-a9b5-241963fb0cb1/refund/' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_TOKEN' \
--data '{"amount": 150}'
Response (200 OK)
JSON
{
"id": "db068bb0-5a36-4eeb-b671-78f8828805de",
"status": "success",
"payment": {"amount": 150}
}
Result: Refundable amount is now 80 ZAR (230 - 150)
Response (400 Bad Request)
JSON
{
"__all__": [
{
"message": "You can only refund this Purchase for 80 (check `.refundable_amount`).",
"code": "purchase_refund_amount_too_large"
}
]
}
Action: Adjust the refund amount to 80 ZAR.
Response (200 OK)
JSON
{
"id": "eca06fa0-45a9-4d36-a95e-2f6adaa2ee98",
"status": "success",
"payment": {"amount": 80},
"refund_availability": "none"
}
Result: The purchase has now been fully refunded. refundable_amount is 0 ZAR.
JAVASCRIPT
const axios = require('axios');
class RefundManager {
constructor(apiToken, baseUrl = 'https://gate.reviopay.com/api/v1') {
this.apiToken = apiToken;
this.baseUrl = baseUrl;
this.headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`
};
}
async getPurchaseDetails(purchaseId) {
const response = await axios.get(
`${this.baseUrl}/purchases/${purchaseId}`,
{ headers: this.headers }
);
return response.data;
}
async initiateRefund(purchaseId, amount) {
const response = await axios.post(
`${this.baseUrl}/purchases/${purchaseId}/refund/`,
{ amount },
{ headers: this.headers }
);
return response.data;
}
async safeRefund(purchaseId, requestedAmount) {
const purchase = await this.getPurchaseDetails(purchaseId);
const refundableAmount = purchase.refundable_amount;
if (refundableAmount === 0) throw new Error('Purchase is fully refunded');
if (requestedAmount > refundableAmount) requestedAmount = refundableAmount;
return await this.initiateRefund(purchaseId, requestedAmount);
}
}
PYTHON
import requests
import time
class RefundManager:
def __init__(self, api_token, base_url='https://gate.reviopay.com/api/v1'):
self.api_token = api_token
self.base_url = base_url
self.headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {api_token}'
}
def get_purchase_details(self, purchase_id):
url = f'{self.base_url}/purchases/{purchase_id}'
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
def initiate_refund(self, purchase_id, amount):
url = f'{self.base_url}/purchases/{purchase_id}/refund/'
response = requests.post(url, json={'amount': amount}, headers=self.headers)
response.raise_for_status()
return response.json()
def safe_refund(self, purchase_id, requested_amount):
purchase = self.get_purchase_details(purchase_id)
refundable_amount = purchase.get('refundable_amount', 0)
if refundable_amount == 0:
raise ValueError('Purchase is fully refunded')
if requested_amount > refundable_amount:
requested_amount = refundable_amount
return self.initiate_refund(purchase_id, requested_amount)
JAVASCRIPT
async function handleRefundFailed(event) {
const failedAttempt = event.transaction_data?.attempts?.find(
attempt => attempt.type === 'refund' && attempt.successful === false
);
if (failedAttempt) {
console.log('Failure reason:', failedAttempt.error?.code);
console.log('Failed amount:', failedAttempt.amount);
}
await updateRefundStatus(event.id, 'failed', 0, failedAttempt?.error?.message);
if (shouldRetryRefund(failedAttempt)) {
await queueRefundRetry(event.id, failedAttempt);
}
}
function shouldRetryRefund(failedAttempt) {
const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'TEMPORARY_ERROR'];
return retryableCodes.includes(failedAttempt?.error?.code);
}