Skip to main content

ShopeePay

Accept payments from ShopeePay, the digital wallet integrated into Southeast Asia's leading e-commerce platform with 150+ million active users.

Payment Flow​

ShopeePay Payment Flow

The image shows the customer journey: choosing the wallet, scanning the QR code, reviewing the summary, and confirming payment.

Overview​

ShopeePay is the payment wallet within the Shopee app ecosystem, one of Southeast Asia's largest e-commerce platforms. Users can pay with their ShopeePay balance for instant, secure transactions both within and outside the Shopee platform.

Key Features:

  • ✅ Massive reach - 150+ million active Shopee users
  • ✅ Instant confirmation - Real-time payment processing
  • ✅ Mobile-first - Seamless mobile payment experience
  • ✅ Trusted platform - Part of Sea Group (NYSE: SE)
  • ✅ Regional coverage - Multi-country availability
  • ✅ Cashback rewards - Users earn Shopee Coins

Supported Regions​

RegionCurrencyMin AmountMax AmountDaily Limit
ThailandTHBā¸ŋ20.00ā¸ŋ50,000ā¸ŋ200,000*
MalaysiaMYRRM1.00RM1,500RM5,000*

*Daily limits vary based on customer's wallet verification level

Transaction Limits​

Thailand (THB)​

Verification LevelPer TransactionDaily LimitMonthly Limit
Basic (Phone only)ā¸ŋ50,000ā¸ŋ50,000ā¸ŋ200,000
Plus (ID verified)ā¸ŋ50,000ā¸ŋ200,000ā¸ŋ500,000

Malaysia (MYR)​

Verification LevelPer TransactionDaily LimitMonthly Limit
Basic (Phone only)RM1,500RM1,500RM3,000
Plus (ID verified)RM1,500RM5,000RM10,000

How It Works​

Customer Experience:

  1. Customer selects ShopeePay at checkout
  2. Redirected to ShopeePay authorization page
  3. Opens Shopee app (deep link)
  4. Reviews payment details
  5. Authenticates with PIN or biometric
  6. Confirms payment
  7. Returns to merchant site

Typical completion time: 1-2 minutes

Implementation​

Step 1: Create ShopeePay Source​

curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=shopeepay" \
-d "amount=25000" \
-d "currency=THB"

Response:

{
"object": "source",
"id": "src_test_5rt6s9vah5lkvi1rh9c",
"type": "shopeepay",
"flow": "redirect",
"amount": 25000,
"currency": "THB"
}

Step 2: Create Charge​

curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=25000" \
-d "currency=THB" \
-d "source=src_test_5rt6s9vah5lkvi1rh9c" \
-d "return_uri=https://yourdomain.com/payment/callback"

Step 3: Redirect Customer​

app.post('/checkout/shopeepay', async (req, res) => {
try {
const { amount, currency, order_id } = req.body;

// Validate currency
if (!['THB', 'MYR'].includes(currency)) {
return res.status(400).json({
error: 'ShopeePay supports THB and MYR only'
});
}

// Validate amount by currency
const limits = {
THB: { min: 2000, max: 5000000 },
MYR: { min: 100, max: 150000 }
};

const { min, max } = limits[currency];
if (amount < min || amount > max) {
return res.status(400).json({
error: `Amount must be between ${min} and ${max} ${currency}`
});
}

// Create source
const source = await omise.sources.create({
type: 'shopeepay',
amount: amount,
currency: currency
});

// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: currency,
source: source.id,
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id
}
});

// Redirect to ShopeePay
res.redirect(charge.authorize_uri);

} catch (error) {
console.error('ShopeePay error:', error);
res.status(500).json({ error: error.message });
}
});

Step 4: Handle Return​

app.get('/payment/callback', async (req, res) => {
try {
const chargeId = req.query.charge_id;
const charge = await omise.charges.retrieve(chargeId);

if (charge.status === 'successful') {
await processOrder(charge.metadata.order_id);
res.redirect('/payment-success');
} else if (charge.status === 'failed') {
res.redirect('/payment-failed?reason=' + charge.failure_message);
} else {
res.redirect('/payment-pending');
}
} catch (error) {
res.redirect('/payment-error');
}
});

Step 5: Handle Webhook​

app.post('/webhooks/omise', (req, res) => {
const event = req.body;

if (event.key === 'charge.complete' && event.data.source.type === 'shopeepay') {
const charge = event.data;

if (charge.status === 'successful') {
processOrder(charge.metadata.order_id);
} else if (charge.status === 'failed') {
handleFailedPayment(charge.metadata.order_id);
}
}

res.sendStatus(200);
});

Complete Implementation Example​

// Express.js server
const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});

const app = express();
app.use(express.json());

// Amount limits by currency
const LIMITS = {
THB: { min: 2000, max: 5000000 },
MYR: { min: 100, max: 150000 }
};

app.post('/checkout/shopeepay', async (req, res) => {
try {
const { amount, currency, order_id } = req.body;

// Validate currency
if (!['THB', 'MYR'].includes(currency)) {
return res.status(400).json({
error: 'ShopeePay only supports THB and MYR'
});
}

// Validate amount
const { min, max } = LIMITS[currency];
if (amount < min || amount > max) {
return res.status(400).json({
error: `Amount must be between ${min} and ${max} ${currency}`
});
}

// Create source
const source = await omise.sources.create({
type: 'shopeepay',
amount: amount,
currency: currency
});

// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: currency,
source: source.id,
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id,
payment_method: 'shopeepay'
}
});

// Return authorization URL
res.json({
authorize_uri: charge.authorize_uri,
charge_id: charge.id
});

} catch (error) {
console.error('ShopeePay error:', error);
res.status(500).json({ error: error.message });
}
});

// Callback handler
app.get('/payment/callback', async (req, res) => {
try {
const chargeId = req.query.charge_id;
const charge = await omise.charges.retrieve(chargeId);

if (charge.status === 'successful') {
res.redirect(`/order-success?order=${charge.metadata.order_id}`);
} else {
res.redirect(`/payment-failed?charge=${chargeId}`);
}
} catch (error) {
res.redirect('/payment-error');
}
});

// Webhook handler
app.post('/webhooks/omise', (req, res) => {
const event = req.body;

if (event.key === 'charge.complete') {
const charge = event.data;

if (charge.source.type === 'shopeepay') {
if (charge.status === 'successful') {
updateOrderStatus(charge.metadata.order_id, 'paid');
sendConfirmation(charge.metadata.customer_email);
} else {
updateOrderStatus(charge.metadata.order_id, 'failed');
}
}
}

res.sendStatus(200);
});

app.listen(3000);

Void and Refund Support​

Voiding Charges​

ShopeePay supports voiding within 24 hours:

// Void immediately (full amount)
const refund = await omise.charges.refund('chrg_test_...', {
amount: 25000
});

if (refund.voided) {
console.log('Charge was voided (within 24 hours)');
}

Refunds​

Full refunds only within 30 days:

// Full refund only
const refund = await omise.charges.refund('chrg_test_...', {
amount: 25000 // Must be full amount
});
No Partial Refunds

ShopeePay does NOT support partial refunds. Only full refunds are allowed within 30 days.

Common Issues & Troubleshooting​

Issue: Customer doesn't have Shopee app​

Cause: Customer selected ShopeePay but doesn't have Shopee app

Solution:

function checkShopeeApp() {
if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
alert('ShopeePay requires the Shopee mobile app. Please use a mobile device.');
return false;
}
return true;
}

Issue: Insufficient balance​

Error: Payment declined

Solution:

if (charge.failure_code === 'insufficient_balance') {
showMessage('Insufficient ShopeePay balance. Please top up in the Shopee app.');
offerAlternativePayment();
}

Issue: Payment timeout​

Solution:

const TIMEOUT = 15 * 60 * 1000; // 15 minutes

setTimeout(() => {
if (!paymentConfirmed) {
showTimeoutMessage();
allowRetry();
}
}, TIMEOUT);

Best Practices​

1. Display Instructions​

<div class="shopeepay-instructions">
<h3>Pay with ShopeePay</h3>
<ol>
<li>Ensure you have the Shopee app installed</li>
<li>Check your ShopeePay balance is sufficient</li>
<li>You'll be redirected to the Shopee app</li>
<li>Confirm payment with PIN or biometric</li>
</ol>
<p>Top up ShopeePay balance in the Shopee app if needed.</p>
</div>

2. Validate Amount​

function validateAmount(amount, currency) {
const limits = {
THB: { min: 2000, max: 5000000, symbol: 'ā¸ŋ' },
MYR: { min: 100, max: 150000, symbol: 'RM' }
};

const { min, max, symbol } = limits[currency];

if (amount < min) {
return `Minimum amount is ${symbol}${min / 100}`;
}

if (amount > max) {
return `Maximum amount is ${symbol}${max / 100}`;
}

return null;
}

3. Mobile-First Design​

function isMobileDevice() {
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
}

if (!isMobileDevice()) {
// Hide ShopeePay option on desktop
document.getElementById('shopeepay-option').style.display = 'none';
}

FAQ​

What countries support ShopeePay?

ShopeePay payments through Omise are currently available in Thailand and Malaysia.

Do customers need a Shopee account?

Yes, customers must have the Shopee app installed with an activated ShopeePay wallet.

What are the transaction limits?
  • Thailand: ā¸ŋ20 - ā¸ŋ50,000 per transaction
  • Malaysia: RM1 - RM1,500 per transaction

Daily and monthly limits depend on verification level.

How long does settlement take?

ShopeePay settlements typically occur within 1-3 business days.

Can I refund ShopeePay payments?

Yes, full refunds are supported within 30 days. Partial refunds are not available. Voiding is possible within 24 hours.

What if customer has insufficient balance?

Payment will be declined. Customer can top up ShopeePay balance via credit/debit card, bank transfer, or at 7-Eleven stores (Thailand).

Does ShopeePay work on desktop?

ShopeePay requires the Shopee mobile app, so it's mobile-only. Desktop users should see alternative payment methods.

Testing​

Test Mode​

ShopeePay can be tested using your test API keys. In test mode:

Test Credentials:

  • Use test API keys (skey_test_xxx)
  • Test all supported currencies: THB, MYR
  • No actual ShopeePay account required for testing

Test Flow:

  1. Create source and charge with test API keys
  2. Customer redirects to test authorize_uri
  3. Test authorization page simulates ShopeePay flow
  4. Use Omise Dashboard Actions to mark charge as successful/failed
  5. Verify webhook notifications and return_uri callbacks

Testing Implementation:

// Test ShopeePay for both countries
const testConfigs = [
{ country: 'TH', currency: 'THB', amount: 2000 },
{ country: 'MY', currency: 'MYR', amount: 100 }
];

for (const config of testConfigs) {
const source = await omise.sources.create({
type: 'shopeepay',
amount: config.amount,
currency: config.currency
});

const charge = await omise.charges.create({
amount: config.amount,
currency: config.currency,
source: source.id,
return_uri: 'https://example.com/callback'
});

console.log(`Test ${config.country}:`, charge.authorize_uri);
}

Test Scenarios:

  • Successful payment: Complete redirect flow and order processing
  • Failed payment: Test error handling and user messaging
  • Both countries: Test Thailand (THB) and Malaysia (MYR) separately
  • Amount validation: Verify min/max limits per currency
  • Mobile flow: Test deep-linking to Shopee app
  • Timeout handling: Test abandoned payment scenarios
  • Webhook delivery: Verify all webhook events are received

Important Notes:

  • Test mode doesn't connect to real Shopee servers
  • Use dashboard to simulate payment completion
  • Test both THB and MYR currencies before going live
  • Verify webhooks for all charge statuses
  • Test mobile-specific flows (app switching, deep links)

For comprehensive testing guidelines, see the Testing Documentation.

Next Steps​

  1. Create ShopeePay source
  2. Implement redirect flow
  3. Set up webhooks
  4. Test integration
  5. Go live