Thanh toán VietQR cho shop online: tạo mã QR theo đơn và tự động xác nhận chuyển khoản
Cách tạo mã VietQR có sẵn số tiền và mã đơn, quy ước nội dung chuyển khoản, tự động xác nhận thanh toán qua webhook của dịch vụ biến động số dư, code mẫu, bảo mật và xử lý các tình huống lệch tiền.

Trong bài này
- 12 phút đọc
- 14 phần
- 3 mẫu sao chép được
- 6 bảng tra cứu
Mục lục
- VietQR là gì, QR tĩnh và QR động khác nhau thế nào
- Tạo mã VietQR động bằng đường link
- Quy ước nội dung chuyển khoản
- Tự động xác nhận thanh toán: luồng xử lý
- Code mẫu nhận webhook
- Chọn dịch vụ nhận biến động số dư
- Ví dụ trọn vẹn một đơn hàng
- Bảo mật: những điều không được bỏ qua
- Xử lý các tình huống lệch
- Trải nghiệm thanh toán cho khách
- So với cổng thanh toán và ví điện tử
- Danh sách kiểm tra trước khi chạy thật
- Câu hỏi thường gặp
- Kết luận
Chuyển khoản bằng mã QR đã trở thành cách thanh toán quen thuộc nhất của người mua hàng online ở Việt Nam: mở ứng dụng ngân hàng, quét mã, xác nhận. Với người bán, phần khó nằm ở phía sau: kiểm tra từng giao dịch trong ứng dụng ngân hàng, đoán xem khoản 350.000 đồng với nội dung “chuyen tien” là của đơn nào, nhắn khách hỏi lại khi họ ghi sai nội dung. Khi có vài chục đơn mỗi ngày, việc đối soát thủ công vừa tốn thời gian vừa dễ nhầm.
Bài này hướng dẫn cách dùng mã VietQR có sẵn số tiền và nội dung cho từng đơn hàng, rồi tự động xác nhận thanh toán khi tiền về tài khoản bằng dịch vụ nhận thông báo biến động số dư qua webhook. Bài có cấu trúc đường link tạo mã VietQR, quy ước đặt nội dung chuyển khoản, sơ đồ luồng xử lý, đoạn code mẫu, các lưu ý bảo mật và các tình huống lệch tiền cần xử lý.
VietQR là gì, QR tĩnh và QR động khác nhau thế nào#
VietQR là chuẩn mã QR cho chuyển khoản liên ngân hàng tại Việt Nam, gắn với hệ thống chuyển tiền nhanh của NAPAS. Mã VietQR quét được bằng ứng dụng của hầu hết ngân hàng trong nước. Có hai cách dùng:
| Loại | Chứa thông tin gì | Ưu điểm | Nhược điểm |
|---|---|---|---|
| QR tĩnh | Ngân hàng và số tài khoản | In một lần, dán ở quầy, dùng mãi | Khách tự nhập số tiền và nội dung, dễ sai |
| QR động | Ngân hàng, số tài khoản, số tiền, nội dung chuyển khoản | Khách chỉ cần xác nhận, đúng tiền, đúng nội dung | Phải tạo riêng cho từng đơn |
Với bán hàng online, QR động gần như luôn tốt hơn. Nội dung chuyển khoản chứa mã đơn hàng là thứ giúp hệ thống tự động khớp tiền với đơn.
Tạo mã VietQR động bằng đường link#
Cách đơn giản nhất để tạo mã QR động là dùng đường link tạo ảnh nhanh của VietQR.io. Theo tài liệu của VietQR.io, cấu trúc như sau:
https://img.vietqr.io/image/<BANK_ID>-<ACCOUNT_NO>-<TEMPLATE>.svg?amount=<AMOUNT>&addInfo=<DESCRIPTION>&accountName=<ACCOUNT_NAME>
| Thành phần | Ý nghĩa | Lưu ý |
|---|---|---|
| BANK_ID | Mã ngân hàng | Dùng mã BIN hoặc tên viết tắt. Ví dụ với VietinBank: 970415, Vietinbank hoặc ICB |
| ACCOUNT_NO | Số tài khoản nhận | Tối đa 19 ký tự chữ hoặc số |
| TEMPLATE | Kiểu ảnh | Ví dụ compact2 (có mã QR, logo và thông tin chuyển khoản), compact, qr_only, print |
| amount | Số tiền | Số dương, tối đa 13 chữ số |
| addInfo | Nội dung chuyển khoản | Tối đa 50 ký tự, không ký tự đặc biệt |
| accountName | Tên người nhận hiển thị trên ảnh | Chỉ để hiển thị, không thuộc chuẩn VietQR |
Tài liệu cũng lưu ý rằng với dự án chính thức, bạn nên tạo mẫu ảnh riêng kế thừa từ các mẫu cơ bản. Ví dụ tạo đường link cho một đơn hàng bằng JavaScript:
function vietqrUrl({ bank, account, amount, orderCode, accountName }) {
const params = new URLSearchParams({
amount: String(amount),
addInfo: orderCode, // ví dụ "MMO12345"
accountName: accountName,
});
return `https://img.vietqr.io/image/${bank}-${account}-compact2.svg?${params}`;
}
// Dùng trong trang thanh toán:
// <img src="${vietqrUrl({ bank: '970415', account: '0123456789', amount: 350000,
// orderCode: 'MMO12345', accountName: 'NGUYEN VAN A' })}" alt="Mã QR thanh toán đơn MMO12345">
Thay số tài khoản ví dụ bằng tài khoản thật của bạn. Dùng hàm URLSearchParams để mã hoá đúng các ký tự trong đường link.
Quy ước nội dung chuyển khoản#
Nội dung chuyển khoản là “chìa khoá” để khớp tiền với đơn. Một quy ước tốt:
- Ngắn: nằm gọn trong giới hạn 50 ký tự, và đủ ngắn để ngân hàng không cắt bớt.
- Không dấu, không ký tự đặc biệt: chỉ chữ cái và số.
- Có tiền tố riêng của shop: ví dụ
MMOtrước mã đơn. Tiền tố giúp phân biệt với các khoản chuyển khoản khác vào cùng tài khoản. - Mã đơn không trùng lặp: mỗi đơn một mã duy nhất, không dùng lại.
- Dễ đọc khi khách phải gõ tay: tránh các ký tự dễ nhầm như O và 0, I và 1, nếu khách có thể phải tự nhập.
Lưu ý thực tế: một số ứng dụng ngân hàng tự thêm chữ vào trước hoặc sau nội dung khách nhập, ví dụ mã giao dịch hay tên người chuyển. Vì vậy hệ thống nên tìm mã đơn trong nội dung bằng mẫu (ví dụ MMO theo sau là các chữ số), không so khớp nguyên văn cả chuỗi.
Tự động xác nhận thanh toán: luồng xử lý#
Ngân hàng không gửi thông báo trực tiếp cho website của bạn. Cầu nối là các dịch vụ nhận biến động số dư (ví dụ SePay, Casso và một số dịch vụ khác): bạn liên kết tài khoản ngân hàng với dịch vụ, dịch vụ theo dõi giao dịch đến và gửi thông tin giao dịch tới website qua webhook. Luồng xử lý:
| Bước | Ai làm | Việc |
|---|---|---|
| 1 | Website | Khách đặt hàng, website tạo mã đơn MMO12345, trạng thái “chờ thanh toán” |
| 2 | Website | Hiển thị mã VietQR động với số tiền và nội dung là mã đơn |
| 3 | Khách | Quét mã bằng ứng dụng ngân hàng, xác nhận chuyển khoản |
| 4 | Dịch vụ biến động số dư | Phát hiện giao dịch đến, gửi webhook tới website |
| 5 | Website | Kiểm tra webhook hợp lệ, tìm mã đơn trong nội dung, so số tiền |
| 6 | Website | Đúng: chuyển đơn sang “đã thanh toán”, gửi xác nhận cho khách. Lệch: đưa vào danh sách cần xử lý tay |
Với website WordPress dùng WooCommerce, một số dịch vụ có sẵn plugin, ví dụ SePay có plugin trên kho plugin của WordPress. Với website tự viết, bạn cần một đường dẫn nhận webhook như ví dụ dưới.
Code mẫu nhận webhook#
Ví dụ dưới đây viết bằng PHP, dùng các trường dữ liệu theo tài liệu lập trình webhook của SePay (transferType, transferAmount, content, referenceCode…). Nếu dùng dịch vụ khác, hãy đổi tên trường theo tài liệu của dịch vụ đó.
<?php
// webhook.php: nhận thông báo giao dịch, xác nhận đơn hàng
$expectedKey = getenv('WEBHOOK_API_KEY');
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!hash_equals('Apikey ' . $expectedKey, $auth)) { // kiểm tra theo cách dịch vụ quy định
http_response_code(401); exit;
}
$tx = json_decode(file_get_contents('php://input'), true);
if (!$tx || ($tx['transferType'] ?? '') !== 'in') { echo json_encode(['success' => true]); exit; }
// 1. Chống xử lý trùng: mỗi giao dịch chỉ xử lý một lần
$ref = $tx['referenceCode'] ?? '';
if ($ref === '' || transaction_already_processed($ref)) { echo json_encode(['success' => true]); exit; }
// 2. Tìm mã đơn trong nội dung chuyển khoản
if (!preg_match('/MMO(\d{4,10})/i', $tx['content'] ?? '', $m)) {
save_unmatched($tx, 'khong tim thay ma don'); echo json_encode(['success' => true]); exit;
}
$order = find_order('MMO' . $m[1]);
// 3. So số tiền
$amount = (int) $tx['transferAmount'];
if (!$order) {
save_unmatched($tx, 'ma don khong ton tai');
} elseif ($amount === $order['total']) {
mark_order_paid($order['id'], $ref);
} else {
save_unmatched($tx, $amount < $order['total'] ? 'chuyen thieu' : 'chuyen thua');
}
mark_transaction_processed($ref);
echo json_encode(['success' => true]);
Các hàm như find_order, mark_order_paid, save_unmatched là phần bạn tự viết theo cơ sở dữ liệu của mình. Ba nguyên tắc trong đoạn code quan trọng hơn chi tiết cú pháp: xác thực nguồn gửi, chống xử lý trùng, và không tự động xác nhận khi có bất kỳ điểm nào lệch.
Chọn dịch vụ nhận biến động số dư#
Có nhiều dịch vụ làm cầu nối giữa ngân hàng và website. Thay vì so sánh theo tên, hãy so theo các tiêu chí sau:
| Tiêu chí | Câu hỏi cần trả lời |
|---|---|
| Ngân hàng hỗ trợ | Ngân hàng bạn đang dùng có trong danh sách không? Kết nối theo cách nào? |
| Độ trễ | Từ lúc tiền về đến lúc webhook đến mất bao lâu, trong giờ cao điểm thì sao? |
| Xác thực webhook | Có hỗ trợ khoá bí mật, danh sách IP, chữ ký không? |
| Gửi lại khi lỗi | Nếu website của bạn tạm lỗi, dịch vụ có gửi lại không, gửi lại bao nhiêu lần? |
| Tích hợp sẵn | Có plugin cho WooCommerce, có thư viện cho ngôn ngữ bạn dùng, có gửi thông báo vào nhóm chat không? |
| Chi phí | Tính theo số giao dịch, theo số tài khoản hay theo gói tháng? Vượt gói thì sao? |
| Bảo mật dữ liệu | Dịch vụ lưu những dữ liệu gì, lưu bao lâu, chính sách bảo mật ra sao? |
Hãy dùng thử với số lượng giao dịch nhỏ trước khi chuyển toàn bộ đơn hàng sang. Tiêu chí “gửi lại khi lỗi” thường bị bỏ qua nhưng rất quan trọng: nếu website của bạn bảo trì 10 phút đúng lúc khách thanh toán, bạn cần biết giao dịch đó có được gửi lại không.
Ví dụ trọn vẹn một đơn hàng#
Để hình dung toàn bộ quy trình, đây là hành trình của một đơn hàng 350.000 đồng:
- Khách bấm “Đặt hàng”. Website tạo đơn mã
MMO12345, trạng thái “chờ thanh toán”, giữ hàng trong 30 phút. - Trang thanh toán hiện mã VietQR với số tiền 350.000 đồng và nội dung
MMO12345, kèm nút sao chép số tài khoản và nội dung. - Khách quét mã bằng ứng dụng ngân hàng, thấy sẵn số tiền và nội dung, chỉ cần xác nhận.
- Vài giây đến vài phút sau, dịch vụ biến động số dư gửi webhook. Nội dung đến website là
MMO12345 FT26312xxxxx, ngân hàng đã tự thêm mã giao dịch phía sau. - Website kiểm tra khoá bí mật, thấy mã tham chiếu chưa từng xử lý, tìm được
MMO12345trong nội dung, số tiền khớp 350.000 đồng. - Đơn chuyển sang “đã thanh toán”. Trang thanh toán của khách tự chuyển sang “Đã nhận thanh toán”. Khách nhận email hoặc tin nhắn xác nhận. Kho nhận lệnh đóng gói.
Không ai trong đội bán hàng phải mở ứng dụng ngân hàng, không khách nào phải gửi ảnh chụp màn hình. Người phụ trách chỉ xem danh sách giao dịch chưa khớp vào cuối ngày.
Bảo mật: những điều không được bỏ qua#
- Xác thực webhook. Đường dẫn nhận webhook là công khai. Nếu không xác thực, bất kỳ ai cũng có thể gửi một yêu cầu giả để “xác nhận” đơn chưa thanh toán. Hãy bật xác thực bằng khoá bí mật (API key) theo cách dịch vụ hỗ trợ, và có thể giới hạn địa chỉ IP gửi đến nếu dịch vụ công bố danh sách IP.
- Chống xử lý trùng. Dịch vụ có thể gửi lại cùng một giao dịch nhiều lần khi mạng chập chờn. Lưu mã tham chiếu giao dịch và bỏ qua giao dịch đã xử lý.
- So số tiền chính xác. Không xác nhận đơn chỉ vì nội dung có mã đơn. Kẻ gian có thể chuyển 1.000 đồng với đúng nội dung.
- Giữ khoá bí mật ngoài mã nguồn. Dùng biến môi trường, không để khoá trong code đưa lên kho mã công khai.
- Dùng HTTPS cho đường dẫn nhận webhook.
- Cân nhắc quyền truy cập dữ liệu. Dịch vụ biến động số dư được đọc thông tin giao dịch của tài khoản. Hãy đọc kỹ điều khoản, chính sách bảo mật, và cân nhắc dùng một tài khoản riêng chỉ để nhận tiền bán hàng.
Xử lý các tình huống lệch#
| Tình huống | Cách xử lý đề xuất |
|---|---|
| Khách chuyển thiếu | Giữ đơn ở trạng thái chờ, nhắn khách chuyển bổ sung với cùng nội dung, hoặc liên hệ hoàn tiền nếu khách muốn huỷ |
| Khách chuyển thừa | Xác nhận đơn sau khi người phụ trách kiểm tra, liên hệ khách để hoàn phần thừa |
| Khách ghi sai hoặc không ghi nội dung | Đưa vào danh sách chờ khớp tay. Hiển thị trên trang thanh toán dòng nhắc: “Vui lòng giữ nguyên nội dung chuyển khoản” |
| Khách chuyển hai lần cho một đơn | Hệ thống chỉ xác nhận một lần, lần thứ hai vào danh sách cần hoàn |
| Khách chuyển sau khi đơn đã huỷ vì quá hạn | Khôi phục đơn nếu còn hàng, hoặc hoàn tiền |
Một danh sách “giao dịch chưa khớp” được người phụ trách xem mỗi ngày là đủ để xử lý những trường hợp này. Mục tiêu của tự động hoá không phải là không còn việc tay, mà là chỉ còn việc tay ở những trường hợp thật sự bất thường.
Trải nghiệm thanh toán cho khách#
- Hiển thị rõ: mã QR lớn, số tiền, tên người nhận, nội dung chuyển khoản, có nút sao chép từng dòng cho khách thanh toán trên cùng điện thoại.
- Hướng dẫn cho người dùng điện thoại: khách đang xem trên điện thoại không quét được mã trên chính màn hình đó. Gợi ý: lưu ảnh mã QR rồi chọn ảnh trong ứng dụng ngân hàng, hoặc sao chép số tài khoản và nội dung.
- Tự cập nhật trạng thái: trang thanh toán kiểm tra trạng thái đơn mỗi vài giây và tự chuyển sang trang “đã nhận thanh toán” khi webhook đến. Khách không phải gửi ảnh chụp màn hình.
- Thời hạn thanh toán: hiển thị thời gian giữ đơn, ví dụ 30 phút, để khách biết và để kho không giữ hàng quá lâu.
Điểm cuối cùng có tác dụng phụ quan trọng: khi hệ thống tự xác nhận từ dữ liệu ngân hàng, bạn không còn phải dựa vào ảnh chụp màn hình chuyển khoản của khách. Ảnh chụp có thể bị làm giả, xem thêm bài chống lừa đảo thanh toán cho shop online.
So với cổng thanh toán và ví điện tử#
| Tiêu chí | VietQR và webhook | Cổng thanh toán, ví điện tử |
|---|---|---|
| Chi phí | Phí dịch vụ biến động số dư (theo gói), chuyển khoản liên ngân hàng thường miễn phí với khách | Phí giao dịch theo phần trăm hoặc cố định |
| Phương thức | Chỉ chuyển khoản ngân hàng | Thẻ nội địa, thẻ quốc tế, ví, trả sau tuỳ cổng |
| Tiền về | Thẳng vào tài khoản của bạn ngay khi khách chuyển | Qua cổng, đối soát và chuyển về theo chu kỳ |
| Hoàn tiền | Tự chuyển lại cho khách | Có quy trình hoàn qua cổng |
| Hợp với | Shop nhỏ và vừa, khách trong nước | Shop cần nhận thẻ quốc tế, ví, hoặc cần quy trình hoàn chuẩn |
Hai cách không loại trừ nhau. Nhiều website cho khách chọn: chuyển khoản QR (thường kèm ưu đãi nhỏ vì chi phí thấp hơn) hoặc thanh toán qua cổng.
Danh sách kiểm tra trước khi chạy thật#
- Thử chuyển khoản với số tiền nhỏ từ ít nhất hai ngân hàng khác nhau, kiểm tra nội dung đến webhook có giữ nguyên mã đơn không.
- Gửi thử một webhook giả không có khoá bí mật, đảm bảo bị từ chối.
- Gửi lại cùng một webhook hai lần, đảm bảo đơn chỉ được xác nhận một lần.
- Chuyển thiếu, chuyển thừa, chuyển sai nội dung, kiểm tra giao dịch vào danh sách chưa khớp.
- Kiểm tra trang thanh toán trên điện thoại: có nút sao chép, có hướng dẫn lưu ảnh QR.
- Phân công người xem danh sách giao dịch chưa khớp mỗi ngày.
Câu hỏi thường gặp#
Có cần tài khoản ngân hàng doanh nghiệp không?
Không bắt buộc về mặt kỹ thuật, các dịch vụ thường hỗ trợ cả tài khoản cá nhân. Nhưng nên dùng một tài khoản riêng cho kinh doanh, tách khỏi chi tiêu cá nhân, để dễ đối soát và dễ tổng hợp doanh thu khi kê khai thuế.
Webhook đến chậm thì sao?
Thời gian phụ thuộc vào dịch vụ và ngân hàng. Hãy hiển thị cho khách thông điệp “hệ thống đang chờ xác nhận, thường mất vài phút”, và có kênh liên hệ nếu quá thời gian. Đừng yêu cầu khách chuyển lại.
Có dùng được cho bán hàng qua fanpage, Zalo không?
Được. Bạn có thể gửi cho khách đường link ảnh VietQR với số tiền và mã đơn, hoặc dùng phần mềm quản lý đơn có tích hợp sẵn. Khi tiền về, dịch vụ biến động số dư có thể gửi thông báo vào nhóm chat của đội bán hàng thay vì website.
Kết luận#
Mã VietQR động cho từng đơn cộng với dịch vụ nhận biến động số dư qua webhook biến chuyển khoản ngân hàng, cách thanh toán rẻ và phổ biến nhất, thành một quy trình gần như tự động. Phần quan trọng không nằm ở việc tạo mã QR, mà ở ba nguyên tắc phía sau: xác thực webhook, chống xử lý trùng và không tự xác nhận khi có bất kỳ điểm nào lệch. Làm đúng ba điều đó, bạn vừa tiết kiệm thời gian đối soát, vừa đóng lại một kẽ hở lừa đảo phổ biến.
Nguồn: VietQR.io: Link tạo mã nhanh; SePay: Hướng dẫn lập trình webhooks; SePay Gateway trên WordPress.org.


