Initiate Refund
Description: Initiates a refund for a specific transaction. This endpoint allows you to refund either the full transaction amount or a partial amount, giving you precise control over refund amounts. This is particularly useful for partial returns, price adjustments, or goodwill refunds.
-
Use this endpoint (POST refunds) when:
- You want to refund only part of the transaction (partial refund)
- You want explicit control over the refund amount
- You need to attach custom attributes to the refund
- You want to specify a specific order ID for the refund
-
Use DELETE transaction when:
- You want to return the full transaction amount
- You want the system to automatically decide between void/refund
- You want a simpler, one-step process
- The transaction must be settled before you can refund it. If the transaction is not settled, our acquirer may void instead of refund.
- You cannot refund more than the original transaction amount
- You can issue multiple partial refunds, but the total cannot exceed the original amount
- Refunds typically take 3-5 business days (credit cards) or 5-7 days (ACH) to appear
- Refunds cannot be reversed once processed
Path Parameters
Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
| amount | object | Optional | The refund amount (if omitted, refunds full transaction amount) |
| orderid | string | Optional | Your system's order/tracking number for this refund |
| customAttributes | object | Optional | Custom key-value pairs for tracking refund metadata |
- If you omit
amountentirely, the full transaction amount is refunded - The refund amount cannot exceed the remaining refundable amount
- If the transaction was already partially refunded, you can only refund the remaining balance
Authentication
This endpoint requires bearer token authentication using the Authorization header.
Code Examples
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using Newtonsoft.Json;
public class RefundRequest
{
[JsonProperty("amount")]
public AmountRequest Amount { get; set; }
[JsonProperty("orderid")]
public string OrderId { get; set; }
[JsonProperty("customAttributes")]
public Dictionary<string, string> CustomAttributes { get; set; }
}
public class AmountRequest
{
[JsonProperty("dollars")]
public decimal? Dollars { get; set; }
[JsonProperty("cents")]
public decimal? Cents { get; set; }
}
public class TransactionClient
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl = "https://your-api-domain.com";
private readonly string _bearerToken;
public TransactionClient(string bearerToken)
{
_bearerToken = bearerToken;
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri(_baseUrl);
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _bearerToken);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
public async Task<string> InitiateRefundAsync(
string merchantId,
string transactionId,
RefundRequest refundRequest)
{
try
{
string endpoint = $"/v1/rest/merchants/{merchantId}/transactions/{transactionId}/refunds";
string jsonContent = JsonConvert.SerializeObject(
refundRequest,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
});
var content = new StringContent(
jsonContent,
Encoding.UTF8,
"application/json");
HttpResponseMessage response = await _httpClient.PostAsync(endpoint, content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException e)
{
Console.WriteLine($"Request error: {e.Message}");
throw;
}
}
// Helper method for full refund
public async Task<string> InitiateFullRefundAsync(
string merchantId,
string transactionId,
string reason = null)
{
var refundRequest = new RefundRequest
{
OrderId = $"REFUND-{DateTime.Now:yyyyMMddHHmmss}",
CustomAttributes = new Dictionary<string, string>()
};
if (!string.IsNullOrEmpty(reason))
{
refundRequest.CustomAttributes["refundReason"] = reason;
}
return await InitiateRefundAsync(merchantId, transactionId, refundRequest);
}
// Helper method for partial refund
public async Task<string> InitiatePartialRefundAsync(
string merchantId,
string transactionId,
decimal amount,
string reason = null)
{
var refundRequest = new RefundRequest
{
Amount = new AmountRequest { Dollars = amount },
OrderId = $"PARTIAL-REFUND-{DateTime.Now:yyyyMMddHHmmss}",
CustomAttributes = new Dictionary<string, string>()
};
if (!string.IsNullOrEmpty(reason))
{
refundRequest.CustomAttributes["refundReason"] = reason;
}
return await InitiateRefundAsync(merchantId, transactionId, refundRequest);
}
}
// Example 1: Full refund
var client = new TransactionClient("your-bearer-token-here");
string result = await client.InitiateFullRefundAsync(
"12345678901",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"Customer returned product"
);
// Example 2: Partial refund of $25.00
string result2 = await client.InitiatePartialRefundAsync(
"12345678901",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
25.00m,
"Partial product damage - 25% refund"
);
Imports System
Imports System.Net.Http
Imports System.Net.Http.Headers
Imports System.Text
Imports System.Threading.Tasks
Imports System.Collections.Generic
Imports Newtonsoft.Json
Public Class RefundRequest
<JsonProperty("amount")>
Public Property Amount As AmountRequest
<JsonProperty("orderid")>
Public Property OrderId As String
<JsonProperty("customAttributes")>
Public Property CustomAttributes As Dictionary(Of String, String)
End Class
Public Class AmountRequest
<JsonProperty("dollars")>
Public Property Dollars As Decimal?
<JsonProperty("cents")>
Public Property Cents As Decimal?
End Class
Public Class TransactionClient
Private ReadOnly _httpClient As HttpClient
Private ReadOnly _baseUrl As String = "https://your-api-domain.com"
Private ReadOnly _bearerToken As String
Public Sub New(bearerToken As String)
_bearerToken = bearerToken
_httpClient = New HttpClient()
_httpClient.BaseAddress = New Uri(_baseUrl)
_httpClient.DefaultRequestHeaders.Authorization =
New AuthenticationHeaderValue("Bearer", _bearerToken)
_httpClient.DefaultRequestHeaders.Accept.Add(
New MediaTypeWithQualityHeaderValue("application/json"))
End Sub
Public Async Function InitiateRefundAsync(
merchantId As String,
transactionId As String,
refundRequest As RefundRequest) As Task(Of String)
Try
Dim endpoint As String = $"/v1/rest/merchants/{merchantId}/transactions/{transactionId}/refunds"
Dim jsonContent As String = JsonConvert.SerializeObject(
refundRequest,
New JsonSerializerSettings With {
.NullValueHandling = NullValueHandling.Ignore
})
Dim content As New StringContent(
jsonContent,
Encoding.UTF8,
"application/json")
Dim response As HttpResponseMessage = Await _httpClient.PostAsync(endpoint, content)
response.EnsureSuccessStatusCode()
Return Await response.Content.ReadAsStringAsync()
Catch e As HttpRequestException
Console.WriteLine($"Request error: {e.Message}")
Throw
End Try
End Function
End Class
' Example: Partial refund
Dim client As New TransactionClient("your-bearer-token-here")
Dim refundRequest As New RefundRequest With {
.Amount = New AmountRequest With {.Dollars = 25D},
.OrderId = "PARTIAL-REFUND-001",
.CustomAttributes = New Dictionary(Of String, String) From {
{"refundReason", "Partial product damage"}
}
}
Dim result As String = Await client.InitiateRefundAsync(
"12345678901",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
refundRequest
)
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.google.gson.Gson;
import java.util.HashMap;
import java.util.Map;
class RefundRequest {
private AmountRequest amount;
private String orderid;
private Map<String, String> customAttributes;
// Constructor, getters, and setters...
}
class AmountRequest {
private Double dollars;
private Double cents;
public AmountRequest(Double dollars) {
this.dollars = dollars;
}
// Getters and setters...
}
public class TransactionClient {
private final String baseUrl;
private final String bearerToken;
private final HttpClient httpClient;
private final Gson gson;
public TransactionClient(String baseUrl, String bearerToken) {
this.baseUrl = baseUrl;
this.bearerToken = bearerToken;
this.httpClient = HttpClient.newHttpClient();
this.gson = new Gson();
}
public String initiateRefund(
String merchantId,
String transactionId,
RefundRequest refundRequest)
throws IOException, InterruptedException {
String endpoint = baseUrl + "/v1/rest/merchants/" + merchantId +
"/transactions/" + transactionId + "/refunds";
String jsonBody = gson.toJson(refundRequest);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.header("Authorization", "Bearer " + bearerToken)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() != 200 && response.statusCode() != 201) {
throw new IOException("HTTP Error: " + response.statusCode());
}
return response.body();
}
}
// Example: Partial refund
TransactionClient client = new TransactionClient(
"https://your-api-domain.com",
"your-bearer-token-here"
);
RefundRequest refundRequest = new RefundRequest();
refundRequest.setAmount(new AmountRequest(25.00));
refundRequest.setOrderid("PARTIAL-REFUND-001");
Map<String, String> attributes = new HashMap<>();
attributes.put("refundReason", "Partial product damage");
refundRequest.setCustomAttributes(attributes);
String result = client.initiateRefund(
"12345678901",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
refundRequest
);
require 'net/http'
require 'uri'
require 'json'
class TransactionClient
def initialize(base_url, bearer_token)
@base_url = base_url
@bearer_token = bearer_token
end
def initiate_refund(merchant_id, transaction_id, refund_request)
endpoint = "#{@base_url}/v1/rest/merchants/#{merchant_id}" \
"/transactions/#{transaction_id}/refunds"
uri = URI.parse(endpoint)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request['Authorization'] = "Bearer #{@bearer_token}"
request['Content-Type'] = 'application/json'
request['Accept'] = 'application/json'
request.body = refund_request.to_json
response = http.request(request)
if response.code.to_i != 200 && response.code.to_i != 201
raise "HTTP Error: #{response.code} - #{response.body}"
end
response.body
end
end
# Example: Partial refund
client = TransactionClient.new(
'https://your-api-domain.com',
'your-bearer-token-here'
)
refund_request = {
amount: {
dollars: 25.00
},
orderid: 'PARTIAL-REFUND-001',
customAttributes: {
refundReason: 'Partial product damage'
}
}
result = client.initiate_refund(
'12345678901',
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
refund_request
)
puts result
Example Request Bodies
Example 1: Full Refund (No Amount Specified)
{
"orderid": "REFUND-20260402-001",
"customAttributes": {
"refundReason": "Customer returned all items",
"returnDate": "2026-04-02",
"processedBy": "John Doe"
}
}
Example 2: Partial Refund - $25.00
{
"amount": {
"dollars": 25.00
},
"orderid": "PARTIAL-REFUND-001",
"customAttributes": {
"refundReason": "One item damaged - refunding damaged item only",
"damagedItem": "Widget-123"
}
}
Example 3: Partial Refund - Using Cents
{
"amount": {
"cents": 2500
},
"orderid": "PARTIAL-REFUND-002",
"customAttributes": {
"refundReason": "Price adjustment",
"originalPrice": "100.00",
"adjustedPrice": "75.00"
}
}
Example 4: Goodwill Refund
{
"amount": {
"dollars": 10.00
},
"orderid": "GOODWILL-REFUND-001",
"customAttributes": {
"refundReason": "Customer service goodwill gesture",
"issue": "Late delivery",
"approvedBy": "Manager",
"supportTicket": "TICKET-12345"
}
}
Response Format
Success Response (HTTP 201)
{
"responseStatus": "TransactionRefunded",
"responseMessage": "Refund created successfully",
"responseCode": "000",
"responseReason": null,
"responseResult": "success",
"legacyResponseCode": "A",
"correlationId": "refund-123-456-789",
"customAttributes": {
"refundReason": "Partial product damage",
"originalTransactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
},
"transactionId": "c3d4e5f6-a7b8-9012-cdef-a12345678901",
"retref": 123456791,
"merchid": "12345678901",
"transactionAmount": "25.00",
"authAmount": "25.00",
"accttype": "VISA",
"maskedAccountNumber": "************1234",
"lastfour": "1234",
"accountHolderName": "John Doe",
"orderid": "PARTIAL-REFUND-001",
"requestDate": "2026-04-02T14:30:00Z",
"authDate": "2026-04-02T14:30:03Z",
"settlementStatus": "Authorized",
"linkedTransactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
HTTP Status Codes
| Status Code | Description |
|---|---|
| 201 | Created - Refund initiated successfully |
| 400 | Bad Request - Validation error (invalid amount, exceeds available balance, etc.) |
| 401 | Unauthorized - Invalid or expired bearer token |
| 404 | Not Found - Transaction with specified ID does not exist |
| 409 | Conflict - Transaction already fully refunded or cannot be refunded |
| 410 | Gone - Missing payment info required for refund |
| 500 | Internal Server Error - Server error processing refund |
Error Responses
Refund Exceeds Available Amount (HTTP 400)
{
"responseStatus": "ValidationError",
"responseMessage": "Refund amount exceeds available balance",
"responseCode": "400",
"responseReason": "The transaction has $50 available for refund, but you requested $75",
"responseResult": "failure",
"correlationId": "error-400-123-456"
}
Already Fully Refunded (HTTP 409)
{
"responseStatus": "Error",
"responseMessage": "Transaction already fully refunded",
"responseCode": "409",
"responseReason": "This transaction has been completely refunded",
"responseResult": "failure",
"correlationId": "error-409-789-012"
}
Best Practices
1. Check Refundable Amount First
Before initiating a refund, retrieve the transaction to check how much is available to refund:
// Get original transaction
var originalTxn = await client.GetTransactionAsync(merchantId, transactionId);
decimal originalAmount = decimal.Parse(originalTxn.TransactionAmount);
// Calculate refundable amount (original - already refunded)
decimal refundableAmount = CalculateRefundableAmount(originalTxn);
if (refundAmount <= refundableAmount)
{
await client.InitiateRefundAsync(merchantId, transactionId, refundRequest);
}
else
{
Console.WriteLine($"Cannot refund ${refundAmount}. Only ${refundableAmount} available.");
}
2. Always Include Reason in Custom Attributes
Store the reason for the refund in custom attributes for audit purposes:
var refundRequest = new RefundRequest
{
Amount = new AmountRequest { Dollars = 25.00m },
CustomAttributes = new Dictionary<string, string>
{
{ "refundReason", "Product arrived damaged" },
{ "processedBy", currentUserId },
{ "supportTicket", ticketNumber },
{ "approvalDate", DateTime.Now.ToString("yyyy-MM-dd") }
}
};
3. Use Descriptive Order IDs
Create meaningful order IDs that help you track refunds:
// Good - descriptive and unique "REFUND-20260402-001" "PARTIAL-REFUND-ORDER-12345" "GOODWILL-REFUND-TICKET-789" // Bad - unclear "REF-001" "12345"
4. Log All Refund Requests
Maintain an audit log of all refunds:
await AuditLog.LogAsync(new AuditEntry
{
Action = "InitiateRefund",
OriginalTransactionId = transactionId,
RefundAmount = refundAmount,
Reason = refundReason,
ProcessedBy = currentUserId,
Timestamp = DateTime.UtcNow
});
await client.InitiateRefundAsync(merchantId, transactionId, refundRequest);
5. Notify Customers
Send email confirmation when refund is processed:
await client.InitiateRefundAsync(merchantId, transactionId, refundRequest);
await SendEmail(customerEmail,
"Refund Processed",
$"A refund of ${refundAmount} has been initiated. " +
"Please allow 3-5 business days for funds to appear."
);
6. Handle Partial Refunds Carefully
Keep track of how much has been refunded to avoid exceeding the original amount:
// Get transaction and calculate remaining refundable amount
var txn = await client.GetTransactionAsync(merchantId, transactionId);
decimal originalAmount = decimal.Parse(txn.TransactionAmount);
decimal totalRefunded = CalculateTotalRefunded(txn);
decimal remainingRefundable = originalAmount - totalRefunded;
Console.WriteLine($"Original: ${originalAmount}");
Console.WriteLine($"Already refunded: ${totalRefunded}");
Console.WriteLine($"Available to refund: ${remainingRefundable}");
Common Use Cases
1. Partial Product Return
// Customer returns 2 of 5 items @ $25 each
var refundRequest = new RefundRequest
{
Amount = new AmountRequest { Dollars = 50.00m }, // 2 items × $25
OrderId = "PARTIAL-RETURN-ORD-12345",
CustomAttributes = new Dictionary<string, string>
{
{ "refundReason", "Customer returned 2 of 5 items" },
{ "returnedItems", "2" },
{ "totalItems", "5" }
}
};
2. Price Match Adjustment
// Customer found item cheaper elsewhere - refund difference
var priceDifference = originalPrice - competitorPrice;
var refundRequest = new RefundRequest
{
Amount = new AmountRequest { Dollars = priceDifference },
OrderId = $"PRICE-MATCH-{orderId}",
CustomAttributes = new Dictionary<string, string>
{
{ "refundReason", "Price match guarantee" },
{ "originalPrice", originalPrice.ToString() },
{ "matchedPrice", competitorPrice.ToString() },
{ "competitor", "CompetitorStore" }
}
};
3. Shipping Cost Refund
// Refund shipping cost due to late delivery
var refundRequest = new RefundRequest
{
Amount = new AmountRequest { Dollars = 15.00m }, // Shipping cost
OrderId = $"SHIPPING-REFUND-{orderId}",
CustomAttributes = new Dictionary<string, string>
{
{ "refundReason", "Late delivery - refunding shipping" },
{ "originalDeliveryDate", "2026-03-30" },
{ "actualDeliveryDate", "2026-04-05" }
}
};
4. Damaged Product - Keep Item
// Partial refund for damage, customer keeps item
var refundRequest = new RefundRequest
{
Amount = new AmountRequest { Dollars = 30.00m }, // 30% refund
OrderId = $"DAMAGE-DISCOUNT-{orderId}",
CustomAttributes = new Dictionary<string, string>
{
{ "refundReason", "Product arrived with minor cosmetic damage" },
{ "damageType", "Scratches on surface" },
{ "refundPercentage", "30%" },
{ "customerKeepsItem", "true" }
}
};
5. Subscription Cancellation Prorated Refund
// Refund unused portion of subscription
var unusedAmount = CalculateUnusedSubscriptionAmount(subscriptionEndDate);
var refundRequest = new RefundRequest
{
Amount = new AmountRequest { Dollars = unusedAmount },
OrderId = $"PRORATED-REFUND-SUB-{subscriptionId}",
CustomAttributes = new Dictionary<string, string>
{
{ "refundReason", "Subscription cancelled - prorated refund" },
{ "subscriptionId", subscriptionId },
{ "cancellationDate", DateTime.Now.ToString("yyyy-MM-dd") },
{ "unusedDays", unusedDays.ToString() }
}
};
Calculating Refundable Amount
- Get the original transaction amount
- Sum all previous refunds for this transaction
- Subtract total refunded from original amount
- The result is the maximum you can still refund
Example Calculation:
Original Transaction: $100.00 Previous Refunds: - Refund 1: $25.00 - Refund 2: $15.00 Total Refunded: $40.00 Remaining Refundable: $100.00 - $40.00 = $60.00 You can refund up to $60.00 more.
Troubleshooting
- Check how much has already been refunded on this transaction
- Calculate remaining refundable amount before requesting refund
- Reduce refund amount to be within available balance
- Verify you're not trying to refund more than the original transaction
- The transaction has been completely refunded already
- Check transaction settlement status - should show "Refunded"
- No further refunds are possible on this transaction
- The original payment method information is no longer available
- This can happen with very old transactions
- Contact support to process the refund manually
- Refunds take 3-5 business days for credit cards
- ACH refunds take 5-7 business days
- Check that refund was successful (settlementStatus should be "Authorized" or "Accepted")
- Verify refund amount and confirm it went to the correct account
- Check with payment processor if delayed beyond expected timeframe
Additional Notes
- This endpoint creates a new refund transaction with its own transaction ID
- The
linkedTransactionIdfield links the refund back to the original transaction - You can issue multiple partial refunds as long as the total doesn't exceed the original amount
- Refunds typically take 3-5 business days (credit cards) or 5-7 days (ACH) to complete
- The original transaction's settlement status changes to "PartiallyRefunded" or "Refunded"
- You cannot refund a transaction that hasn't settled yet - wait for settlement or use DELETE to void
- Each refund gets its own order ID for tracking purposes
- Refund transactions appear in search results just like original transactions
- Custom attributes on refunds are stored separately from the original transaction's custom attributes