Touch 'n Go eWallet
Accept payments from Touch 'n Go eWallet, Malaysia's most popular digital wallet with 17+ million users and extensive merchant network across the country.
Overviewโ
User numbers are approximate and based on publicly available information. Actual active user counts may vary.
Touch 'n Go eWallet (TNG eWallet) is Malaysia's leading digital payment platform, originally known for highway tolls and now expanded to a full-featured digital wallet. Users can pay with their TNG eWallet balance for instant, secure transactions both online and offline.
Key Features:
- โ 17+ million users - Malaysia's most popular e-wallet
- โ Instant confirmation - Real-time payment processing
- โ Wide adoption - Accepted everywhere in Malaysia
- โ Trusted brand - Part of Touch 'n Go Group
- โ No transaction fees - For customers
- โ Rewards program - Users earn GO+ points
Supported Regionsโ
| Region | Currency | Min Amount | Max Amount | Daily Limit |
|---|---|---|---|---|
| Malaysia | MYR | RM1.00 | RM30,000 | RM30,000* |
*Daily limits vary based on customer's wallet verification level
Transaction Limits by Verification Levelโ
| Verification Level | Per Transaction | Daily Limit | Monthly Limit |
|---|---|---|---|
| Lite (Phone + IC) | RM1,500 | RM1,500 | RM10,000 |
| Full (Bank linked) | RM30,000 | RM30,000 | RM100,000 |
How It Worksโ
Customer Experience:
- Customer selects "Touch 'n Go eWallet" at checkout
- Redirected to TNG authorization page
- Opens TNG eWallet app (deep link)
- Reviews payment details
- Authenticates with 6-digit PIN or biometric
- Confirms payment
- Returns to merchant site
Typical completion time: 30 seconds - 2 minutes
Implementationโ
Step 1: Create Touch 'n Go Sourceโ
- cURL
- Node.js
- PHP
- Python
- Ruby
- Go
- Java
- C#
curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=touch_n_go" \
-d "amount=15000" \
-d "currency=MYR"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
const source = await omise.sources.create({
type: 'touch_n_go',
amount: 15000, // MYR 150.00
currency: 'MYR'
});
<?php
$source = OmiseSource::create(array(
'type' => 'touch_n_go',
'amount' => 15000,
'currency' => 'MYR'
));
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
source = omise.Source.create(
type='touch_n_go',
amount=15000,
currency='MYR'
)
require 'omise'
Omise.api_key = 'skey_test_YOUR_SECRET_KEY'
source = Omise::Source.create({
type: 'touch_n_go',
amount: 15000,
currency: 'MYR'
})
source, err := client.Sources().Create(&operations.CreateSource{
Type: "touch_n_go",
Amount: 15000,
Currency: "MYR",
})
Source source = client.sources().create(new Source.CreateParams()
.type("touch_n_go")
.amount(15000L)
.currency("MYR"));
var source = await client.Sources.Create(new CreateSourceRequest
{
Type = "touch_n_go",
Amount = 15000,
Currency = "MYR"
});
Response:
{
"object": "source",
"id": "src_test_5rt6s9vah5lkvi1rh9c",
"type": "touch_n_go",
"flow": "redirect",
"amount": 15000,
"currency": "MYR"
}
Step 2: Create Chargeโ
curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=15000" \
-d "currency=MYR" \
-d "source=src_test_5rt6s9vah5lkvi1rh9c" \
-d "return_uri=https://yourdomain.com/payment/callback"
Step 3: Redirect Customerโ
app.post('/checkout/touch-n-go', async (req, res) => {
try {
const { amount, order_id } = req.body;
// Validate amount
if (amount < 100 || amount > 3000000) {
return res.status(400).json({
error: 'Amount must be between RM1 and RM30,000'
});
}
// Create source
const source = await omise.sources.create({
type: 'touch_n_go',
amount: amount,
currency: 'MYR'
});
// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: 'MYR',
source: source.id,
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id
}
});
// Redirect to Touch 'n Go
res.redirect(charge.authorize_uri);
} catch (error) {
console.error('Touch n Go 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 === 'touch_n_go') {
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());
app.post('/checkout/touch-n-go', async (req, res) => {
try {
const { amount, order_id } = req.body;
// Validate amount (RM1 - RM30,000)
if (amount < 100 || amount > 3000000) {
return res.status(400).json({
error: 'Amount must be between RM1 and RM30,000'
});
}
// Create source
const source = await omise.sources.create({
type: 'touch_n_go',
amount: amount,
currency: 'MYR'
});
// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: 'MYR',
source: source.id,
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id,
payment_method: 'touch_n_go'
}
});
// Return authorization URL
res.json({
authorize_uri: charge.authorize_uri,
charge_id: charge.id
});
} catch (error) {
console.error('TNG 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 === 'touch_n_go') {
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โ
Touch 'n Go supports voiding within 24 hours:
// Void immediately (full amount)
const refund = await omise.charges.refund('chrg_test_...', {
amount: 15000
});
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: 15000 // Must be full amount
});
Touch 'n Go eWallet does NOT support partial refunds. Only full refunds are allowed within 30 days.
Common Issues & Troubleshootingโ
Issue: Customer doesn't have TNG eWallet appโ
Cause: Customer selected Touch 'n Go but doesn't have app
Solution:
function checkMobile() {
if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
alert('Touch n Go eWallet requires the 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 TNG eWallet balance. Please reload your wallet.');
offerAlternativePayment();
}
Issue: Payment timeoutโ
Solution:
const TIMEOUT = 15 * 60 * 1000; // 15 minutes
setTimeout(() => {
if (!paymentConfirmed) {
showTimeoutMessage();
allowRetry();
}
}, TIMEOUT);
Issue: Different currency errorโ
Cause: Touch 'n Go only supports MYR
Solution:
if (currency !== 'MYR') {
throw new Error('Touch n Go only supports MYR currency');
}
Best Practicesโ
1. Display Instructionsโ
<div class="tng-instructions">
<h3>Pay with Touch 'n Go eWallet</h3>
<ol>
<li>Ensure you have TNG eWallet app installed</li>
<li>Check your wallet has sufficient balance</li>
<li>You'll be redirected to the TNG eWallet app</li>
<li>Enter your 6-digit PIN or use biometric</li>
<li>Confirm the payment</li>
</ol>
<p>Need to reload? Use online banking, credit card, or at retailers.</p>
</div>
2. Validate Amountโ
function validateTNGAmount(amount) {
const MIN = 100; // RM1.00
const MAX = 3000000; // RM30,000.00
if (amount < MIN) {
return 'Minimum amount is RM1.00';
}
if (amount > MAX) {
return 'Maximum amount is RM30,000.00';
}
return null;
}
3. Mobile-Only Optionโ
// Only show TNG on mobile
if (/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
document.getElementById('tng-option').style.display = 'block';
} else {
document.getElementById('tng-option').style.display = 'none';
}
4. Use Webhooksโ
// Webhook is primary notification
app.post('/webhooks/omise', handleWebhook);
// Callback is backup
app.get('/payment/callback', handleCallback);
FAQโ
What is Touch 'n Go eWallet?
Touch 'n Go eWallet is Malaysia's most popular digital wallet with 17+ million users. Originally known for highway toll payments, it's now a full-featured e-wallet accepted widely across Malaysia.
Do customers need a TNG eWallet account?
Yes, customers must have the Touch 'n Go eWallet app installed with an activated account and sufficient balance.
What are the transaction limits?
- Lite users: RM1 - RM1,500 per transaction, RM1,500 daily
- Full users: RM1 - RM30,000 per transaction, RM30,000 daily
Customers can upgrade verification level in the app.
How long does settlement take?
Touch 'n Go eWallet settlements typically occur within 1-3 business days. Check your Omise dashboard for settlement schedules.
Can I refund Touch 'n Go 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 reload TNG eWallet via:
- Online banking
- Credit/debit card
- 7-Eleven, 99 Speedmart, MyNews
- Petronas stations
- ATMs
Does Touch 'n Go work on desktop?
No, Touch 'n Go eWallet requires the mobile app. Desktop users should see alternative payment methods.
Testingโ
Test Modeโ
Touch 'n Go eWallet can be tested using your test API keys. In test mode:
Test Credentials:
- Use test API keys (skey_test_xxx)
- Currency: MYR (Malaysian Ringgit)
- No actual Touch 'n Go account required for testing
Test Flow:
- Create source and charge with test API keys
- Customer redirects to test
authorize_uri - Test page simulates Touch 'n Go authorization
- Use Omise Dashboard Actions to mark charge as successful/failed
- Verify webhook notifications and callbacks
Testing Implementation:
// Test Touch 'n Go payment
const source = await omise.sources.create({
type: 'touch_n_go',
amount: 5000, // RM50.00
currency: 'MYR'
});
const charge = await omise.charges.create({
amount: 5000,
currency: 'MYR',
source: source.id,
return_uri: 'https://example.com/callback'
});
console.log('Test authorize URL:', charge.authorize_uri);
Test Scenarios:
- Successful payment: Verify order fulfillment flow
- Failed payment: Test error handling
- Amount limits: Test RM1 minimum and various amounts
- Mobile flow: Test deep-linking to TNG app
- Insufficient balance: Simulate low wallet balance
- Timeout: Test abandoned payment scenarios
- Webhook delivery: Verify all webhook events
Important Notes:
- Test mode doesn't connect to real TNG servers
- Use dashboard to simulate payment outcomes
- Test mobile app flow thoroughly
- Verify webhook handling for all statuses
- Test amount validation logic
For comprehensive testing guidelines, see the Testing Documentation.
Related Resourcesโ
- Digital Wallets Overview - All wallet options
- Boost - Alternative Malaysia wallet
- GrabPay - Multi-country wallet
- DuitNow QR - Malaysia QR payment
- Refunds - Refund policies