Creating Refunds
Refunds allow you to return money to your customers for transactions that have been successfully charged. This guide covers how to create full refunds through both the API and Dashboard.
Overviewโ
Refunds are essential for handling returns, cancellations, customer disputes, and other scenarios where you need to reverse a payment. When you create a refund:
- The full charge amount is returned to the customer's payment method
- Funds are typically returned within 5-10 business days (varies by card issuer)
- The refund status can be tracked throughout the process
- Original transaction fees are not returned by default
- The charge remains in your transaction history with a refunded status
Key Featuresโ
- Immediate Processing: Refunds are processed immediately through the API
- Multiple Payment Methods: Support for card refunds and alternative payment method refunds
- Status Tracking: Real-time refund status updates
- Automatic Notifications: Customer notifications (if configured)
- Metadata Support: Add custom metadata to track refund reasons
- Webhook Events: Receive notifications when refund status changes
When to Use Refundsโ
Common scenarios for creating refunds:
- Product Returns: Customer returns merchandise
- Service Cancellations: Customer cancels a service or subscription
- Duplicate Charges: Accidental double charging
- Customer Disputes: Resolving customer complaints
- Defective Products: Items that don't meet quality standards
- Out of Stock: Unable to fulfill orders
- Price Adjustments: Correcting billing errors
Creating Refunds via APIโ
Full Refundโ
Create a full refund by not specifying an amount:
- Node.js
- Python
- Ruby
- PHP
- Go
const omise = require('omise')({
secretKey: 'skey_test_123456789',
});
// Create a full refund
async function createFullRefund() {
try {
const refund = await omise.charges.createRefund('chrg_test_123456789', {
metadata: {
reason: 'Customer requested refund',
order_id: 'ORD-12345',
refund_type: 'product_return'
}
});
console.log('Refund created:', refund.id);
console.log('Amount refunded:', refund.amount);
console.log('Status:', refund.status);
console.log('Created at:', new Date(refund.created * 1000));
return refund;
} catch (error) {
console.error('Refund failed:', error.message);
throw error;
}
}
// Create refund with webhook handling
async function createRefundWithTracking(chargeId, reason) {
try {
const refund = await omise.charges.createRefund(chargeId, {
metadata: {
reason: reason,
initiated_by: 'customer_service',
timestamp: new Date().toISOString()
}
});
// Log for audit trail
console.log(`Refund ${refund.id} created for charge ${chargeId}`);
console.log(`Reason: ${reason}`);
return {
success: true,
refundId: refund.id,
amount: refund.amount,
currency: refund.currency,
status: refund.status
};
} catch (error) {
return {
success: false,
error: error.message,
code: error.code
};
}
}
// Example usage
createFullRefund();
createRefundWithTracking('chrg_test_123456789', 'Product defective');
import omise
from datetime import datetime
omise.api_secret = 'skey_test_123456789'
def create_full_refund(charge_id):
"""Create a full refund for a charge"""
try:
refund = omise.Charge.retrieve(charge_id).refund(
metadata={
'reason': 'Customer requested refund',
'order_id': 'ORD-12345',
'refund_type': 'product_return'
}
)
print(f"Refund created: {refund.id}")
print(f"Amount refunded: {refund.amount}")
print(f"Status: {refund.status}")
print(f"Created at: {datetime.fromtimestamp(refund.created)}")
return refund
except omise.errors.BaseError as e:
print(f"Refund failed: {str(e)}")
raise
def create_refund_with_tracking(charge_id, reason):
"""Create refund with detailed tracking"""
try:
charge = omise.Charge.retrieve(charge_id)
refund = charge.refund(
metadata={
'reason': reason,
'initiated_by': 'customer_service',
'timestamp': datetime.now().isoformat()
}
)
# Log for audit trail
print(f"Refund {refund.id} created for charge {charge_id}")
print(f"Reason: {reason}")
return {
'success': True,
'refund_id': refund.id,
'amount': refund.amount,
'currency': refund.currency,
'status': refund.status
}
except omise.errors.BaseError as e:
return {
'success': False,
'error': str(e),
'code': e.code if hasattr(e, 'code') else None
}
# Example usage
create_full_refund('chrg_test_123456789')
create_refund_with_tracking('chrg_test_123456789', 'Product defective')
require 'omise'
Omise.api_key = 'skey_test_123456789'
# Create a full refund
def create_full_refund(charge_id)
begin
charge = Omise::Charge.retrieve(charge_id)
refund = charge.refund(
metadata: {
reason: 'Customer requested refund',
order_id: 'ORD-12345',
refund_type: 'product_return'
}
)
puts "Refund created: #{refund.id}"
puts "Amount refunded: #{refund.amount}"
puts "Status: #{refund.status}"
puts "Created at: #{Time.at(refund.created)}"
refund
rescue Omise::Error => e
puts "Refund failed: #{e.message}"
raise
end
end
# Create refund with tracking
def create_refund_with_tracking(charge_id, reason)
begin
charge = Omise::Charge.retrieve(charge_id)
refund = charge.refund(
metadata: {
reason: reason,
initiated_by: 'customer_service',
timestamp: Time.now.iso8601
}
)
# Log for audit trail
puts "Refund #{refund.id} created for charge #{charge_id}"
puts "Reason: #{reason}"
{
success: true,
refund_id: refund.id,
amount: refund.amount,
currency: refund.currency,
status: refund.status
}
rescue Omise::Error => e
{
success: false,
error: e.message,
code: e.code
}
end
end
# Example usage
create_full_refund('chrg_test_123456789')
create_refund_with_tracking('chrg_test_123456789', 'Product defective')
<?php
require_once 'vendor/autoload.php';
define('OMISE_SECRET_KEY', 'skey_test_123456789');
// Create a full refund
function createFullRefund($chargeId) {
try {
$charge = OmiseCharge::retrieve($chargeId);
$refund = $charge->refund([
'metadata' => [
'reason' => 'Customer requested refund',
'order_id' => 'ORD-12345',
'refund_type' => 'product_return'
]
]);
echo "Refund created: {$refund['id']}\n";
echo "Amount refunded: {$refund['amount']}\n";
echo "Status: {$refund['status']}\n";
echo "Created at: " . date('Y-m-d H:i:s', $refund['created']) . "\n";
return $refund;
} catch (Exception $e) {
echo "Refund failed: {$e->getMessage()}\n";
throw $e;
}
}
// Create refund with tracking
function createRefundWithTracking($chargeId, $reason) {
try {
$charge = OmiseCharge::retrieve($chargeId);
$refund = $charge->refund([
'metadata' => [
'reason' => $reason,
'initiated_by' => 'customer_service',
'timestamp' => date('c')
]
]);
// Log for audit trail
echo "Refund {$refund['id']} created for charge {$chargeId}\n";
echo "Reason: {$reason}\n";
return [
'success' => true,
'refund_id' => $refund['id'],
'amount' => $refund['amount'],
'currency' => $refund['currency'],
'status' => $refund['status']
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'code' => $e->getCode()
];
}
}
// Example usage
createFullRefund('chrg_test_123456789');
createRefundWithTracking('chrg_test_123456789', 'Product defective');
?>
package main
import (
"fmt"
"log"
"time"
"github.com/omise/omise-go"
"github.com/omise/omise-go/operations"
)
const secretKey = "skey_test_123456789"
// CreateFullRefund creates a full refund for a charge
func CreateFullRefund(chargeID string) (*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,
Metadata: map[string]interface{}{
"reason": "Customer requested refund",
"order_id": "ORD-12345",
"refund_type": "product_return",
},
})
if err != nil {
return nil, fmt.Errorf("refund failed: %w", err)
}
fmt.Printf("Refund created: %s\n", refund.ID)
fmt.Printf("Amount refunded: %d\n", refund.Amount)
fmt.Printf("Status: %s\n", refund.Status)
fmt.Printf("Created at: %s\n", refund.Created.Format(time.RFC3339))
return refund, nil
}
// RefundResult represents the result of a refund operation
type RefundResult struct {
Success bool `json:"success"`
RefundID string `json:"refund_id,omitempty"`
Amount int64 `json:"amount,omitempty"`
Currency string `json:"currency,omitempty"`
Status string `json:"status,omitempty"`
Error string `json:"error,omitempty"`
Code string `json:"code,omitempty"`
}
// CreateRefundWithTracking creates a refund with detailed tracking
func CreateRefundWithTracking(chargeID, reason string) *RefundResult {
client, err := omise.NewClient(secretKey, "")
if err != nil {
return &RefundResult{
Success: false,
Error: err.Error(),
}
}
refund := &omise.Refund{}
err = client.Do(refund, &operations.CreateRefund{
ChargeID: chargeID,
Metadata: map[string]interface{}{
"reason": reason,
"initiated_by": "customer_service",
"timestamp": time.Now().Format(time.RFC3339),
},
})
if err != nil {
return &RefundResult{
Success: false,
Error: err.Error(),
}
}
// Log for audit trail
fmt.Printf("Refund %s created for charge %s\n", refund.ID, chargeID)
fmt.Printf("Reason: %s\n", reason)
return &RefundResult{
Success: true,
RefundID: refund.ID,
Amount: refund.Amount,
Currency: refund.Currency,
Status: string(refund.Status),
}
}
func main() {
// Example usage
refund, err := CreateFullRefund("chrg_test_123456789")
if err != nil {
log.Fatalf("Failed to create refund: %v", err)
}
result := CreateRefundWithTracking("chrg_test_123456789", "Product defective")
if !result.Success {
log.Printf("Refund failed: %s", result.Error)
}
}
API Responseโ
A successful refund creation returns:
{
"object": "refund",
"id": "rfnd_test_5xyz789abc",
"location": "/charges/chrg_test_123456789/refunds/rfnd_test_5xyz789abc",
"amount": 100000,
"currency": "thb",
"charge": "chrg_test_123456789",
"transaction": "trxn_test_5xyz789abc",
"created": "2024-01-15T10:30:00Z",
"status": "pending",
"metadata": {
"reason": "Customer requested refund",
"order_id": "ORD-12345",
"refund_type": "product_return"
},
"funding_amount": 100000,
"funding_currency": "thb"
}
Creating Refunds via Dashboardโ
Step-by-Step Processโ
-
Navigate to Transactions
- Log in to your Omise Dashboard
- Go to "Transactions" > "Charges"
-
Find the Charge
- Use the search bar to find the charge ID
- Or filter by date, amount, or status
- Click on the charge to view details
-
Initiate Refund
- Click the "Refund" button
- The full charge amount will be pre-filled
- You can modify the amount for partial refunds
-
Add Refund Details (Optional)
- Enter a reason for the refund
- Add internal notes
- Select refund category (return, cancellation, etc.)
-
Confirm Refund
- Review the refund details
- Click "Confirm Refund"
- The refund will be processed immediately
-
Track Status
- View refund status on the charge details page
- Check the "Refunds" section for all refund attempts
- Monitor webhook events for status updates
Refund Status Lifecycleโ
Understanding refund statuses:
Pendingโ
- Refund has been created and is being processed
- Typically takes a few minutes
- Customer has not yet received funds
Successfulโ
- Refund has been completed
- Funds have been sent to the payment provider
- Customer should receive funds within 5-10 business days
Failedโ
- Refund could not be processed
- Common reasons: insufficient balance, invalid card, expired card
- Contact support if issues persist
Status Flow Diagramโ
Create Refund โ Pending โ Successful โ Funds Returned to Customer
โ
Failed โ Contact Support
Common Use Casesโ
1. Product Return Flowโ
async function handleProductReturn(orderId, chargeId) {
// Update order status
await updateOrderStatus(orderId, 'return_requested');
// Create refund
const refund = await omise.charges.createRefund(chargeId, {
metadata: {
reason: 'product_return',
order_id: orderId,
return_tracking: 'TRACK123456',
warehouse_received: 'pending'
}
});
// Send customer notification
await sendCustomerEmail({
template: 'refund_initiated',
orderId: orderId,
refundId: refund.id,
estimatedDays: '5-10 business days'
});
return refund;
}
2. Service Cancellationโ
def handle_subscription_cancellation(subscription_id, charge_id, prorate=True):
"""Handle subscription cancellation with optional proration"""
# Calculate refund amount if prorating
if prorate:
charge = omise.Charge.retrieve(charge_id)
days_used = calculate_days_used(subscription_id)
days_in_period = 30
refund_amount = int(charge.amount * (days_in_period - days_used) / days_in_period)
else:
refund_amount = None # Full refund
# Create refund
refund = omise.Charge.retrieve(charge_id).refund(
amount=refund_amount,
metadata={
'reason': 'subscription_cancellation',
'subscription_id': subscription_id,
'prorated': prorate
}
)
# Cancel subscription
cancel_subscription(subscription_id)
return refund
3. Duplicate Charge Handlingโ
def handle_duplicate_charge(original_charge_id, duplicate_charge_id)
begin
# Verify it's actually a duplicate
original = Omise::Charge.retrieve(original_charge_id)
duplicate = Omise::Charge.retrieve(duplicate_charge_id)
if original.amount == duplicate.amount &&
original.customer == duplicate.customer &&
(duplicate.created - original.created) < 300 # Within 5 minutes
# Refund the duplicate
refund = duplicate.refund(
metadata: {
reason: 'duplicate_charge',
original_charge: original_charge_id,
auto_detected: true
}
)
# Log the incident
log_duplicate_charge(original_charge_id, duplicate_charge_id)
# Notify customer
send_apology_email(duplicate.customer)
return { success: true, refund: refund }
else
return { success: false, reason: 'not_duplicate' }
end
rescue Omise::Error => e
return { success: false, error: e.message }
end
end
Best Practicesโ
1. Always Add Metadataโ
Include context for every refund:
const refund = await omise.charges.createRefund(chargeId, {
metadata: {
reason: 'customer_request',
category: 'product_return',
requested_by: 'customer_service_rep_42',
ticket_id: 'SUPPORT-12345',
timestamp: new Date().toISOString()
}
});
2. Implement Proper Error Handlingโ
def safe_refund(charge_id, reason):
"""Create refund with comprehensive error handling"""
try:
refund = omise.Charge.retrieve(charge_id).refund(
metadata={'reason': reason}
)
return {'success': True, 'refund_id': refund.id}
except omise.errors.InvalidRequestError as e:
# Charge already refunded or doesn't exist
return {'success': False, 'error': 'invalid_request', 'message': str(e)}
except omise.errors.AuthenticationError as e:
# API key issues
return {'success': False, 'error': 'authentication', 'message': str(e)}
except omise.errors.BaseError as e:
# Other Omise errors
return {'success': False, 'error': 'api_error', 'message': str(e)}
except Exception as e:
# Unexpected errors
return {'success': False, 'error': 'unknown', 'message': str(e)}
3. Track Refund Statusโ
def track_refund_status(charge_id, refund_id)
charge = Omise::Charge.retrieve(charge_id)
refund = charge.refunds.retrieve(refund_id)
case refund.status
when 'pending'
puts "Refund is being processed"
when 'successful'
puts "Refund completed successfully"
notify_customer_refund_complete(charge.customer)
when 'failed'
puts "Refund failed: #{refund.failure_message}"
escalate_to_support(refund_id)
end
refund
end
4. Customer Communicationโ
async function refundWithCustomerNotification(chargeId, reason) {
const refund = await omise.charges.createRefund(chargeId, {
metadata: { reason }
});
// Get customer details
const charge = await omise.charges.retrieve(chargeId);
// Send email notification
await sendEmail({
to: charge.customer_email,
subject: 'Refund Processed',
template: 'refund-notification',
data: {
amount: formatCurrency(refund.amount, refund.currency),
refundId: refund.id,
reason: reason,
estimatedDays: '5-10 business days',
supportEmail: 'support@yourcompany.com'
}
});
return refund;
}
5. Audit Trailโ
Maintain detailed logs:
function createRefundWithAudit($chargeId, $reason, $userId) {
// Create refund
$refund = OmiseCharge::retrieve($chargeId)->refund([
'metadata' => [
'reason' => $reason,
'user_id' => $userId,
'timestamp' => date('c')
]
]);
// Log to audit trail
logAuditEvent([
'event' => 'refund_created',
'refund_id' => $refund['id'],
'charge_id' => $chargeId,
'amount' => $refund['amount'],
'currency' => $refund['currency'],
'reason' => $reason,
'user_id' => $userId,
'ip_address' => $_SERVER['REMOTE_ADDR'],
'timestamp' => time()
]);
return $refund;
}
Error Handlingโ
Common Errorsโ
Already Refundedโ
// Error: charge_already_refunded
{
"object": "error",
"location": "https://www.omise.co/api-errors#charge-already-refunded",
"code": "charge_already_refunded",
"message": "charge was already fully refunded"
}
// Handle this error
try {
const refund = await omise.charges.createRefund(chargeId);
} catch (error) {
if (error.code === 'charge_already_refunded') {
console.log('This charge has already been refunded');
// Check existing refunds
const charge = await omise.charges.retrieve(chargeId);
console.log('Existing refunds:', charge.refunds.data);
}
}
Insufficient Balanceโ
# Error: insufficient_fund
try:
refund = omise.Charge.retrieve(charge_id).refund()
except omise.errors.InvalidRequestError as e:
if 'insufficient' in str(e).lower():
print("Insufficient balance to process refund")
# Contact finance team or wait for settlement
notify_finance_team(charge_id)
Invalid Chargeโ
# Error: invalid_charge
begin
refund = Omise::Charge.retrieve(charge_id).refund
rescue Omise::InvalidRequestError => e
if e.message.include?('not found')
puts "Charge not found or invalid"
# Verify charge ID
end
end
Error Recovery Strategiesโ
async function refundWithRetry(chargeId, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const refund = await omise.charges.createRefund(chargeId);
return { success: true, refund };
} catch (error) {
attempt++;
if (error.code === 'charge_already_refunded') {
// Don't retry
return { success: false, reason: 'already_refunded' };
}
if (error.code === 'insufficient_fund' && attempt < maxRetries) {
// Wait and retry
console.log(`Attempt ${attempt} failed, retrying in 5 minutes...`);
await sleep(300000); // 5 minutes
continue;
}
if (attempt === maxRetries) {
return { success: false, error: error.message };
}
}
}
}
Webhook Integrationโ
Handle refund webhook events:
app.post('/webhooks/omise', async (req, res) => {
const event = req.body;
if (event.key === 'refund.create') {
const refund = event.data;
console.log(`Refund created: ${refund.id}`);
// Update database
await updateRefundStatus(refund.charge, refund.id, 'pending');
} else if (event.key === 'refund.success') {
const refund = event.data;
console.log(`Refund successful: ${refund.id}`);
// Update order status
await updateOrderStatus(refund.metadata.order_id, 'refunded');
// Notify customer
await notifyCustomer(refund.charge, 'refund_complete');
} else if (event.key === 'refund.fail') {
const refund = event.data;
console.log(`Refund failed: ${refund.id}`);
// Escalate to support
await createSupportTicket({
type: 'refund_failed',
refundId: refund.id,
chargeId: refund.charge
});
}
res.sendStatus(200);
});
Testing Refundsโ
Test Modeโ
Use test API keys to test refund flows:
// Test refund creation
const testRefund = await omise.charges.createRefund('chrg_test_123', {
metadata: { test: true }
});
// Verify refund details
console.assert(testRefund.status === 'pending');
console.assert(testRefund.amount === testCharge.amount);
Test Scenariosโ
def test_refund_scenarios():
"""Test various refund scenarios"""
# Test 1: Full refund
charge1 = create_test_charge(10000)
refund1 = omise.Charge.retrieve(charge1.id).refund()
assert refund1.amount == 10000
# Test 2: Refund with metadata
charge2 = create_test_charge(20000)
refund2 = omise.Charge.retrieve(charge2.id).refund(
metadata={'reason': 'test'}
)
assert refund2.metadata['reason'] == 'test'
# Test 3: Already refunded error
try:
omise.Charge.retrieve(charge1.id).refund()
assert False, "Should have raised error"
except omise.errors.InvalidRequestError as e:
assert 'already' in str(e).lower()
print("All tests passed!")
FAQโ
Can I refund a charge immediately after it's created?โ
Yes, you can refund a charge as soon as it has a successful status. However, if the charge is still in pending status, you should wait for it to complete before initiating a refund.
How long does it take for customers to receive refunds?โ
Refund timing depends on the payment method and card issuer:
- Credit cards: 5-10 business days typically
- Debit cards: 5-10 business days typically
- Alternative payment methods: Varies by method
The refund is processed immediately on Omise's side, but the actual credit to the customer's account depends on their bank or card issuer.
Are refund fees returned?โ
No, the original transaction fees charged by Omise are not refunded. You will still be charged the transaction fee for the original charge, even if you refund it. This is standard practice across payment processors.
Can I refund to a different card?โ
No, refunds must be returned to the original payment method used for the charge. This is a security requirement and cannot be bypassed.
What happens if the customer's card has expired?โ
If the original card has expired, the refund will typically still be processed and the customer's bank will credit their account. However, in some cases, the refund may fail. Contact support if you encounter issues.
Can I cancel a pending refund?โ
No, once a refund is initiated, it cannot be canceled. Refunds are processed immediately and cannot be reversed. If you refund by mistake, you would need to charge the customer again (with their permission).
What if I don't have sufficient balance for a refund?โ
Refunds are deducted from your available balance. If you have insufficient balance, the refund will fail. You'll need to wait for pending settlements to clear or contact Omise support for assistance.
How many times can I refund a charge?โ
You can create multiple partial refunds for a charge until the full amount has been refunded. Once fully refunded, no additional refunds can be created for that charge.
Do refunds trigger webhooks?โ
Yes, refunds trigger several webhook events:
refund.create: When a refund is createdrefund.success: When the refund is successfulrefund.fail: When the refund fails
Can I refund charges from months ago?โ
Yes, you can refund charges from any time period as long as:
- The charge was successful
- The charge hasn't already been refunded
- You have sufficient balance
However, very old refunds may face higher decline rates from card issuers.
Related Resourcesโ
- Partial Refunds - Create partial refunds for flexible refund amounts
- Refund Limitations - Understand refund restrictions and limits
- Transaction History - View and track all refunds
- Webhooks Guide - Handle refund events
- Error Handling - Handle refund errors properly
Next Stepsโ
- Learn about partial refunds for more flexible refund options
- Understand refund limitations and restrictions
- Set up webhook listeners for refund status updates
- Explore transaction history to track refunds
- Review reconciliation for matching refunds to settlements