ShopeePay
Accept payments from ShopeePay, the digital wallet integrated into Southeast Asia's leading e-commerce platform with 150+ million active users.
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â
| Region | Currency | Min Amount | Max Amount | Daily Limit |
|---|---|---|---|---|
| Thailand | THB | ā¸ŋ20.00 | ā¸ŋ50,000 | ā¸ŋ200,000* |
| Malaysia | MYR | RM1.00 | RM1,500 | RM5,000* |
*Daily limits vary based on customer's wallet verification level
Transaction Limitsâ
Thailand (THB)â
| Verification Level | Per Transaction | Daily Limit | Monthly Limit |
|---|---|---|---|
| Basic (Phone only) | ā¸ŋ50,000 | ā¸ŋ50,000 | ā¸ŋ200,000 |
| Plus (ID verified) | ā¸ŋ50,000 | ā¸ŋ200,000 | ā¸ŋ500,000 |
Malaysia (MYR)â
| Verification Level | Per Transaction | Daily Limit | Monthly Limit |
|---|---|---|---|
| Basic (Phone only) | RM1,500 | RM1,500 | RM3,000 |
| Plus (ID verified) | RM1,500 | RM5,000 | RM10,000 |
How It Worksâ
Customer Experience:
- Customer selects ShopeePay at checkout
- Redirected to ShopeePay authorization page
- Opens Shopee app (deep link)
- Reviews payment details
- Authenticates with PIN or biometric
- Confirms payment
- Returns to merchant site
Typical completion time: 1-2 minutes
Implementationâ
Step 1: Create ShopeePay Sourceâ
- cURL
- Node.js
- PHP
- Python
- Ruby
- Go
- Java
- C#
curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=shopeepay" \
-d "amount=25000" \
-d "currency=THB"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
const source = await omise.sources.create({
type: 'shopeepay',
amount: 25000, // THB 250.00
currency: 'THB'
});
<?php
$source = OmiseSource::create(array(
'type' => 'shopeepay',
'amount' => 25000,
'currency' => 'THB'
));
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
source = omise.Source.create(
type='shopeepay',
amount=25000,
currency='THB'
)
require 'omise'
Omise.api_key = 'skey_test_YOUR_SECRET_KEY'
source = Omise::Source.create({
type: 'shopeepay',
amount: 25000,
currency: 'THB'
})
source, err := client.Sources().Create(&operations.CreateSource{
Type: "shopeepay",
Amount: 25000,
Currency: "THB",
})
Source source = client.sources().create(new Source.CreateParams()
.type("shopeepay")
.amount(25000L)
.currency("THB"));
var source = await client.Sources.Create(new CreateSourceRequest
{
Type = "shopeepay",
Amount = 25000,
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
});
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:
- Create source and charge with test API keys
- Customer redirects to test
authorize_uri - Test authorization page simulates ShopeePay flow
- Use Omise Dashboard Actions to mark charge as successful/failed
- 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.
Related Resourcesâ
- Digital Wallets Overview - All wallet options
- GrabPay - Alternative wallet
- TrueMoney - Thailand wallet
- Touch 'n Go - Malaysia wallet
- Refunds - Refund policies