DuitNow QR
Accept instant payments via DuitNow QR, Malaysia's national QR payment standard supported by all major banks and e-wallets with 30+ million users.
Overviewโ
DuitNow QR is Malaysia's national QR payment system operated by PayNet, similar to Thailand's PromptPay and Singapore's PayNow. It provides a standardized QR code that works across all participating banks and e-wallets in Malaysia.
Key Features:
- โ Universal acceptance - Works with 30+ banks and e-wallets
- โ Fast confirmation - Near real-time payment verification (typically within seconds)
- โ 30+ million users - Wide adoption across Malaysia
- โ Interoperable - One QR for all banks/wallets
- โ 24/7 availability - Works anytime
- โ Refund support - Full and partial refunds
Supported Regionโ
| Region | Currency | Min Amount | Max Amount | Transaction Limit |
|---|---|---|---|---|
| Malaysia | MYR | RM 0.01 | RM 20,000 | Varies by bank/wallet* |
*Daily limits vary by customer's bank/e-wallet and are not specified by Omise
Supported Banks & E-Walletsโ
Major Banks:
- Maybank
- CIMB Bank
- Public Bank
- RHB Bank
- Hong Leong Bank
- AmBank
- Bank Islam
- And 20+ more banks
E-Wallets:
- Boost
- Touch 'n Go eWallet
- GrabPay
- ShopeePay
- BigPay
How It Worksโ
Customer scans DuitNow QR โ Opens banking/wallet app โ Confirms payment โ Instant confirmation (10-30 seconds total)
Implementationโ
Create Source and Display QRโ
- Node.js
- PHP
- Python
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
// Create DuitNow QR source
const source = await omise.sources.create({
type: 'duitnow_qr',
amount: 10000, // RM 100.00
currency: 'MYR'
});
// Get QR code image URL
const qrCodeUrl = source.scannable_code.image.download_uri;
// Create charge
const charge = await omise.charges.create({
amount: 10000,
currency: 'MYR',
source: source.id,
metadata: {
order_id: 'ORD-12345'
}
});
// Display QR code and poll for status
res.render('payment-qr', {
qr_code: qrCodeUrl,
charge_id: charge.id,
amount: 'RM 100.00'
});
<?php
$source = OmiseSource::create(array(
'type' => 'duitnow_qr',
'amount' => 10000,
'currency' => 'MYR'
));
$qrCodeUrl = $source['scannable_code']['image']['download_uri'];
$charge = OmiseCharge::create(array(
'amount' => 10000,
'currency' => 'MYR',
'source' => $source['id']
));
// Display QR code
echo '<img src="' . $qrCodeUrl . '" alt="DuitNow QR">';
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
source = omise.Source.create(
type='duitnow_qr',
amount=10000,
currency='MYR'
)
qr_code_url = source.scannable_code['image']['download_uri']
charge = omise.Charge.create(
amount=10000,
currency='MYR',
source=source.id
)
Complete Implementationโ
const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const app = express();
app.use(express.json());
// Create DuitNow QR payment
app.post('/checkout/duitnow-qr', async (req, res) => {
try {
const { amount, order_id } = req.body;
// Validate amount
if (amount < 1) {
return res.status(400).json({
error: 'Minimum amount is RM 0.01'
});
}
if (amount > 500000) {
return res.status(400).json({
error: 'Maximum amount is RM 5,000'
});
}
// Create source
const source = await omise.sources.create({
type: 'duitnow_qr',
amount: amount,
currency: 'MYR'
});
// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: 'MYR',
source: source.id,
metadata: { order_id }
});
res.json({
qr_code_url: source.scannable_code.image.download_uri,
charge_id: charge.id,
expires_at: new Date(Date.now() + 5 * 60 * 1000)
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Check charge status
app.get('/api/charges/:chargeId/status', async (req, res) => {
try {
const charge = await omise.charges.retrieve(req.params.chargeId);
res.json({
status: charge.status,
paid: charge.paid
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Webhook
app.post('/webhooks/omise', (req, res) => {
const event = req.body;
if (event.key === 'charge.complete' && event.data.source.type === 'duitnow_qr') {
const charge = event.data;
if (charge.status === 'successful') {
fulfillOrder(charge.metadata.order_id);
}
}
res.sendStatus(200);
});
app.listen(3000);
QR Code Displayโ
<div class="duitnow-payment">
<h3>Scan untuk Bayar (Scan to Pay)</h3>
<div class="qr-container">
<img id="qr-code" src="{{ qr_code_url }}" alt="DuitNow QR Code">
</div>
<div class="amount">
<strong>RM {{ amount }}</strong>
</div>
<div id="status">
<p>Menunggu pembayaran...</p>
<div class="spinner"></div>
</div>
<div class="instructions">
<h4>Cara Bayar:</h4>
<ol>
<li>Buka app bank atau e-wallet anda</li>
<li>Pilih "Scan QR" atau "DuitNow"</li>
<li>Imbas QR code di atas</li>
<li>Sahkan pembayaran</li>
</ol>
</div>
<div class="supported-apps">
<p><small>Boleh guna dengan semua bank dan e-wallet Malaysia</small></p>
</div>
</div>
<style>
.qr-container {
background: white;
padding: 20px;
border-radius: 12px;
display: inline-block;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
#qr-code {
width: 280px;
height: 280px;
display: block;
}
@media (max-width: 768px) {
#qr-code {
width: 240px;
height: 240px;
}
}
</style>
<script>
// Poll for payment status
const chargeId = '{{ charge_id }}';
const pollInterval = setInterval(async () => {
const response = await fetch(`/api/charges/${chargeId}/status`);
const data = await response.json();
if (data.status === 'successful') {
clearInterval(pollInterval);
document.getElementById('status').innerHTML =
'<p class="success">โ Pembayaran Berjaya!</p>';
setTimeout(() => {
window.location = '/payment-success';
}, 1000);
} else if (data.status === 'failed') {
clearInterval(pollInterval);
document.getElementById('status').innerHTML =
'<p class="error">Pembayaran gagal. Sila cuba lagi.</p>';
}
}, 3000);
// Timeout after 5 minutes
setTimeout(() => {
clearInterval(pollInterval);
document.getElementById('status').innerHTML =
'<p class="error">QR code tamat tempoh. Sila jana semula.</p>';
}, 300000);
</script>
Refund Supportโ
// Full or partial refund within 30 days
const refund = await omise.charges.refund('chrg_test_...', {
amount: 10000 // Full or partial
});
Testingโ
Test Amount: RM 100.00 (10000 smallest unit) Expected: Successful payment in test mode
Best Practicesโ
1. Show Supported Banks/Walletsโ
<div class="supported-apps">
<h4>Boleh guna dengan:</h4>
<div class="app-grid">
<div class="app">
<img src="/banks/maybank.svg" alt="Maybank">
<span>Maybank</span>
</div>
<div class="app">
<img src="/banks/cimb.svg" alt="CIMB">
<span>CIMB</span>
</div>
<div class="app">
<img src="/wallets/tng.svg" alt="TnG">
<span>Touch 'n Go</span>
</div>
<div class="app">
<img src="/wallets/boost.svg" alt="Boost">
<span>Boost</span>
</div>
<p><small>+ 30+ bank dan e-wallet lain</small></p>
</div>
</div>
2. Multi-Language Supportโ
const translations = {
ms: {
title: 'Scan untuk Bayar',
waiting: 'Menunggu pembayaran...',
success: 'Pembayaran Berjaya!',
expired: 'QR code tamat tempoh'
},
en: {
title: 'Scan to Pay',
waiting: 'Waiting for payment...',
success: 'Payment Successful!',
expired: 'QR code expired'
}
};
3. Handle QR Expirationโ
const QR_EXPIRY = 5 * 60 * 1000;
setTimeout(() => {
if (!paymentCompleted) {
showExpiredMessage();
enableRetryButton();
}
}, QR_EXPIRY);
FAQโ
What is DuitNow QR?
DuitNow QR is Malaysia's national QR payment standard operated by PayNet. It provides interoperability between all participating banks and e-wallets in Malaysia.
Which banks and e-wallets support DuitNow QR?
All major Malaysian banks (30+) and popular e-wallets including Maybank, CIMB, Public Bank, RHB, Touch 'n Go, Boost, GrabPay, and ShopeePay.
What are the transaction limits?
- Minimum: RM 0.01
- Maximum: RM 5,000 per transaction
- Daily limits vary by bank/e-wallet (typically RM 30,000)
How long are QR codes valid?
DuitNow QR codes are typically valid for 5 minutes. After expiration, generate a new QR code.
Can I refund DuitNow QR payments?
Yes, full and partial refunds are supported within 30 days of the original transaction.
Is it better than FPX?
DuitNow QR:
- Instant payment
- Works with QR scanner
- Better for in-store and mobile
FPX:
- Internet banking
- Better for desktop users
- No refunds
Choose based on your use case.
Related Resourcesโ
- QR Payments Overview - All QR methods
- Maybank QR - Maybank-specific QR
- FPX - Malaysia internet banking
- Touch 'n Go - Malaysia e-wallet
- Testing - Test your integration
Next Stepsโ
- Create DuitNow QR source
- Display QR code on your site
- Implement status polling
- Set up webhook handler
- Handle QR expiration
- Test with actual banking apps
- Go live