Refunds
Issue full or partial refunds to customers quickly and easily through the Omise API or dashboard.
Overviewโ
Omise allows you to refund payments made through most payment methods. Refunds can be full (entire amount) or partial (portion of amount), depending on the payment method used.
Key Features:
- Instant refund processing
- Full and partial refund support (for supported methods)
- Up to 365-day refund window
- Automatic settlement deduction
- Webhook notifications
Supported Payment Methodsโ
Full & Partial Refund Supportโ
- โ Credit/Debit Cards - Full and partial (up to 15 partials per charge)
- โ DuitNow - Full and partial within 180 days
- โ PayNow - Full and partial within 6 months
- โ GrabPay - Full and partial within 3 months
- โ ShopeePay - Full and partial within 180 days
Full Refund Onlyโ
- โ ๏ธ Installments - Full refunds only (window varies by bank: 30 days to 1 year)
- โ ๏ธ TrueMoney - Full refunds only within 30 days
No Refund Supportโ
- โ PromptPay - NOT refundable
- โ Mobile Banking (Thailand) - NOT refundable
- โ Internet Banking (Thailand) - NOT refundable
- โ Konbini/Pay-easy (Japan) - NOT refundable
- โ Online Direct Debit - NOT refundable
Always check the specific payment method's refund policy before implementation. Refund support and windows vary significantly.
How Refunds Workโ
Creating Refundsโ
Full Refundโ
- cURL
- Node.js
- PHP
- Python
curl https://api.omise.co/charges/chrg_test_5rt6s9vah5lkvi1rh9c/refunds \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=10025"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
// Full refund
const refund = await omise.charges.refund('chrg_test_...', {
amount: 10025
});
console.log(refund.status); // 'pending'
<?php
$charge = OmiseCharge::retrieve('chrg_test_5rt6s9vah5lkvi1rh9c');
$refund = $charge->refund(array(
'amount' => 10025
));
echo $refund['status']; // 'pending'
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
charge = omise.Charge.retrieve('chrg_test_5rt6s9vah5lkvi1rh9c')
refund = charge.refund(amount=10025)
print(refund.status) # 'pending'
Partial Refundโ
// Refund part of the charge (e.g., THB 50 of THB 100.25)
const partialRefund = await omise.charges.refund('chrg_test_...', {
amount: 5000 // THB 50.00 (in smallest unit)
});
// Customer receives THB 50 back
// Merchant keeps THB 50.25
Multiple Partial Refundsโ
// First partial refund (THB 30)
await omise.charges.refund(chargeId, { amount: 3000 });
// Second partial refund (THB 20)
await omise.charges.refund(chargeId, { amount: 2000 });
// Third partial refund (THB 50)
await omise.charges.refund(chargeId, { amount: 5000 });
// Total refunded: THB 100
// Maximum: 15 partial refunds per charge
Refund Limitationsโ
Time Windowsโ
| Payment Method | Refund Window |
|---|---|
| Credit/Debit Cards | 365 days |
| DuitNow | 180 days |
| ShopeePay | 180 days |
| PayNow | 6 months |
| GrabPay | 3 months |
| Atome | 60 days |
| TrueMoney | 30 days (voids only) |
| WeChat Pay | 90 days |
| PayPay | 1 year |
Partial Refund Limitsโ
- Maximum per charge: 15 partial refunds
- Minimum amount: Varies by payment method
- Total cannot exceed: Original charge amount
Voiding vs Refundingโ
Voiding occurs when refund is processed before settlement:
- No actual transfer happened yet
- Charge is canceled
- Faster processing
voidflag set totruein response
Refunding occurs after settlement:
- Money already transferred to merchant
- Actual refund transaction created
- Deducted from merchant balance
voidflag set tofalse
const refund = await omise.charges.refund(chargeId, { amount: 10000 });
if (refund.voided) {
console.log('Charge was voided (pre-settlement)');
} else {
console.log('Refund was processed (post-settlement)');
}
Refund Status & Timelineโ
Refund Statusesโ
pending- Refund created, being processedsuccessful- Refund completedfailed- Refund failed (rare)
Processing Timelineโ
| Payment Method | Customer Receives Refund |
|---|---|
| Credit Cards | 5-10 business days (bank dependent) |
| Debit Cards | 1-7 business days |
| QR Payments | Instant to 1 business day |
| E-Wallets | Instant to 3 business days |
| Bank Transfers | 1-3 business days |
Refund timing depends on the customer's bank. Omise processes refunds immediately, but banks may take several days to credit customer accounts.
Checking Refund Statusโ
Retrieve Specific Refundโ
const refund = await omise.charges.retrieveRefund(
'chrg_test_...',
'rfnd_test_...'
);
console.log(refund.status);
console.log(refund.amount);
console.log(refund.created_at);
List All Refunds for Chargeโ
const refunds = await omise.charges.listRefunds('chrg_test_...');
refunds.data.forEach(refund => {
console.log(`Refund ${refund.id}: ${refund.amount/100} ${refund.currency}`);
});
Check Charge Refund Statusโ
const charge = await omise.charges.retrieve('chrg_test_...');
console.log('Refundable:', charge.refundable);
console.log('Refunded amount:', charge.refunded_amount);
console.log('Net amount:', charge.net - charge.refunded_amount);
Handling Refund Webhooksโ
app.post('/webhooks/omise', (req, res) => {
const event = req.body;
if (event.key === 'refund.create') {
const refund = event.data;
const chargeId = refund.charge;
// Update order status
updateOrderStatus(chargeId, 'refunded');
// Notify customer
sendRefundConfirmation(refund);
// Update inventory if needed
restockItems(chargeId);
}
res.sendStatus(200);
});
Common Use Casesโ
1. Order Cancellationโ
async function cancelOrder(orderId) {
const order = await getOrder(orderId);
if (order.omise_charge_id && order.status === 'paid') {
// Issue full refund
const refund = await omise.charges.refund(order.omise_charge_id, {
amount: order.amount,
metadata: {
order_id: orderId,
reason: 'customer_request'
}
});
// Update order status
await updateOrder(orderId, {
status: 'refunded',
refund_id: refund.id
});
// Notify customer
await sendCancellationEmail(order.customer_email, refund);
}
}
2. Partial Refund for Returnsโ
async function processPartialReturn(orderId, returnedItems) {
const order = await getOrder(orderId);
let refundAmount = 0;
// Calculate refund amount
returnedItems.forEach(item => {
refundAmount += item.price * item.quantity;
});
// Issue partial refund
const refund = await omise.charges.refund(order.omise_charge_id, {
amount: refundAmount,
metadata: {
order_id: orderId,
reason: 'partial_return',
items: JSON.stringify(returnedItems)
}
});
return refund;
}
3. Overpayment Correctionโ
async function correctOverpayment(chargeId, correctAmount) {
const charge = await omise.charges.retrieve(chargeId);
const overpayment = charge.amount - correctAmount;
if (overpayment > 0) {
// Refund the difference
const refund = await omise.charges.refund(chargeId, {
amount: overpayment,
metadata: {
reason: 'overpayment_correction'
}
});
return refund;
}
}
Best Practicesโ
1. Always Check Refundabilityโ
const charge = await omise.charges.retrieve(chargeId);
if (!charge.refundable) {
throw new Error('This charge cannot be refunded');
}
if (charge.refunded_amount >= charge.amount) {
throw new Error('Charge already fully refunded');
}
// Check remaining refundable amount
const remainingAmount = charge.amount - charge.refunded_amount;
console.log(`Can refund up to: ${remainingAmount/100}`);
2. Use Metadata for Trackingโ
const refund = await omise.charges.refund(chargeId, {
amount: 5000,
metadata: {
order_id: 'ORD-12345',
reason: 'defective_product',
initiated_by: 'customer_service',
ticket_id: 'TICKET-789',
notes: 'Customer received damaged item'
}
});
3. Implement Idempotencyโ
async function safeRefund(chargeId, amount, idempotencyKey) {
try {
const refund = await omise.charges.refund(chargeId, {
amount: amount
}, {
headers: {
'Idempotency-Key': idempotencyKey
}
});
return refund;
} catch (error) {
if (error.code === 'already_refunded') {
// Refund already exists
return await findExistingRefund(chargeId, amount);
}
throw error;
}
}
4. Notify Customersโ
async function issueRefundWithNotification(chargeId, amount, reason) {
const charge = await omise.charges.retrieve(chargeId);
// Create refund
const refund = await omise.charges.refund(chargeId, { amount });
// Send email notification
await sendEmail({
to: charge.metadata.customer_email,
subject: 'Refund Processed',
body: `
Your refund of ${amount/100} ${charge.currency} has been processed.
Reason: ${reason}
You should see the credit in your account within 5-10 business days.
Refund ID: ${refund.id}
`
});
return refund;
}
5. Handle Errors Gracefullyโ
async function handleRefund(chargeId, amount) {
try {
const refund = await omise.charges.refund(chargeId, { amount });
return { success: true, refund };
} catch (error) {
console.error('Refund error:', error);
if (error.code === 'invalid_charge') {
return { success: false, error: 'Charge not found' };
}
if (error.code === 'charge_not_refundable') {
return { success: false, error: 'This payment method does not support refunds' };
}
if (error.code === 'insufficient_balance') {
return { success: false, error: 'Insufficient balance for refund' };
}
return { success: false, error: 'Refund failed. Please try again.' };
}
}
Common Issues & Troubleshootingโ
Issue: "Charge not refundable"โ
Causes:
- Payment method doesn't support refunds (e.g., PromptPay, Mobile Banking)
- Refund window expired (> 365 days for cards)
- Charge was unsuccessful
- Already fully refunded
Solution:
const charge = await omise.charges.retrieve(chargeId);
if (!charge.paid) {
console.log('Charge was not successful');
}
if (charge.refunded_amount === charge.amount) {
console.log('Already fully refunded');
}
if (!charge.refundable) {
console.log('Payment method does not support refunds');
// Offer alternative: manual bank transfer, store credit, etc.
}
Issue: "Insufficient balance"โ
Cause: Not enough funds in merchant account
Solution:
- Wait for upcoming settlements
- Manually add funds to account
- Process refund after sufficient balance available
Issue: Partial refund failsโ
Causes:
- Payment method doesn't support partial refunds
- Exceeded 15 partial refund limit
- Amount exceeds remaining refundable amount
Solution:
const charge = await omise.charges.retrieve(chargeId);
const refunds = await omise.charges.listRefunds(chargeId);
if (refunds.total >= 15) {
console.log('Maximum 15 partial refunds reached');
}
const remainingAmount = charge.amount - charge.refunded_amount;
if (requestedAmount > remainingAmount) {
console.log(`Can only refund up to ${remainingAmount/100}`);
}
FAQโ
How long do refunds take to appear in customer accounts?
Refund processing time varies by payment method:
- Credit cards: 5-10 business days (depends on customer's bank)
- Debit cards: 1-7 business days
- E-wallets: Instant to 3 business days
- QR payments: Instant to 1 business day
Omise processes refunds immediately, but banks control when customers see the credit.
Can I refund more than the original charge amount?
No, you cannot refund more than the original charge amount. The total of all refunds cannot exceed the original charge.
What happens to fees when I refund?
Transaction fees are NOT refunded. When you issue a refund:
- Customer receives full refund amount
- Merchant absorbs the transaction fee
- Refund is deducted from transferable balance
Example: THB 100 charge with 3.65% fee = THB 3.65 fee
- Merchant initially receives: THB 96.35
- Full refund issued: THB 100 deducted from balance
- Merchant net loss: THB 103.65 (refund + original fee)
Can I refund charges from test mode?
Yes, you can refund test charges for testing purposes. Test refunds don't involve real money and help you test your refund workflow.
What's the difference between refunds and disputes?
-
Refund: You voluntarily return money to customer
- You control the process
- You initiate the refund
- Lower cost (original transaction fee only)
-
Dispute/Chargeback: Customer files complaint with bank
- Bank controls the process
- Can result in additional fees ($15-30)
- Counts against your dispute rate
- Can lead to account penalties
Best practice: Issue refunds proactively to avoid disputes.
How do I test refunds?
In test mode:
- Create test charge
- Issue refund via API or dashboard
- Check refund status
- Test webhook handling
All test refunds process immediately for testing purposes.
Related Resourcesโ
- Partial Refunds - Detailed guide
- Refund Limitations - Payment method specifics
- Disputes - Handle chargebacks
- Webhooks - Refund event handling
- Balance API - Check available balance