mmo.vn
E-commerce & thanh toán12 phút đọc

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.

Thumbnail bài thanh toán VietQR tự động xác nhận chuyển khoả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
  1. VietQR là gì, QR tĩnh và QR động khác nhau thế nào
  2. Tạo mã VietQR động bằng đường link
  3. Quy ước nội dung chuyển khoản
  4. Tự động xác nhận thanh toán: luồng xử lý
  5. Code mẫu nhận webhook
  6. Chọn dịch vụ nhận biến động số dư
  7. Ví dụ trọn vẹn một đơn hàng
  8. Bảo mật: những điều không được bỏ qua
  9. Xử lý các tình huống lệch
  10. Trải nghiệm thanh toán cho khách
  11. So với cổng thanh toán và ví điện tử
  12. Danh sách kiểm tra trước khi chạy thật
  13. Câu hỏi thường gặp
  14. 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ạiChứa thông tin gìƯu điểmNhược điểm
QR tĩnhNgân hàng và số tài khoảnIn một lần, dán ở quầy, dùng mãiKhách tự nhập số tiền và nội dung, dễ sai
QR độngNgân hàng, số tài khoản, số tiền, nội dung chuyển khoảnKhách chỉ cần xác nhận, đúng tiền, đúng nội dungPhả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.

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ĩaLưu ý
BANK_IDMã ngân hàngDùng mã BIN hoặc tên viết tắt. Ví dụ với VietinBank: 970415, Vietinbank hoặc ICB
ACCOUNT_NOSố tài khoản nhậnTối đa 19 ký tự chữ hoặc số
TEMPLATEKiểu ảnhVí dụ compact2 (có mã QR, logo và thông tin chuyển khoản), compact, qr_only, print
amountSố tiềnSố dương, tối đa 13 chữ số
addInfoNội dung chuyển khoảnTối đa 50 ký tự, không ký tự đặc biệt
accountNameTên người nhận hiển thị trên ảnhChỉ để 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ụ MMO trướ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ướcAi làmViệc
1WebsiteKhách đặt hàng, website tạo mã đơn MMO12345, trạng thái “chờ thanh toán”
2WebsiteHiển thị mã VietQR động với số tiền và nội dung là mã đơn
3KháchQuét mã bằng ứng dụng ngân hàng, xác nhận chuyển khoản
4Dịch vụ biến động số dưPhát hiện giao dịch đến, gửi webhook tới website
5WebsiteKiểm tra webhook hợp lệ, tìm mã đơn trong nội dung, so số tiền
6WebsiteĐú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 webhookCó hỗ trợ khoá bí mật, danh sách IP, chữ ký không?
Gửi lại khi lỗiNế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ẵnCó 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ệuDị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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Website kiểm tra khoá bí mật, thấy mã tham chiếu chưa từng xử lý, tìm được MMO12345 trong nội dung, số tiền khớp 350.000 đồng.
  6. Đơ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ốngCách xử lý đề xuất
Khách chuyển thiếuGiữ đơ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ừaXá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 đơnHệ 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ạnKhô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à webhookCổ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áchPhí giao dịch theo phần trăm hoặc cố định
Phương thứcChỉ chuyển khoản ngân hàngThẻ 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ểnQua cổng, đối soát và chuyển về theo chu kỳ
Hoàn tiềnTự chuyển lại cho kháchCó quy trình hoàn qua cổng
Hợp vớiShop nhỏ và vừa, khách trong nướcShop 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#

  1. 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.
  2. Gửi thử một webhook giả không có khoá bí mật, đảm bảo bị từ chối.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

Mở kho mẫu sao chép