PromptPay
Thailand's national instant payment system. Accept QR code payments from any Thai bank with instant confirmation and low fees.
Overviewâ
PromptPay is Thailand's real-time payment infrastructure that enables instant money transfers via QR codes or mobile numbers. It's one of the most widely adopted digital payment methods in Thailand, supported by all major banks and used by millions of customers daily.
Key Benefits:
- â Instant payment confirmation
- â Competitive transaction fees*
- â Supported by all Thai banks
- â No customer registration required
- â Works with existing banking apps
- â Perfect for offline transactions
Supported Regionsâ
| Region | Currency | Min Amount | Max Amount | API Version |
|---|---|---|---|---|
| Thailand | THB | ā¸ŋ20.00 | ā¸ŋ150,000.00 | 2017-11-02 |
To enable PromptPay, email support@omise.co to request this feature.
Transaction fees vary by merchant agreement, payment method, and business type. Contact Omise sales or check your merchant dashboard for your specific pricing.
Customer Experience:
- Customer sees QR code at checkout
- Opens any Thai banking app
- Scans QR code using built-in scanner
- Confirms payment with PIN/biometric
- Receives instant confirmation
Payment Flow Examplesâ
Desktop Browser Flow:

The desktop flow shows the complete payment journey:
- âļ Customer clicks "Pay with PromptPay" - Initiates payment from merchant checkout page
- ⡠QR code displayed - Merchant shows PromptPay QR code on screen
- ⸠Customer scans QR - Uses mobile banking app to scan the QR code
- âš Opens banking app - QR scan automatically opens the customer's banking app
- âē Review payment details - Customer sees amount and merchant information
- âģ Authenticate & confirm - Customer enters PIN or uses biometric authentication
- âŧ Payment complete - Instant confirmation, customer returns to merchant site
Mobile Browser Flow:

The mobile flow streamlines the experience for smartphone users:
- âļ Customer selects PromptPay - Taps PromptPay payment option at checkout
- ⡠QR code displayed - Payment page shows PromptPay QR code
- ⸠Tap to open app - Customer taps "Open Banking App" button
- âš Banking app launches - Deep link opens the customer's banking app automatically
- âē Review transaction - Pre-filled payment details appear in the app
- âģ Confirm payment - Customer authenticates with PIN/biometric
- âŧ Return to merchant - Automatic redirect back to merchant's success page
Implementation Guideâ
Step 1: Create PromptPay Sourceâ
- cURL
- Node.js
- PHP
- Python
curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=promptpay" \
-d "amount=35000" \
-d "currency=THB"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
const source = await omise.sources.create({
type: 'promptpay',
amount: 35000, // THB 350.00
currency: 'THB'
});
console.log(source.scannable_code.image.download_uri);
<?php
$source = OmiseSource::create(array(
'type' => 'promptpay',
'amount' => 35000,
'currency' => 'THB'
));
$qrCodeUrl = $source['scannable_code']['image']['download_uri'];
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
source = omise.Source.create(
type='promptpay',
amount=35000,
currency='THB'
)
qr_code_url = source.scannable_code.image.download_uri
Response:
{
"object": "source",
"id": "src_test_5rt6s9vah5lkvi1rh9c",
"type": "promptpay",
"flow": "offline",
"amount": 35000,
"currency": "THB",
"scannable_code": {
"type": "qr",
"image": {
"download_uri": "https://api.omise.co/charges/.../documents/qr_code.png"
}
}
}
Step 2: Create Charge with Sourceâ
curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=35000" \
-d "currency=THB" \
-d "source=src_test_5rt6s9vah5lkvi1rh9c" \
-d "return_uri=https://yourdomain.com/payment/callback"
Step 3: Display QR Code to Customerâ
<div class="promptpay-payment">
<h2>Scan to Pay with PromptPay</h2>
<img id="qr-code" alt="PromptPay QR Code" />
<p>Open your banking app and scan this QR code</p>
<p class="amount">Amount: ā¸ŋ350.00</p>
<div id="payment-status">Waiting for payment...</div>
</div>
<script>
// Display QR code from API response
document.getElementById('qr-code').src = qrCodeUrl;
// Poll for payment status or use webhooks
async function checkPaymentStatus(chargeId) {
const response = await fetch(`/api/check-payment/${chargeId}`);
const data = await response.json();
if (data.status === 'successful') {
document.getElementById('payment-status').textContent = 'Payment successful!';
window.location.href = '/payment-success';
} else if (data.status === 'failed') {
document.getElementById('payment-status').textContent = 'Payment failed';
}
}
// Check status every 3 seconds
const chargeId = 'chrg_test_...';
const pollInterval = setInterval(() => {
checkPaymentStatus(chargeId);
}, 3000);
// Stop polling after 24 hours (default QR expiration)
setTimeout(() => {
clearInterval(pollInterval);
document.getElementById('payment-status').textContent = 'Payment expired';
}, 24 * 60 * 60 * 1000);
</script>
Step 4: Handle Webhook Notificationâ
// Server-side webhook handler
app.post('/webhooks/omise', async (req, res) => {
const event = req.body;
if (event.key === 'charge.complete' && event.data.source.type === 'promptpay') {
const charge = event.data;
if (charge.status === 'successful') {
// Update order status
await updateOrderStatus(charge.metadata.order_id, 'paid');
// Send confirmation email
await sendOrderConfirmation(charge.metadata.customer_email);
}
}
res.sendStatus(200);
});
Complete Implementation Exampleâ
Frontend (HTML + JavaScript)â
<!DOCTYPE html>
<html>
<head>
<title>PromptPay Checkout</title>
<style>
.promptpay-container {
max-width: 400px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
text-align: center;
}
.qr-code {
width: 300px;
height: 300px;
margin: 20px auto;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.status.waiting {
background: #fff3cd;
color: #856404;
}
.status.success {
background: #d4edda;
color: #155724;
}
.status.failed {
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<div class="promptpay-container">
<h2>Pay with PromptPay</h2>
<div class="amount">Amount: <strong id="amount-display"></strong></div>
<img id="qr-code" class="qr-code" alt="PromptPay QR Code" />
<div id="instructions">
<p>1. Open your banking app</p>
<p>2. Select "Scan QR" or "PromptPay"</p>
<p>3. Scan the QR code above</p>
<p>4. Confirm the payment</p>
</div>
<div id="status" class="status waiting">Waiting for payment...</div>
<div id="timer"></div>
</div>
<script>
// Initialize payment
async function initializePromptPayPayment() {
const amount = 35000; // THB 350.00
document.getElementById('amount-display').textContent = `ā¸ŋ${(amount / 100).toFixed(2)}`;
try {
// Call your server to create charge
const response = await fetch('/api/create-promptpay-charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: amount,
order_id: 'ORDER-12345',
customer_email: 'customer@example.com'
})
});
const data = await response.json();
// Display QR code
document.getElementById('qr-code').src = data.qr_code_url;
// Start status polling
startStatusPolling(data.charge_id);
// Start expiration timer
startExpirationTimer(data.expires_at);
} catch (error) {
document.getElementById('status').textContent = 'Error creating payment';
document.getElementById('status').className = 'status failed';
}
}
function startStatusPolling(chargeId) {
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/check-charge-status/${chargeId}`);
const data = await response.json();
if (data.status === 'successful') {
clearInterval(pollInterval);
document.getElementById('status').textContent = 'Payment successful!';
document.getElementById('status').className = 'status success';
setTimeout(() => {
window.location.href = '/payment-success';
}, 2000);
} else if (data.status === 'failed') {
clearInterval(pollInterval);
document.getElementById('status').textContent = 'Payment failed';
document.getElementById('status').className = 'status failed';
}
} catch (error) {
console.error('Error checking status:', error);
}
}, 3000); // Check every 3 seconds
}
function startExpirationTimer(expiresAt) {
const expiryTime = new Date(expiresAt).getTime();
const timerInterval = setInterval(() => {
const now = new Date().getTime();
const timeLeft = expiryTime - now;
if (timeLeft <= 0) {
clearInterval(timerInterval);
document.getElementById('timer').textContent = 'QR code expired';
document.getElementById('status').textContent = 'Payment expired. Please try again.';
document.getElementById('status').className = 'status failed';
} else {
const minutes = Math.floor(timeLeft / (1000 * 60));
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
document.getElementById('timer').textContent =
`Time remaining: ${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}, 1000);
}
// Initialize on page load
window.addEventListener('load', initializePromptPayPayment);
</script>
</body>
</html>
Backend (Node.js/Express)â
const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const app = express();
app.use(express.json());
// Create PromptPay charge
app.post('/api/create-promptpay-charge', async (req, res) => {
try {
const { amount, order_id, customer_email } = req.body;
// Step 1: Create source
const source = await omise.sources.create({
type: 'promptpay',
amount: amount,
currency: 'THB'
});
// Step 2: Create charge
const charge = await omise.charges.create({
amount: amount,
currency: 'THB',
source: source.id,
metadata: {
order_id: order_id,
customer_email: customer_email
}
});
res.json({
charge_id: charge.id,
qr_code_url: source.scannable_code.image.download_uri,
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // 24 hours default
});
} catch (error) {
console.error('Error creating PromptPay charge:', error);
res.status(500).json({ error: error.message });
}
});
// Check charge status
app.get('/api/check-charge-status/:chargeId', 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 handler
app.post('/webhooks/omise', async (req, res) => {
const event = req.body;
// Verify webhook signature here (recommended)
if (event.key === 'charge.complete') {
const charge = event.data;
if (charge.status === 'successful' && charge.source.type === 'promptpay') {
// Process successful payment
await processOrder(charge.metadata.order_id);
await sendConfirmationEmail(charge.metadata.customer_email, charge);
}
}
res.sendStatus(200);
});
app.listen(3000);
Features & Customizationâ
QR Code Expirationâ
By default, PromptPay QR codes expire after 24 hours. Customize expiration:
curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=promptpay" \
-d "amount=35000" \
-d "currency=THB" \
-d "expires_in=1800" # 30 minutes (in seconds)
Include Order Detailsâ
const source = await omise.sources.create({
type: 'promptpay',
amount: 35000,
currency: 'THB',
metadata: {
order_id: 'ORD-2024-001',
customer_id: '12345',
items: 'Premium Subscription x1'
}
});
Refund Supportâ
PromptPay does NOT support refunds or voids through Omise. Once a payment is completed, it cannot be automatically refunded via the API.
Alternative Options:
- Manual bank transfer to customer
- Store credit/voucher
- Process refund outside Omise system
Common Issues & Troubleshootingâ
Issue: QR Code Not Displayingâ
Causes:
- Invalid amount (below ā¸ŋ20 or above ā¸ŋ150,000)
- Network issues
- Incorrect API response handling
Solution:
if (!source.scannable_code || !source.scannable_code.image) {
console.error('QR code not available in response');
// Handle error
} else {
const qrCodeUrl = source.scannable_code.image.download_uri;
// Display QR code
}
Issue: Payment Not Confirmingâ
Causes:
- Customer didn't complete payment
- Bank app issues
- Network problems
- QR code expired
Solution:
- Implement proper timeout handling
- Show clear expiration timer
- Provide option to generate new QR code
- Use webhooks for reliable confirmation
Issue: Duplicate Paymentsâ
Cause: Customer scans QR code multiple times
Prevention:
// Mark charge as processing when first scanned
const charge = await omise.charges.retrieve(chargeId);
if (charge.status === 'successful') {
// Already paid - don't process again
return;
}
Best Practicesâ
-
Show Clear Instructions
- Step-by-step guide for customers
- Supported banking apps listed
- Visual instructions with screenshots
-
Implement Timeout Handling
- Show expiration countdown
- Auto-redirect on timeout
- Allow easy QR regeneration
-
Use Webhooks for Confirmation
- Don't rely solely on polling
- Webhooks provide instant notification
- More reliable than status checks
-
Provide Payment Status Feedback
- Real-time status updates
- Clear success/failure messages
- Order confirmation details
-
Mobile-First Design
- Most customers pay via mobile
- Optimize QR code size
- Easy-to-scan positioning
-
Handle Errors Gracefully
- Network failures
- API errors
- Timeout scenarios
- Clear error messages
FAQâ
Which banks support PromptPay?
All major Thai banks support PromptPay, including:
- Bangkok Bank
- Kasikornbank (K-Bank)
- Siam Commercial Bank (SCB)
- Krung Thai Bank
- Bank of Ayudhya (Krungsri)
- TMB Bank
- Government Savings Bank
- All other Thai commercial banks
Customers can use any banking app with PromptPay functionality.
How long does payment confirmation take?
PromptPay payments are instant. Confirmation typically arrives within seconds after the customer approves the payment in their banking app.
Can I use PromptPay for recurring payments?
No, PromptPay requires customer interaction (scanning QR code) for each payment. It cannot be used for automatic recurring charges. For subscriptions, use credit card saved payments instead.
What's the transaction fee for PromptPay?
PromptPay offers competitive transaction fees that are significantly lower than credit card processing fees. Contact Omise sales for specific pricing for your business..
Can international customers use PromptPay?
No, PromptPay is only available to customers with Thai bank accounts. For international payments, use credit cards or multi-currency payments.
How do I test PromptPay integration?
In test mode:
- Create PromptPay source and charge
- QR code will be generated
- Use dashboard Actions to manually mark charge as successful/failed
- Test webhook handling
You cannot actually scan test QR codes with banking apps.
Related Resourcesâ
- QR Payments Overview - All QR payment methods
- PayNow - Singapore equivalent
- DuitNow QR - Malaysia equivalent
- Webhooks - Handle payment events
- Testing - Test PromptPay integration