GrabPay
Accept payments from GrabPay, Southeast Asia's leading digital wallet with 187 million users across Singapore, Malaysia, Thailand, Indonesia, Philippines, Vietnam, Myanmar, and Cambodia.
Payment Flow​

The image illustrates the complete redirect checkout process including phone number entry, OTP verification, payment method selection, and final confirmation.
Overview​
GrabPay is the payment wallet integrated into the Grab super-app, which provides ride-hailing, food delivery, and financial services across Southeast Asia. Customers can pay using their GrabPay balance with instant confirmation.
Key Features:
- ✅ Massive reach - 187 million users across 8 countries
- ✅ Fast confirmation - Near real-time payment verification (typically within seconds)
- ✅ Mobile-first - Optimized for smartphone users
- ✅ Trusted brand - Part of Grab ecosystem
- ✅ No checkout friction - One-tap payment in Grab app
- ✅ Regional coverage - Multi-country support
Supported Regions​
| Region | Currency | Min Amount | Max Amount | Daily Limit |
|---|---|---|---|---|
| Singapore | SGD | $0.50 | $500 | $5,000* |
| Malaysia | MYR | RM1.00 | RM1,500 | RM5,000* |
| Thailand | THB | ฿20.00 | ฿50,000 | ฿200,000* |
*Daily limits vary based on customer's wallet verification level
Transaction Limits by Country​
Singapore (SGD)​
| Verification Level | Per Transaction | Daily Limit | Monthly Limit |
|---|---|---|---|
| Basic (Phone only) | $500 | $2,000 | $5,000 |
| Plus (ID verified) | $500 | $5,000 | $30,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 |
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 |
How It Works​
Customer Experience:
- Customer selects GrabPay at checkout
- Redirected to GrabPay authorization page
- Opens Grab app (deep link)
- Reviews payment details
- Confirms with PIN/biometric
- Returns to merchant site
Typical completion time: 1-2 minutes
Implementation​
Step 1: Create GrabPay Source​
- cURL
- Node.js
- PHP
- Python
- Ruby
- Go
- Java
- C#
curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=grabpay" \
-d "amount=10000" \
-d "currency=SGD"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
const source = await omise.sources.create({
type: 'grabpay',
amount: 10000, // SGD 100.00
currency: 'SGD'
});
<?php
$source = OmiseSource::create(array(
'type' => 'grabpay',
'amount' => 10000,
'currency' => 'SGD'
));
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
source = omise.Source.create(
type='grabpay',
amount=10000,
currency='SGD'
)
require 'omise'
Omise.api_key = 'skey_test_YOUR_SECRET_KEY'
source = Omise::Source.create({
type: 'grabpay',
amount: 10000,
currency: 'SGD'
})
source, err := client.Sources().Create(&operations.CreateSource{
Type: "grabpay",
Amount: 10000,
Currency: "SGD",
})
Source source = client.sources().create(new Source.CreateParams()
.type("grabpay")
.amount(10000L)
.currency("SGD"));
var source = await client.Sources.Create(new CreateSourceRequest
{
Type = "grabpay",
Amount = 10000,
Currency = "SGD"
});
Response:
{
"object": "source",
"id": "src_test_5rt6s9vah5lkvi1rh9c",
"type": "grabpay",
"flow": "redirect",
"amount": 10000,
"currency": "SGD"
}
Step 2: Create Charge​
curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=10000" \
-d "currency=SGD" \
-d "source=src_test_5rt6s9vah5lkvi1rh9c" \
-d "return_uri=https://yourdomain.com/payment/callback"
Step 3: Redirect Customer​
app.post('/checkout/grabpay', async (req, res) => {
try {
const { amount, currency, order_id } = req.body;
// Validate currency
const supportedCurrencies = ['SGD', 'MYR', 'THB'];
if (!supportedCurrencies.includes(currency)) {
return res.status(400).json({
error: 'GrabPay supports SGD, MYR, and THB only'
});
}
// Validate amount by currency
const limits = {
SGD: { min: 50, max: 50000 },
MYR: { min: 100, max: 150000 },
THB: { min: 2000, max: 5000000 }
};
if (amount < limits[currency].min || amount > limits[currency].max) {
return res.status(400).json({
error: `Amount out of range for ${currency}`
});
}
// Create source
const source = await omise.sources.create({
type: 'grabpay',
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 GrabPay
res.redirect(charge.authorize_uri);
} catch (error) {
console.error('GrabPay 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') {
// Payment successful
await processOrder(charge.metadata.order_id);
res.redirect('/payment-success');
} else if (charge.status === 'failed') {
// Payment failed
res.redirect('/payment-failed?reason=' + charge.failure_message);
} else {
// Still pending
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 === 'grabpay') {
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 = {
SGD: { min: 50, max: 50000 },
MYR: { min: 100, max: 150000 },
THB: { min: 2000, max: 5000000 }
};
// Checkout page
app.post('/checkout/grabpay', async (req, res) => {
try {
const { amount, currency, order_id } = req.body;
// Validate currency
if (!['SGD', 'MYR', 'THB'].includes(currency)) {
return res.status(400).json({
error: 'GrabPay only supports SGD, MYR, and THB'
});
}
// 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: 'grabpay',
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: 'grabpay'
}
});
// Return authorization URL
res.json({
authorize_uri: charge.authorize_uri,
charge_id: charge.id
});
} catch (error) {
console.error('GrabPay 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 === 'grabpay') {
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​
GrabPay supports voiding within 24 hours of charge creation:
// Void immediately (full amount only)
const refund = await omise.charges.refund('chrg_test_...', {
amount: 10000 // Full amount
});
if (refund.voided) {
console.log('Charge was voided (within 24 hours)');
}
Refunds​
Full and partial refunds supported within 30 days:
// Full refund
const fullRefund = await omise.charges.refund('chrg_test_...', {
amount: 10000
});
// Partial refund
const partialRefund = await omise.charges.refund('chrg_test_...', {
amount: 5000 // Half refund
});
GrabPay supports both full and partial refunds within 30 days of the original transaction.
Common Issues & Troubleshooting​
Issue: Customer doesn't have Grab app​
Cause: Customer selected GrabPay but doesn't have Grab app installed
Solution:
- Display clear instructions before payment
- Check platform (iOS/Android)
- Provide app download links
function checkGrabApp() {
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const isAndroid = /Android/i.test(navigator.userAgent);
if (!isIOS && !isAndroid) {
alert('GrabPay requires the Grab mobile app. Please use a mobile device.');
return false;
}
return true;
}
Issue: Insufficient balance​
Error: Payment declined
Solution:
- Show clear error message
- Allow customer to top up GrabPay wallet
- Offer alternative payment methods
if (charge.failure_code === 'insufficient_balance') {
showMessage('Insufficient GrabPay balance. Please top up or use another payment method.');
}
Issue: Currency mismatch​
Error: Unsupported currency
Solution:
function validateGrabPayCurrency(currency, country) {
const currencyMap = {
'SG': 'SGD',
'MY': 'MYR',
'TH': 'THB'
};
const expectedCurrency = currencyMap[country];
if (currency !== expectedCurrency) {
throw new Error(`Use ${expectedCurrency} for ${country}`);
}
}
Issue: Payment timeout​
Cause: Customer didn't complete payment within time limit
Solution:
- Set 15-minute timeout
- Allow retry with new charge
- Send reminder
const TIMEOUT = 15 * 60 * 1000; // 15 minutes
setTimeout(() => {
if (!paymentConfirmed) {
showTimeoutMessage();
allowRetry();
}
}, TIMEOUT);
Best Practices​
1. Display Clear Instructions​
<div class="grabpay-instructions">
<h3>Pay with GrabPay</h3>
<ol>
<li>Make sure you have the Grab app installed</li>
<li>Ensure sufficient balance in your GrabPay wallet</li>
<li>You'll be redirected to the Grab app</li>
<li>Authenticate and confirm the payment</li>
</ol>
<p>Don't have enough balance? <a href="https://grab.com/sg/pay/">Top up now</a></p>
</div>
2. Handle Mobile Deep Links​
function openGrabApp(authorizeUri) {
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
window.location = authorizeUri;
// Check if app opened
setTimeout(() => {
if (!document.hidden) {
showInstallAppMessage();
}
}, 2000);
} else {
alert('GrabPay is only available on mobile devices');
}
}
3. Validate Amount by Currency​
function validateAmount(amount, currency) {
const limits = {
SGD: { min: 50, max: 50000, label: '$' },
MYR: { min: 100, max: 150000, label: 'RM' },
THB: { min: 2000, max: 5000000, label: '฿' }
};
const { min, max, label } = limits[currency];
if (amount < min) {
return `Minimum amount is ${label}${min / 100}`;
}
if (amount > max) {
return `Maximum amount is ${label}${max / 100}`;
}
return null; // Valid
}
4. Use Webhooks​
// Webhook is primary notification
app.post('/webhooks/omise', handleWebhook);
// Callback is backup
app.get('/payment/callback', handleCallback);
5. Handle Regional Differences​
const GRABPAY_CONFIG = {
SG: {
currency: 'SGD',
minAmount: 50,
maxAmount: 50000,
displayName: 'GrabPay (Singapore)'
},
MY: {
currency: 'MYR',
minAmount: 100,
maxAmount: 150000,
displayName: 'GrabPay (Malaysia)'
},
TH: {
currency: 'THB',
minAmount: 2000,
maxAmount: 5000000,
displayName: 'GrabPay (Thailand)'
}
};
FAQ​
What countries support GrabPay?
GrabPay is available in Singapore, Malaysia, and Thailand through Omise. Grab operates in 8 countries total, but payment integration is currently available for these three markets.
Do customers need a Grab account?
Yes, customers must have the Grab app installed and a GrabPay wallet activated. The app is free and available on iOS and Android.
What are the transaction limits?
Limits vary by country:
- Singapore: $0.50 - $500 per transaction
- Malaysia: RM1.00 - RM1,500 per transaction
- Thailand: ฿20 - ฿50,000 per transaction
Daily and monthly limits depend on customer verification level.
How long does settlement take?
GrabPay settlements typically occur within 1-3 business days. Check your Omise dashboard for specific settlement schedules for your account.
Can I refund GrabPay payments?
Yes, GrabPay supports both full and partial refunds within 30 days of the original transaction. Voiding is available within 24 hours.
What if customer has insufficient balance?
The payment will be declined. Customers can top up their GrabPay wallet via:
- Credit/debit card
- Bank transfer
- 7-Eleven, FamilyMart (in Thailand)
- Other cash channels
Provide clear error messages and offer alternative payment methods.
Does GrabPay work on desktop?
GrabPay requires the Grab mobile app, so it's mobile-only. Desktop users should be shown alternative payment methods.
Testing​
Test Mode​
GrabPay can be tested in test mode using your test API keys. In test mode:
Test Credentials:
- Use test API keys (skey_test_xxx)
- Test all supported countries: Singapore, Malaysia, Thailand
- Test different currencies: SGD, MYR, THB
Test Flow:
- Create source and charge with test API keys
- Redirect customer to test
authorize_uri - Test authorization page will be displayed
- Use dashboard Actions to simulate payment success/failure
- Verify webhook delivery and return_uri handling
Testing Implementation:
// Test GrabPay for different countries
const testCountries = ['SG', 'MY', 'TH'];
for (const country of testCountries) {
const config = countryConfig[country];
const source = await omise.sources.create({
type: 'grabpay',
amount: config.minAmount,
currency: config.currency
});
const charge = await omise.charges.create({
amount: config.minAmount,
currency: config.currency,
source: source.id,
return_uri: 'https://example.com/callback'
});
console.log(`Test ${country}:`, charge.authorize_uri);
}
Test Scenarios:
- Successful payment: Verify order completion workflow
- Failed payment: Test error handling and retry mechanisms
- Multi-country: Test each supported country separately
- Amount limits: Verify country-specific limits are enforced
- Currency validation: Ensure proper currency for each country
- Mobile flow: Test redirect and deep-linking on mobile
- Timeout: Test abandoned payment handling
Important Notes:
- Test mode won't connect to real Grab servers
- Use Omise Dashboard to mark test charges as successful/failed
- Test each country/currency combination before going live
- Verify webhooks are received for all status changes
- Test on both iOS and Android if supporting mobile
For comprehensive testing guidelines, see the Testing Documentation.
Related Resources​
- Digital Wallets Overview - All wallet options
- TrueMoney - Thailand wallet
- ShopeePay - Alternative wallet
- Touch 'n Go - Malaysia wallet
- Refunds - Refund policies
- Testing - Test GrabPay integration