Tutorial
Vietnam Payment API Tutorial: Integrate Bank Transfers in Your App
๐
July 14, 2026โฑ 10 min readโ๏ธ RMPay Team
Integrating Vietnamese bank transfer payments into your application is simpler than most developers expect. Vietnam's payment ecosystem runs on NAPAS โ a standardized interbank network โ which means a single API integration can cover 57 banks simultaneously. This tutorial walks through the complete integration process using RMPay's payment API.
Prerequisites
- RMPay merchant account (contact @rmpay1 to get started)
- Your
merchant_id and api_key
- A publicly accessible webhook URL on your server
- Basic knowledge of HTTP requests and JSON
Step 1: Create a Payment Order
When a customer reaches your checkout and selects bank transfer payment, your server creates a payment order via the RMPay API.
API Endpoint
Content-Type: application/json
X-API-Key: your_api_key
Request Body
{
"merchant_id": "your_merchant_id",
"order_id": "ORD_20260714_001",
"amount": 500000,
"currency": "VND",
"callback_url": "https://yoursite.com/webhook/rmpay",
"description": "Order #ORD_20260714_001"
}
Python Example
import requests
def create_payment_order(order_id: str, amount: int) -> dict:
response = requests.post(
"https://api.rmpay.co/v1/order/create",
headers={
"Content-Type": "application/json",
"X-API-Key": "your_api_key"
},
json={
"merchant_id": "your_merchant_id",
"order_id": order_id,
"amount": amount,
"currency": "VND",
"callback_url": "https://yoursite.com/webhook/rmpay"
},
timeout=10
)
response.raise_for_status()
return response.json()
Successful Response
{
"order_id": "ORD_20260714_001",
"cashier_url": "https://pay.rmpay.io/pay?oid=abc123",
"qr_code": "https://pay.rmpay.io/qr/abc123.png",
"reference": "RMPAY001",
"amount": 500000,
"expires_at": "2026-07-14T10:30:00+07:00",
"status": "pending"
}
Step 2: Redirect Customer to Cashier
Redirect your customer to the cashier_url returned in the response. The RMPay cashier page displays:
- A VietQR code compatible with all 57 Vietnamese banking apps
- Bank transfer details (bank name, account number, reference code)
- A countdown timer showing order expiry
- Instructions in Vietnamese, English, or Chinese
from flask import redirect
@app.route('/checkout/pay', methods=['POST'])
def initiate_payment():
order = create_payment_order(
order_id=generate_unique_order_id(),
amount=int(request.form['amount'])
)
db.orders.update(order['order_id'], status='pending')
return redirect(order['cashier_url'])
Step 3: Handle Webhook Callback
This is the most critical part of the integration. When the customer completes payment, RMPay sends a POST request to your callback_url within seconds.
Webhook Payload
{
"event": "payment.success",
"order_id": "ORD_20260714_001",
"merchant_id": "your_merchant_id",
"amount": 500000,
"currency": "VND",
"bank": "VCB",
"reference": "RMPAY001",
"status": "paid",
"paid_at": "2026-07-14T09:53:02+07:00"
}
Webhook Handler (Python/Flask)
import hmac, hashlib, json
from flask import Flask, request, jsonify
WEBHOOK_SECRET = "your_webhook_secret"
@app.route('/webhook/rmpay', methods=['POST'])
def rmpay_webhook():
sig = request.headers.get('X-RMPay-Signature', '')
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
return jsonify({'error': 'Unauthorized'}), 401
event = request.json
if event['event'] == 'payment.success':
order_id = event['order_id']
amount = event['amount']
order = db.orders.get(order_id)
if order['amount'] != amount:
return jsonify({'error': 'Amount mismatch'}), 400
db.orders.update(order_id, status='paid')
fulfill_order(order_id)
return jsonify({'status': 'ok'}), 200
Step 4: Query Order Status (Fallback)
Always implement a status polling fallback in case your webhook endpoint is temporarily down or a webhook is missed.
def check_order_status(order_id: str) -> str:
response = requests.get(
f"https://api.rmpay.co/v1/order/{order_id}",
headers={"X-API-Key": "your_api_key"},
timeout=10
)
return response.json()['status']
Security Best Practices
Always Verify Webhook Signatures
Never process a webhook without verifying the HMAC signature. Anyone can POST to your webhook endpoint โ only RMPay's signed requests should trigger order fulfillment.
Verify Amount in Webhook
Always cross-check the amount in the webhook against your stored order amount. Never fulfill an order if the amounts don't match exactly.
Idempotent Webhook Handling
Webhooks may occasionally be delivered more than once. Make your handler idempotent โ check if the order is already marked as paid before processing again.
if event['event'] == 'payment.success':
order = db.orders.get(event['order_id'])
if order['status'] == 'paid':
return jsonify({'status': 'already_processed'}), 200
Use HTTPS for Webhook URL
Your webhook endpoint must use HTTPS. RMPay will not deliver webhooks to HTTP endpoints.
Testing Your Integration
Before going live, test thoroughly in the sandbox environment:
- Use sandbox API credentials (provided on merchant onboarding)
- Create test orders with small amounts
- Simulate payment confirmation via the sandbox dashboard
- Verify your webhook handler processes events correctly
- Test error cases: expired orders, amount mismatches, invalid signatures
Going Live Checklist
- โ
Webhook endpoint on HTTPS and publicly accessible
- โ
Signature verification implemented and tested
- โ
Amount verification in webhook handler
- โ
Idempotent webhook handling
- โ
Order status polling as fallback
- โ
Error logging for failed webhooks
- โ
Switch API credentials from sandbox to production
Ready to Integrate Vietnam Payments?
Contact RMPay to receive your API credentials and sandbox access. Most integrations complete in 2โ4 hours. 57 Vietnamese banks, real-time settlement.
Get API Access โ
Related resources:
โ Python SDK on GitHub
โ PHP SDK on GitHub
โ RMPay How It Works
โ Vietnam Bank Transfer Guide