Skip to main content

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 Statistics

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โ€‹

RegionCurrencyMin AmountMax AmountDaily Limit
MalaysiaMYRRM1.00RM30,000RM30,000*

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

Transaction Limits by Verification Levelโ€‹

Verification LevelPer TransactionDaily LimitMonthly Limit
Lite (Phone + IC)RM1,500RM1,500RM10,000
Full (Bank linked)RM30,000RM30,000RM100,000

How It Worksโ€‹

Customer Experience:

  1. Customer selects "Touch 'n Go eWallet" at checkout
  2. Redirected to TNG authorization page
  3. Opens TNG eWallet app (deep link)
  4. Reviews payment details
  5. Authenticates with 6-digit PIN or biometric
  6. Confirms payment
  7. Returns to merchant site

Typical completion time: 30 seconds - 2 minutes

Implementationโ€‹

Step 1: Create Touch 'n Go Sourceโ€‹

curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=touch_n_go" \
-d "amount=15000" \
-d "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
});
No Partial Refunds

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:

  1. Create source and charge with test API keys
  2. Customer redirects to test authorize_uri
  3. Test page simulates Touch 'n Go authorization
  4. Use Omise Dashboard Actions to mark charge as successful/failed
  5. 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.

Next Stepsโ€‹

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