Procare Pay Integration Service

Complete API Documentation for Payment Processing

Initiate Refund

POST /v1/rest/merchants/{merchantId}/transactions/{transactionId}/refunds

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.

When to use this endpoint vs DELETE transaction:
  • 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
Important Considerations:
  • 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

merchantId (required)
Type: string
Description: The merchant identifier (BAM ID or TEID)
transactionId (required)
Type: string
Description: The GUID or numeric RetRef of the transaction to refund

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
About the amount field:
  • If you omit amount entirely, 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.

Note: The bearer token must be a valid Cognito token.

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

How to calculate remaining refundable amount:
  1. Get the original transaction amount
  2. Sum all previous refunds for this transaction
  3. Subtract total refunded from original amount
  4. 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

Issue: Getting 400 error "Refund amount exceeds available balance"
  • 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
Issue: Getting 409 Conflict - Already fully refunded
  • The transaction has been completely refunded already
  • Check transaction settlement status - should show "Refunded"
  • No further refunds are possible on this transaction
Issue: Getting 410 Gone - Missing payment info
  • The original payment method information is no longer available
  • This can happen with very old transactions
  • Contact support to process the refund manually
Issue: Customer hasn't received refund
  • 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 linkedTransactionId field 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