Skip to main content

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
Check Payment Method Documentation

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 https://api.omise.co/charges/chrg_test_5rt6s9vah5lkvi1rh9c/refunds \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=10025"

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 MethodRefund Window
Credit/Debit Cards365 days
DuitNow180 days
ShopeePay180 days
PayNow6 months
GrabPay3 months
Atome60 days
TrueMoney30 days (voids only)
WeChat Pay90 days
PayPay1 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
  • void flag set to true in response

Refunding occurs after settlement:

  • Money already transferred to merchant
  • Actual refund transaction created
  • Deducted from merchant balance
  • void flag set to false
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 processed
  • successful - Refund completed
  • failed - Refund failed (rare)

Processing Timelineโ€‹

Payment MethodCustomer Receives Refund
Credit Cards5-10 business days (bank dependent)
Debit Cards1-7 business days
QR PaymentsInstant to 1 business day
E-WalletsInstant to 3 business days
Bank Transfers1-3 business days
Bank Processing Times

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.

Learn more about disputes โ†’

How do I test refunds?

In test mode:

  1. Create test charge
  2. Issue refund via API or dashboard
  3. Check refund status
  4. Test webhook handling

All test refunds process immediately for testing purposes.

Next Stepsโ€‹

  1. Implement refund endpoint
  2. Set up refund webhook handling
  3. Test refund workflow
  4. Add customer notifications
  5. Monitor refund analytics