Partial Refunds
Partial refunds allow you to return a portion of a charge amount to your customers. This is useful for scenarios like partial returns, price adjustments, shipping refunds, or any situation where a full refund isn't necessary.
Overviewโ
Partial refunds provide flexibility in how you handle returns and adjustments:
- Flexible Amounts: Refund any amount up to the original charge amount
- Multiple Refunds: Create multiple partial refunds until fully refunded
- Remaining Balance: Track how much can still be refunded
- Same Process: Use the same API as full refunds, just specify an amount
- Audit Trail: All partial refunds are tracked on the charge
Key Featuresโ
- Precise Amount Control: Specify exact refund amounts
- Currency Aware: Automatically uses the charge's currency
- Balance Tracking: See remaining refundable amount
- Metadata Support: Tag each refund with context
- No Limits on Count: Create as many partial refunds as needed
- Automatic Validation: System prevents over-refunding
When to Use Partial Refundsโ
Common scenarios for partial refunds:
- Partial Product Returns: Customer returns some items from an order
- Price Adjustments: Correcting billing errors or applying discounts
- Shipping Refunds: Refunding delivery fees only
- Damaged Items: Partial compensation for damaged goods
- Service Credits: Offering partial refunds for service issues
- Promotional Adjustments: Applying coupons post-purchase
- Quantity Corrections: Refunding for incorrect quantities
- Bundle Adjustments: Refunding specific items from a bundle
Creating Partial Refunds via APIโ
Basic Partial Refundโ
Specify the amount parameter to create a partial refund:
- Node.js
- Python
- Ruby
- PHP
- Go
const omise = require('omise')({
secretKey: 'skey_test_123456789',
});
// Create a partial refund
async function createPartialRefund(chargeId, amount) {
try {
const refund = await omise.charges.createRefund(chargeId, {
amount: amount, // Amount in smallest currency unit
metadata: {
reason: 'Partial product return',
items_returned: '2 out of 5 items'
}
});
console.log('Partial refund created:', refund.id);
console.log('Amount refunded:', refund.amount);
console.log('Remaining refundable:', calculateRemaining(refund));
return refund;
} catch (error) {
console.error('Partial refund failed:', error.message);
throw error;
}
}
// Calculate remaining refundable amount
function calculateRemaining(refund) {
// Get the charge to see total refunded
return omise.charges.retrieve(refund.charge).then(charge => {
const totalRefunded = charge.refunded_amount || 0;
const remaining = charge.amount - totalRefunded;
return remaining;
});
}
// Refund shipping cost only
async function refundShippingCost(chargeId, shippingAmount) {
const refund = await omise.charges.createRefund(chargeId, {
amount: shippingAmount,
metadata: {
reason: 'shipping_refund',
type: 'delivery_fee'
}
});
console.log(`Refunded shipping: ${shippingAmount / 100} THB`);
return refund;
}
// Example usage
createPartialRefund('chrg_test_123456789', 50000); // Refund 500 THB
refundShippingCost('chrg_test_123456789', 10000); // Refund 100 THB shipping
import omise
omise.api_secret = 'skey_test_123456789'
def create_partial_refund(charge_id, amount):
"""Create a partial refund for a charge"""
try:
refund = omise.Charge.retrieve(charge_id).refund(
amount=amount, # Amount in smallest currency unit
metadata={
'reason': 'Partial product return',
'items_returned': '2 out of 5 items'
}
)
print(f"Partial refund created: {refund.id}")
print(f"Amount refunded: {refund.amount}")
# Calculate remaining
charge = omise.Charge.retrieve(charge_id)
remaining = charge.amount - charge.refunded_amount
print(f"Remaining refundable: {remaining}")
return refund
except omise.errors.BaseError as e:
print(f"Partial refund failed: {str(e)}")
raise
def refund_shipping_cost(charge_id, shipping_amount):
"""Refund only the shipping cost"""
refund = omise.Charge.retrieve(charge_id).refund(
amount=shipping_amount,
metadata={
'reason': 'shipping_refund',
'type': 'delivery_fee'
}
)
print(f"Refunded shipping: {shipping_amount / 100} THB")
return refund
def refund_line_items(charge_id, items):
"""Refund specific line items from an order"""
total_refund = sum(item['price'] * item['quantity'] for item in items)
refund = omise.Charge.retrieve(charge_id).refund(
amount=total_refund,
metadata={
'reason': 'partial_line_item_refund',
'items': str(items),
'item_count': len(items)
}
)
return refund
# Example usage
create_partial_refund('chrg_test_123456789', 50000) # Refund 500 THB
refund_shipping_cost('chrg_test_123456789', 10000) # Refund 100 THB
# Refund specific items
items_to_refund = [
{'name': 'T-Shirt', 'price': 29900, 'quantity': 1},
{'name': 'Socks', 'price': 9900, 'quantity': 2}
]
refund_line_items('chrg_test_123456789', items_to_refund)
require 'omise'
Omise.api_key = 'skey_test_123456789'
# Create a partial refund
def create_partial_refund(charge_id, amount)
begin
charge = Omise::Charge.retrieve(charge_id)
refund = charge.refund(
amount: amount, # Amount in smallest currency unit
metadata: {
reason: 'Partial product return',
items_returned: '2 out of 5 items'
}
)
puts "Partial refund created: #{refund.id}"
puts "Amount refunded: #{refund.amount}"
# Calculate remaining
charge.reload
remaining = charge.amount - charge.refunded_amount
puts "Remaining refundable: #{remaining}"
refund
rescue Omise::Error => e
puts "Partial refund failed: #{e.message}"
raise
end
end
# Refund shipping cost only
def refund_shipping_cost(charge_id, shipping_amount)
charge = Omise::Charge.retrieve(charge_id)
refund = charge.refund(
amount: shipping_amount,
metadata: {
reason: 'shipping_refund',
type: 'delivery_fee'
}
)
puts "Refunded shipping: #{shipping_amount / 100.0} THB"
refund
end
# Refund specific line items
def refund_line_items(charge_id, items)
total_refund = items.sum { |item| item[:price] * item[:quantity] }
charge = Omise::Charge.retrieve(charge_id)
refund = charge.refund(
amount: total_refund,
metadata: {
reason: 'partial_line_item_refund',
items: items.to_s,
item_count: items.length
}
)
refund
end
# Price adjustment
def apply_price_adjustment(charge_id, discount_amount, reason)
charge = Omise::Charge.retrieve(charge_id)
refund = charge.refund(
amount: discount_amount,
metadata: {
reason: 'price_adjustment',
adjustment_type: reason,
applied_at: Time.now.iso8601
}
)
puts "Price adjustment applied: #{discount_amount / 100.0} THB"
refund
end
# Example usage
create_partial_refund('chrg_test_123456789', 50000)
refund_shipping_cost('chrg_test_123456789', 10000)
items = [
{ name: 'T-Shirt', price: 29900, quantity: 1 },
{ name: 'Socks', price: 9900, quantity: 2 }
]
refund_line_items('chrg_test_123456789', items)
apply_price_adjustment('chrg_test_123456789', 5000, 'price_match')
<?php
require_once 'vendor/autoload.php';
define('OMISE_SECRET_KEY', 'skey_test_123456789');
// Create a partial refund
function createPartialRefund($chargeId, $amount) {
try {
$charge = OmiseCharge::retrieve($chargeId);
$refund = $charge->refund([
'amount' => $amount, // Amount in smallest currency unit
'metadata' => [
'reason' => 'Partial product return',
'items_returned' => '2 out of 5 items'
]
]);
echo "Partial refund created: {$refund['id']}\n";
echo "Amount refunded: {$refund['amount']}\n";
// Calculate remaining
$charge->reload();
$remaining = $charge['amount'] - $charge['refunded_amount'];
echo "Remaining refundable: {$remaining}\n";
return $refund;
} catch (Exception $e) {
echo "Partial refund failed: {$e->getMessage()}\n";
throw $e;
}
}
// Refund shipping cost only
function refundShippingCost($chargeId, $shippingAmount) {
$charge = OmiseCharge::retrieve($chargeId);
$refund = $charge->refund([
'amount' => $shippingAmount,
'metadata' => [
'reason' => 'shipping_refund',
'type' => 'delivery_fee'
]
]);
echo "Refunded shipping: " . ($shippingAmount / 100) . " THB\n";
return $refund;
}
// Refund specific line items
function refundLineItems($chargeId, $items) {
$totalRefund = 0;
foreach ($items as $item) {
$totalRefund += $item['price'] * $item['quantity'];
}
$charge = OmiseCharge::retrieve($chargeId);
$refund = $charge->refund([
'amount' => $totalRefund,
'metadata' => [
'reason' => 'partial_line_item_refund',
'items' => json_encode($items),
'item_count' => count($items)
]
]);
return $refund;
}
// Price adjustment
function applyPriceAdjustment($chargeId, $discountAmount, $reason) {
$charge = OmiseCharge::retrieve($chargeId);
$refund = $charge->refund([
'amount' => $discountAmount,
'metadata' => [
'reason' => 'price_adjustment',
'adjustment_type' => $reason,
'applied_at' => date('c')
]
]);
echo "Price adjustment applied: " . ($discountAmount / 100) . " THB\n";
return $refund;
}
// Check remaining refundable amount
function getRemainingRefundable($chargeId) {
$charge = OmiseCharge::retrieve($chargeId);
$remaining = $charge['amount'] - $charge['refunded_amount'];
return [
'charge_amount' => $charge['amount'],
'refunded_amount' => $charge['refunded_amount'],
'remaining' => $remaining,
'currency' => $charge['currency']
];
}
// Example usage
createPartialRefund('chrg_test_123456789', 50000);
refundShippingCost('chrg_test_123456789', 10000);
$items = [
['name' => 'T-Shirt', 'price' => 29900, 'quantity' => 1],
['name' => 'Socks', 'price' => 9900, 'quantity' => 2]
];
refundLineItems('chrg_test_123456789', $items);
applyPriceAdjustment('chrg_test_123456789', 5000, 'price_match');
$remaining = getRemainingRefundable('chrg_test_123456789');
print_r($remaining);
?>
package main
import (
"encoding/json"
"fmt"
"log"
"time"
"github.com/omise/omise-go"
"github.com/omise/omise-go/operations"
)
const secretKey = "skey_test_123456789"
// LineItem represents an item in an order
type LineItem struct {
Name string `json:"name"`
Price int64 `json:"price"`
Quantity int `json:"quantity"`
}
// CreatePartialRefund creates a partial refund for a charge
func CreatePartialRefund(chargeID string, amount int64) (*omise.Refund, error) {
client, err := omise.NewClient(secretKey, "")
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}
refund := &omise.Refund{}
err = client.Do(refund, &operations.CreateRefund{
ChargeID: chargeID,
Amount: amount,
Metadata: map[string]interface{}{
"reason": "Partial product return",
"items_returned": "2 out of 5 items",
},
})
if err != nil {
return nil, fmt.Errorf("partial refund failed: %w", err)
}
fmt.Printf("Partial refund created: %s\n", refund.ID)
fmt.Printf("Amount refunded: %d\n", refund.Amount)
// Calculate remaining
charge := &omise.Charge{}
err = client.Do(charge, &operations.RetrieveCharge{ChargeID: chargeID})
if err == nil {
remaining := charge.Amount - charge.RefundedAmount
fmt.Printf("Remaining refundable: %d\n", remaining)
}
return refund, nil
}
// RefundShippingCost refunds only the shipping cost
func RefundShippingCost(chargeID string, shippingAmount int64) (*omise.Refund, error) {
client, err := omise.NewClient(secretKey, "")
if err != nil {
return nil, err
}
refund := &omise.Refund{}
err = client.Do(refund, &operations.CreateRefund{
ChargeID: chargeID,
Amount: shippingAmount,
Metadata: map[string]interface{}{
"reason": "shipping_refund",
"type": "delivery_fee",
},
})
if err != nil {
return nil, err
}
fmt.Printf("Refunded shipping: %.2f THB\n", float64(shippingAmount)/100)
return refund, nil
}
// RefundLineItems refunds specific line items from an order
func RefundLineItems(chargeID string, items []LineItem) (*omise.Refund, error) {
client, err := omise.NewClient(secretKey, "")
if err != nil {
return nil, err
}
// Calculate total refund amount
var totalRefund int64
for _, item := range items {
totalRefund += item.Price * int64(item.Quantity)
}
// Convert items to JSON for metadata
itemsJSON, _ := json.Marshal(items)
refund := &omise.Refund{}
err = client.Do(refund, &operations.CreateRefund{
ChargeID: chargeID,
Amount: totalRefund,
Metadata: map[string]interface{}{
"reason": "partial_line_item_refund",
"items": string(itemsJSON),
"item_count": len(items),
},
})
return refund, err
}
// ApplyPriceAdjustment applies a price adjustment as a partial refund
func ApplyPriceAdjustment(chargeID string, discountAmount int64, reason string) (*omise.Refund, error) {
client, err := omise.NewClient(secretKey, "")
if err != nil {
return nil, err
}
refund := &omise.Refund{}
err = client.Do(refund, &operations.CreateRefund{
ChargeID: chargeID,
Amount: discountAmount,
Metadata: map[string]interface{}{
"reason": "price_adjustment",
"adjustment_type": reason,
"applied_at": time.Now().Format(time.RFC3339),
},
})
if err != nil {
return nil, err
}
fmt.Printf("Price adjustment applied: %.2f THB\n", float64(discountAmount)/100)
return refund, nil
}
// RemainingRefundable represents remaining refundable information
type RemainingRefundable struct {
ChargeAmount int64 `json:"charge_amount"`
RefundedAmount int64 `json:"refunded_amount"`
Remaining int64 `json:"remaining"`
Currency string `json:"currency"`
}
// GetRemainingRefundable checks how much can still be refunded
func GetRemainingRefundable(chargeID string) (*RemainingRefundable, error) {
client, err := omise.NewClient(secretKey, "")
if err != nil {
return nil, err
}
charge := &omise.Charge{}
err = client.Do(charge, &operations.RetrieveCharge{ChargeID: chargeID})
if err != nil {
return nil, err
}
return &RemainingRefundable{
ChargeAmount: charge.Amount,
RefundedAmount: charge.RefundedAmount,
Remaining: charge.Amount - charge.RefundedAmount,
Currency: charge.Currency,
}, nil
}
func main() {
// Example usage
refund, err := CreatePartialRefund("chrg_test_123456789", 50000)
if err != nil {
log.Fatalf("Failed to create partial refund: %v", err)
}
RefundShippingCost("chrg_test_123456789", 10000)
items := []LineItem{
{Name: "T-Shirt", Price: 29900, Quantity: 1},
{Name: "Socks", Price: 9900, Quantity: 2},
}
RefundLineItems("chrg_test_123456789", items)
ApplyPriceAdjustment("chrg_test_123456789", 5000, "price_match")
remaining, _ := GetRemainingRefundable("chrg_test_123456789")
fmt.Printf("Remaining: %+v\n", remaining)
}
API Responseโ
{
"object": "refund",
"id": "rfnd_test_5xyz789abc",
"location": "/charges/chrg_test_123456789/refunds/rfnd_test_5xyz789abc",
"amount": 50000,
"currency": "thb",
"charge": "chrg_test_123456789",
"transaction": "trxn_test_5xyz789abc",
"created": "2024-01-15T10:30:00Z",
"status": "pending",
"metadata": {
"reason": "Partial product return",
"items_returned": "2 out of 5 items"
}
}
Multiple Partial Refundsโ
You can create multiple partial refunds for a single charge:
async function handleMultiplePartialRefunds(chargeId) {
// Original charge: 100,000 (1,000 THB)
// First partial refund - shipping
const refund1 = await omise.charges.createRefund(chargeId, {
amount: 10000, // 100 THB
metadata: { reason: 'shipping_refund' }
});
// Second partial refund - one item
const refund2 = await omise.charges.createRefund(chargeId, {
amount: 30000, // 300 THB
metadata: { reason: 'item_return', item: 'Product A' }
});
// Third partial refund - another item
const refund3 = await omise.charges.createRefund(chargeId, {
amount: 25000, // 250 THB
metadata: { reason: 'item_return', item: 'Product B' }
});
// Check remaining: 100,000 - 10,000 - 30,000 - 25,000 = 35,000 (350 THB)
const charge = await omise.charges.retrieve(chargeId);
console.log('Total refunded:', charge.refunded_amount);
console.log('Remaining:', charge.amount - charge.refunded_amount);
return [refund1, refund2, refund3];
}
Common Use Casesโ
1. Line Item Refundsโ
Refund specific items from an order:
class OrderRefundManager:
def __init__(self, charge_id):
self.charge_id = charge_id
self.charge = omise.Charge.retrieve(charge_id)
def refund_items(self, item_ids):
"""Refund specific items by their IDs"""
order = self.get_order_details()
# Calculate refund amount
refund_amount = 0
refunded_items = []
for item_id in item_ids:
item = next((i for i in order['items'] if i['id'] == item_id), None)
if item:
refund_amount += item['price'] * item['quantity']
refunded_items.append(item)
# Create refund
refund = self.charge.refund(
amount=refund_amount,
metadata={
'reason': 'line_item_refund',
'refunded_items': str(refunded_items),
'item_count': len(refunded_items)
}
)
# Update order
self.update_order_items(item_ids, 'refunded')
return refund
def refund_by_percentage(self, percentage):
"""Refund a percentage of the total charge"""
if not 0 < percentage <= 100:
raise ValueError("Percentage must be between 0 and 100")
refund_amount = int(self.charge.amount * percentage / 100)
refund = self.charge.refund(
amount=refund_amount,
metadata={
'reason': 'percentage_refund',
'percentage': percentage
}
)
return refund
def get_remaining_refundable(self):
"""Get remaining refundable amount"""
self.charge = omise.Charge.retrieve(self.charge_id)
return self.charge.amount - self.charge.refunded_amount
# Example usage
manager = OrderRefundManager('chrg_test_123456789')
manager.refund_items(['item_1', 'item_3'])
manager.refund_by_percentage(10) # 10% discount
remaining = manager.get_remaining_refundable()
2. Progressive Refundsโ
Handle refunds in stages:
class ProgressiveRefund
def initialize(charge_id)
@charge_id = charge_id
@charge = Omise::Charge.retrieve(charge_id)
@refund_schedule = []
end
def schedule_refund(amount, date, reason)
@refund_schedule << {
amount: amount,
date: date,
reason: reason,
status: 'scheduled'
}
end
def process_due_refunds
due_refunds = @refund_schedule.select { |r|
r[:status] == 'scheduled' && r[:date] <= Date.today
}
due_refunds.each do |scheduled|
begin
refund = @charge.refund(
amount: scheduled[:amount],
metadata: {
reason: scheduled[:reason],
scheduled_date: scheduled[:date].to_s,
processing_date: Date.today.to_s
}
)
scheduled[:status] = 'processed'
scheduled[:refund_id] = refund.id
puts "Processed refund: #{refund.id} for #{scheduled[:amount]}"
rescue Omise::Error => e
scheduled[:status] = 'failed'
scheduled[:error] = e.message
puts "Failed to process refund: #{e.message}"
end
end
@refund_schedule
end
def get_schedule_summary
{
total_scheduled: @refund_schedule.sum { |r| r[:amount] },
processed: @refund_schedule.count { |r| r[:status] == 'processed' },
pending: @refund_schedule.count { |r| r[:status] == 'scheduled' },
failed: @refund_schedule.count { |r| r[:status] == 'failed' }
}
end
end
# Example: Refund in 3 installments
progressive = ProgressiveRefund.new('chrg_test_123456789')
progressive.schedule_refund(33333, Date.today, 'First installment')
progressive.schedule_refund(33333, Date.today + 30, 'Second installment')
progressive.schedule_refund(33334, Date.today + 60, 'Third installment')
progressive.process_due_refunds
3. Price Match Refundsโ
Handle price match guarantees:
class PriceMatchRefund {
private $chargeId;
private $originalPrice;
public function __construct($chargeId) {
$this->chargeId = $chargeId;
$charge = OmiseCharge::retrieve($chargeId);
$this->originalPrice = $charge['amount'];
}
public function applyPriceMatch($competitorPrice, $competitorName) {
// Calculate difference
$difference = $this->originalPrice - $competitorPrice;
if ($difference <= 0) {
return [
'success' => false,
'message' => 'Competitor price is not lower'
];
}
// Apply price match policy (e.g., 110% of difference)
$refundAmount = intval($difference * 1.10);
// Ensure we don't refund more than charged
$charge = OmiseCharge::retrieve($this->chargeId);
$remaining = $charge['amount'] - $charge['refunded_amount'];
$refundAmount = min($refundAmount, $remaining);
// Create refund
$refund = $charge->refund([
'amount' => $refundAmount,
'metadata' => [
'reason' => 'price_match',
'competitor' => $competitorName,
'competitor_price' => $competitorPrice,
'original_price' => $this->originalPrice,
'difference' => $difference,
'policy_rate' => '110%'
]
]);
return [
'success' => true,
'refund_id' => $refund['id'],
'amount' => $refundAmount,
'savings' => $difference,
'bonus' => $refundAmount - $difference
];
}
public function applyDiscountCode($discountAmount, $code) {
$charge = OmiseCharge::retrieve($this->chargeId);
$remaining = $charge['amount'] - $charge['refunded_amount'];
if ($discountAmount > $remaining) {
throw new Exception('Discount amount exceeds remaining refundable amount');
}
$refund = $charge->refund([
'amount' => $discountAmount,
'metadata' => [
'reason' => 'discount_code',
'code' => $code,
'applied_at' => date('c')
]
]);
return $refund;
}
}
// Example usage
$priceMatch = new PriceMatchRefund('chrg_test_123456789');
$result = $priceMatch->applyPriceMatch(95000, 'Competitor Store');
if ($result['success']) {
echo "Price match applied! Refunding {$result['amount']} satangs\n";
echo "You save {$result['savings']} + bonus {$result['bonus']}\n";
}
4. Subscription Prorationโ
Prorate refunds for subscription cancellations:
class SubscriptionRefund {
constructor(chargeId, subscriptionStart, subscriptionEnd) {
this.chargeId = chargeId;
this.subscriptionStart = new Date(subscriptionStart);
this.subscriptionEnd = new Date(subscriptionEnd);
}
async calculateProratedRefund(cancellationDate) {
const charge = await omise.charges.retrieve(this.chargeId);
// Calculate days
const totalDays = this.daysBetween(this.subscriptionStart, this.subscriptionEnd);
const usedDays = this.daysBetween(this.subscriptionStart, cancellationDate);
const remainingDays = totalDays - usedDays;
// Calculate refund amount
const dailyRate = charge.amount / totalDays;
const refundAmount = Math.floor(dailyRate * remainingDays);
return {
totalAmount: charge.amount,
totalDays: totalDays,
usedDays: usedDays,
remainingDays: remainingDays,
refundAmount: refundAmount,
effectiveRate: dailyRate
};
}
async processProratedRefund(cancellationDate, reason) {
const calculation = await this.calculateProratedRefund(cancellationDate);
const refund = await omise.charges.createRefund(this.chargeId, {
amount: calculation.refundAmount,
metadata: {
reason: 'subscription_cancellation_prorated',
cancellation_date: cancellationDate.toISOString(),
subscription_start: this.subscriptionStart.toISOString(),
subscription_end: this.subscriptionEnd.toISOString(),
total_days: calculation.totalDays,
used_days: calculation.usedDays,
remaining_days: calculation.remainingDays,
cancellation_reason: reason
}
});
return {
refund: refund,
calculation: calculation
};
}
daysBetween(date1, date2) {
const oneDay = 24 * 60 * 60 * 1000;
return Math.round(Math.abs((date2 - date1) / oneDay));
}
}
// Example usage
const subsRefund = new SubscriptionRefund(
'chrg_test_123456789',
'2024-01-01',
'2024-01-31'
);
const result = await subsRefund.processProratedRefund(
new Date('2024-01-15'),
'Customer requested cancellation'
);
console.log(`Refunding ${result.calculation.refundAmount} for ${result.calculation.remainingDays} unused days`);
Tracking Refund Balanceโ
Monitor how much has been refunded and what remains:
class RefundTracker:
def __init__(self, charge_id):
self.charge_id = charge_id
self.refresh()
def refresh(self):
"""Refresh charge data"""
self.charge = omise.Charge.retrieve(self.charge_id)
def get_refund_summary(self):
"""Get comprehensive refund summary"""
self.refresh()
refunds = self.charge.refunds.data if hasattr(self.charge, 'refunds') else []
return {
'charge_id': self.charge_id,
'original_amount': self.charge.amount,
'refunded_amount': self.charge.refunded_amount,
'remaining_amount': self.charge.amount - self.charge.refunded_amount,
'refund_count': len(refunds),
'fully_refunded': self.charge.refunded,
'currency': self.charge.currency,
'refunds': [
{
'id': r.id,
'amount': r.amount,
'status': r.status,
'created': r.created,
'reason': r.metadata.get('reason', 'N/A') if r.metadata else 'N/A'
}
for r in refunds
]
}
def can_refund(self, amount):
"""Check if amount can be refunded"""
self.refresh()
remaining = self.charge.amount - self.charge.refunded_amount
return amount <= remaining
def get_refund_percentage(self):
"""Get percentage of charge that has been refunded"""
self.refresh()
if self.charge.amount == 0:
return 0
return (self.charge.refunded_amount / self.charge.amount) * 100
def format_summary(self):
"""Format summary for display"""
summary = self.get_refund_summary()
output = f"""
Refund Summary for {summary['charge_id']}
{'=' * 50}
Original Amount: {summary['original_amount'] / 100:.2f} {summary['currency'].upper()}
Refunded Amount: {summary['refunded_amount'] / 100:.2f} {summary['currency'].upper()}
Remaining Amount: {summary['remaining_amount'] / 100:.2f} {summary['currency'].upper()}
Number of Refunds: {summary['refund_count']}
Fully Refunded: {'Yes' if summary['fully_refunded'] else 'No'}
Refund Percentage: {self.get_refund_percentage():.1f}%
Refund History:
"""
for refund in summary['refunds']:
output += f" - {refund['id']}: {refund['amount'] / 100:.2f} {summary['currency'].upper()} ({refund['status']})\n"
output += f" Reason: {refund['reason']}\n"
return output
# Example usage
tracker = RefundTracker('chrg_test_123456789')
print(tracker.format_summary())
if tracker.can_refund(50000):
print("Can refund 500 THB")
else:
print("Insufficient remaining balance")
Best Practicesโ
1. Validate Refund Amountโ
Always validate before creating a refund:
async function safePartialRefund(chargeId, amount, reason) {
// Retrieve charge
const charge = await omise.charges.retrieve(chargeId);
// Validate amount
if (amount <= 0) {
throw new Error('Refund amount must be positive');
}
const remaining = charge.amount - charge.refunded_amount;
if (amount > remaining) {
throw new Error(`Amount exceeds remaining refundable balance. Remaining: ${remaining}`);
}
// Validate currency (ensure amount is in smallest unit)
if (amount % 1 !== 0) {
throw new Error('Amount must be an integer (smallest currency unit)');
}
// Create refund
const refund = await omise.charges.createRefund(chargeId, {
amount: amount,
metadata: {
reason: reason,
validated_at: new Date().toISOString()
}
});
return refund;
}
2. Maintain Detailed Recordsโ
class RefundAuditLog
def self.log_refund(charge_id, refund, context = {})
log_entry = {
timestamp: Time.now.iso8601,
charge_id: charge_id,
refund_id: refund.id,
amount: refund.amount,
currency: refund.currency,
status: refund.status,
reason: refund.metadata['reason'],
user_id: context[:user_id],
ip_address: context[:ip_address],
user_agent: context[:user_agent],
notes: context[:notes]
}
# Save to database or logging system
save_audit_log(log_entry)
# Also log to file for compliance
File.open('refund_audit.log', 'a') do |f|
f.puts JSON.generate(log_entry)
end
end
def self.save_audit_log(entry)
# Save to your database
# AuditLog.create!(entry)
end
end
3. Handle Edge Casesโ
function createRefundWithValidation($chargeId, $amount, $metadata = []) {
try {
$charge = OmiseCharge::retrieve($chargeId);
// Check if charge is refundable
if (!$charge['paid']) {
throw new Exception('Charge has not been paid yet');
}
if ($charge['refunded']) {
throw new Exception('Charge has already been fully refunded');
}
// Check remaining balance
$remaining = $charge['amount'] - $charge['refunded_amount'];
if ($amount > $remaining) {
throw new Exception("Amount exceeds remaining balance of {$remaining}");
}
// Check if charge is too old (e.g., more than 180 days)
$chargeAge = time() - $charge['created'];
$maxAge = 180 * 24 * 60 * 60; // 180 days
if ($chargeAge > $maxAge) {
// Log warning but allow refund
error_log("Warning: Refunding charge older than 180 days: {$chargeId}");
}
// Create refund
$refund = $charge->refund([
'amount' => $amount,
'metadata' => $metadata
]);
return [
'success' => true,
'refund' => $refund
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
4. Customer Communicationโ
def refund_with_notification(charge_id, amount, reason, customer_email):
"""Create refund and notify customer"""
# Create refund
refund = omise.Charge.retrieve(charge_id).refund(
amount=amount,
metadata={'reason': reason}
)
# Send email notification
send_refund_email(
to=customer_email,
subject='Partial Refund Processed',
template='partial_refund',
data={
'refund_amount': format_currency(amount, refund.currency),
'reason': reason,
'refund_id': refund.id,
'estimated_days': '5-10 business days'
}
)
# Send SMS for large amounts
if amount >= 100000: # 1000 THB or more
send_sms_notification(
customer_phone,
f"Refund of {format_currency(amount, refund.currency)} has been processed. Check your email for details."
)
return refund
FAQโ
What's the minimum refund amount?โ
The minimum refund amount depends on the currency. For Thai Baht (THB), the minimum is 1 satang (0.01 THB). Always specify amounts in the smallest currency unit (satangs for THB, cents for USD, etc.).
Can I refund more than the original charge amount?โ
No, the total of all refunds (full and partial) cannot exceed the original charge amount. The API will return an error if you attempt to over-refund.
How many partial refunds can I create for one charge?โ
There's no limit to the number of partial refunds you can create, as long as the total doesn't exceed the original charge amount. You can create as many partial refunds as needed until the charge is fully refunded.
What happens if I try to refund more than the remaining balance?โ
The API will return an error with code invalid_request indicating that the refund amount exceeds the available balance. Always check the remaining refundable amount before creating a refund.
Can I cancel or modify a partial refund after it's created?โ
No, refunds cannot be canceled or modified once created. If you refund by mistake, you would need to charge the customer again (with their permission). Double-check amounts before creating refunds.
Do partial refunds affect transaction fees?โ
No, transaction fees are not refunded for partial refunds (or full refunds). You still pay the original transaction fee even if you refund part or all of the charge.
How do I track which items were refunded?โ
Use the metadata field to record detailed information about what was refunded. Include item IDs, SKUs, quantities, or any other relevant information to maintain a clear audit trail.
Can I automate partial refunds based on rules?โ
Yes, you can build automation around partial refunds. For example, automatically refund shipping if delivery is late, or refund a percentage for quality issues. Just ensure you have proper validation and error handling.
Related Resourcesโ
- Creating Refunds - Learn about full refunds
- Refund Limitations - Understand refund restrictions
- Transaction History - View refund history
- Webhooks Guide - Handle refund events
- API Reference - Complete refund API documentation
Next Stepsโ
- Understand refund limitations and restrictions
- Set up webhook handlers for refund events
- Learn about transaction tracking
- Implement reconciliation for refund matching
- Review error handling best practices