Skip to main content

PromptPay

Thailand's national instant payment system. Accept QR code payments from any Thai bank with instant confirmation and low fees.

Overview​

Information Accuracy

Payment method features, limits, user statistics, and fees are subject to change. Information is based on publicly available sources and may not reflect your specific merchant agreement. Always refer to official Omise documentation and your merchant dashboard for current, binding information.

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​

RegionCurrencyMin AmountMax Amount
ThailandTHBā¸ŋ20.00ā¸ŋ150,000.00
Transaction Fees

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:

  1. Customer sees QR code at checkout
  2. Opens any Thai banking app
  3. Scans QR code using built-in scanner
  4. Confirms payment with PIN/biometric
  5. Receives instant confirmation

Payment Flow Examples​

Desktop Browser Flow:

PromptPay Desktop Payment Flow

The desktop flow shows the complete payment journey:

  1. âļ Customer clicks "Pay with PromptPay" - Initiates payment from merchant checkout page
  2. ❷ QR code displayed - Merchant shows PromptPay QR code on screen
  3. ❸ Customer scans QR - Uses mobile banking app to scan the QR code
  4. ❚ Opens banking app - QR scan automatically opens the customer's banking app
  5. âē Review payment details - Customer sees amount and merchant information
  6. âģ Authenticate & confirm - Customer enters PIN or uses biometric authentication
  7. âŧ Payment complete - Instant confirmation, customer returns to merchant site

Mobile Browser Flow:

PromptPay Mobile Payment Flow

The mobile flow streamlines the experience for smartphone users:

  1. âļ Customer selects PromptPay - Taps PromptPay payment option at checkout
  2. ❷ QR code displayed - Payment page shows PromptPay QR code
  3. ❸ Tap to open app - Customer taps "Open Banking App" button
  4. ❚ Banking app launches - Deep link opens the customer's banking app automatically
  5. âē Review transaction - Pre-filled payment details appear in the app
  6. âģ Confirm payment - Customer authenticates with PIN/biometric
  7. âŧ Return to merchant - Automatic redirect back to merchant's success page

Implementation Guide​

Step 1: Create PromptPay Source​

curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=promptpay" \
-d "amount=35000" \
-d "currency=THB"

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​

No Refunds Available

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​

  1. Show Clear Instructions

    • Step-by-step guide for customers
    • Supported banking apps listed
    • Visual instructions with screenshots
  2. Implement Timeout Handling

    • Show expiration countdown
    • Auto-redirect on timeout
    • Allow easy QR regeneration
  3. Use Webhooks for Confirmation

    • Don't rely solely on polling
    • Webhooks provide instant notification
    • More reliable than status checks
  4. Provide Payment Status Feedback

    • Real-time status updates
    • Clear success/failure messages
    • Order confirmation details
  5. Mobile-First Design

    • Most customers pay via mobile
    • Optimize QR code size
    • Easy-to-scan positioning
  6. 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:

  1. Create PromptPay source and charge
  2. QR code will be generated
  3. Use dashboard Actions to manually mark charge as successful/failed
  4. Test webhook handling

You cannot actually scan test QR codes with banking apps.

Testing Guide →

Next Steps​

  1. Create your first PromptPay charge
  2. Implement QR code display
  3. Set up webhook handling
  4. Test with test mode
  5. Go live