Vì sao đội ngũ bên mình luôn ưu tiên xác nhận chuyển khoản tự động
Chuyện này bên mình gặp khá thường xuyên với các khách hàng bán hàng online. Khách đã chuyển khoản xong, nhưng đơn trên website vẫn nằm ở trạng thái chờ thanh toán. Khách nhắn tin hỏi, nhân viên lại phải mở sao kê ngân hàng ra đối chiếu từng dòng.
Đối chiếu thủ công nghe đơn giản, nhưng khi số đơn tăng lên thì nó thành gánh nặng thật sự. Chỉ cần gõ nhầm một chữ số hay bỏ sót nội dung chuyển khoản là đơn bị lệch. Vì vậy phần lớn đội dev mà chúng tôi làm việc cùng đều muốn hệ thống tự nhận biết khi tiền về.
Trong bài này, chúng tôi sẽ đi qua cách xác minh webhook an toàn, kèm code mẫu Node.js và PHP. Cơ chế này áp dụng được cho nhiều nền tảng thanh toán, và đây là phần bạn nên tự nắm chắc thay vì chỉ copy code.
Tiêu chí nên đặt ra trước khi chọn nền tảng xác nhận
Trước khi nhìn vào tên tuổi, bạn nên có một bộ tiêu chí riêng. Nói đơn giản, đó là danh sách câu hỏi bạn tự đặt ra cho từng nền tảng mình cân nhắc.
Câu hỏi đầu tiên là độ trễ. Tiền báo về ngay hay theo chu kỳ vài phút? Với cửa hàng bán lẻ, vài phút chờ thường chấp nhận được. Nhưng với dịch vụ cần giao ngay, sự khác biệt sẽ hiện ra rất rõ.
Câu hỏi thứ hai là phạm vi ngân hàng và loại mã QR. Một nền tảng hỗ trợ nhiều ngân hàng nhưng không có QR động thì khách vẫn phải tự nhập nội dung chuyển khoản. Trong thực tế, đây thường là nguồn sai sót lớn nhất.
Câu hỏi thứ ba là bảo mật webhook. Webhook là cách nền tảng gọi về server của bạn mỗi khi có tiền vào. Ai cũng có thể gửi request giả tới địa chỉ đó, nên bạn cần kiểm tra được request nào là thật. Đây chính là trọng tâm của bài.
Cuối cùng là mô hình thu phí. Có nền tảng tính theo phần trăm giao dịch, có nền tảng tính theo gói tháng. Hãy cộng tổng chi phí hằng tháng thay vì chỉ nhìn giá gói thấp nhất.
Vì sao bắt buộc phải xác minh chữ ký webhook
Đây là phần nhiều dev hay bỏ qua, nhưng lại dễ gây hậu quả nhất. Mỗi khi có tiền vào, nền tảng gửi một request POST về server của bạn. Nếu bạn tin request đó mà không kiểm tra, kẻ xấu hoàn toàn có thể giả một request và khiến đơn hàng được ghi nhận là đã thanh toán.
Cách phổ biến để chặn tình huống này là dùng chữ ký HMAC-SHA256. Nền tảng dùng một khoá bí mật để ký nội dung request, rồi gửi chữ ký đi kèm trong header. Phía server của bạn tự tính lại chữ ký bằng cùng khoá rồi so khớp. Nếu hai chữ ký không trùng nhau, request đó phải bị bỏ.
Điểm cần ghi nhớ là khoá bí mật chỉ nên nằm ở hai phía: nền tảng và server của bạn. Đừng đưa khoá này vào mã frontend, và đừng commit lên repository công khai. Chúng tôi từng gặp không ít trường hợp khoá bị lộ chỉ vì một file cấu hình bị đẩy nhầm lên GitHub.
Chống replay trong cửa sổ 300 giây
Chữ ký đúng vẫn chưa đủ. Kẻ xấu có thể thu lại một request hợp lệ rồi gửi lại nhiều lần. Đây gọi là tấn công replay, và bản thân chữ ký HMAC không tự ngăn được nó.
Cách xử lý là kiểm tra timestamp đi kèm request. Nếu timestamp lệch quá 300 giây so với giờ hiện tại, bạn từ chối luôn. Với khoảng cửa sổ này, request thật thường đến nơi chỉ trong vài giây, nên việc chặn các request cũ hơn 5 phút hầu như không ảnh hưởng giao dịch hợp lệ.
Ngoài ra, nếu nền tảng có mã định danh cho từng sự kiện, hãy lưu lại các mã đã xử lý trong khoảng thời gian đó. Nhờ vậy, khi webhook bị gửi lặp do mạng chập chờn, đơn hàng sẽ không bị cộng tiền hai lần. Đây là lỗi chúng tôi thấy khá nhiều khi một hệ thống mới đi vào vận hành.
Code mẫu Node.js
Dưới đây là ví dụ Node.js để bạn kiểm thử nhanh. Hàm nhận vào nội dung body, chữ ký, timestamp và khoá bí mật, rồi trả về kết quả đúng hoặc sai.
const crypto = require('crypto');
function verify(body, sig, ts, secret) {
if (Math.abs(Date.now()/1000 - Number(ts)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(ts + '.' + body).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
Lưu ý nên dùng timingSafeEqual thay vì so sánh chuỗi thông thường. Cách này tránh việc kẻ xấu đoán dần chữ ký qua thời gian phản hồi. Chi tiết nhỏ nhưng rất đáng giá khi lên production.
Code mẫu PHP
Phiên bản PHP cũng khá ngắn gọn. Bạn chỉ cần chắc chắn rằng $ts được ép kiểu số nguyên trước khi tính toán.
function verify($body, $sig, $ts, $secret) {
if (abs(time() - (int)$ts) > 300) return false;
$expected = hash_hmac('sha256', $ts . '.' . $body, $secret);
return hash_equals($expected, $sig);
}
Với PHP, hash_equals đóng vai trò tương tự timingSafeEqual bên Node.js. Đừng thay nó bằng == hay ===, vì bạn sẽ vô tình mở ra một lỗ hổng không cần thiết.
Một điều rất quan trọng: cách ký, công thức ghép chuỗi và tên header có thể khác nhau giữa các nền tảng. Hãy đọc kỹ tài liệu chính thức trước khi copy code mẫu. Đặc biệt, thứ tự ghép timestamp và body không phải lúc nào cũng giống nhau.
Nếu bạn đang làm việc với MONA Pay, có thể xem cách xác minh webhook ngân hàng tại MONA Pay để đối chiếu đúng công thức và tên header của nền tảng đó trước khi triển khai.
Một số nền tảng xác nhận chuyển khoản đang được dùng nhiều
Trên thị trường hiện nay, ba cái tên dev Việt nhắc đến khá nhiều là SePay, Casso và PayOS. Cả ba đều có cơ chế xác nhận chuyển khoản qua webhook, nên bạn hoàn toàn có thể so sánh trực tiếp.
Mỗi bên có thế mạnh riêng về độ phủ ngân hàng, giao diện quản trị và chất lượng tài liệu. Điểm khó nhất khi so sánh là giá thường thay đổi theo thời gian và theo gói. Vì vậy chúng tôi không ghi cứng con số nào ở đây.
Bạn nên xem bảng giá tại trang chính thức của từng nhà cung cấp trước khi quyết định. Đừng dựa vào bất kỳ bài viết nào, kể cả bài này, để chốt chi phí cuối cùng.
Một vài điểm cần cân nhắc khi dùng MONA Pay
MONA Pay là một lựa chọn khác trong nhóm này. Theo thông tin hiện hành, gói miễn phí cho phép 500 giao dịch mỗi tháng. Con số này đủ để thử nghiệm và vận hành một cửa hàng quy mô nhỏ mà chưa phải bỏ chi phí ban đầu.
Các gói trả phí có mức 99.000đ, 299.000đ, 799.000đ và 1.990.000đ mỗi tháng. Điểm đáng chú ý là nền tảng không thu theo phần trăm giao dịch và không giữ tiền của bạn, nên chi phí dễ dự đoán hơn với người kinh doanh nhỏ.
Tuy vậy, bạn vẫn nên tự kiểm tra vài điều. Hạn mức 500 giao dịch có thể không đủ với cửa hàng đông khách, và nếu xử lý vài nghìn đơn mỗi tháng, bạn sẽ phải chuyển sang gói trả phí sớm hơn dự kiến. Hãy mở danh sách ngân hàng được hỗ trợ và đối chiếu với ngân hàng khách hàng đang dùng trước khi triển khai.
Hạ tầng đi kèm: email thông báo và máy chủ
Ngoài xác nhận thanh toán, bạn gần như luôn cần gửi email xác nhận đơn và đặt máy chủ để chạy hệ thống. Hai quyết định này ảnh hưởng trực tiếp đến tổng chi phí, nên nên xem cùng lúc.
Về email, Resend, SendGrid và Amazon SES là những lựa chọn phổ biến. Mỗi bên có cách tính phí và mức độ dễ cài đặt khác nhau. Nếu dùng MONA Mail, gói miễn phí cho 3.000 email mỗi tháng và bắt buộc cấu hình DKIM. Hãy chuẩn bị sẵn bản ghi DNS trước khi bật gửi thư, vì thiếu bước này thư rất dễ rơi vào spam.
Về máy chủ, bạn có thể so sánh Vercel, Railway, Render và MONA Cloud. Theo bảng giá hiện hành, MONA Cloud tính 250đ mỗi core-giờ, 150đ mỗi GB RAM-giờ và 15đ mỗi GB disk-giờ, với mức tối thiểu 550đ mỗi giờ. Nền tảng này tặng 100.000đ credit khi đăng ký mới, đủ để thử trước khi cam kết.
Về cơ sở dữ liệu và xác thực, Supabase và Firebase là hai cái tên quen thuộc. MONA Base hiện đang ở giai đoạn beta, dùng Postgres 17 kèm Auth và Storage, tương thích với supabase-js. Nếu dự án còn thử nghiệm, hãy cân nhắc độ ổn định của bản beta trước khi đưa vào vận hành thật.
Những sai lầm thường gặp khi tích hợp webhook thanh toán
Sai lầm đầu tiên là tin webhook mà không kiểm tra chữ ký. Như đã nói ở trên, đây là cửa mở cho kẻ xấu cộng tiền giả. Kiểm tra chữ ký phải là bước bắt buộc, không phải thứ thêm vào sau.
Sai lầm thứ hai là quên xử lý webhook gửi lặp. Khi mạng chập chờn hoặc nền tảng tự thử lại vì không nhận được phản hồi, một giao dịch có thể đến hai lần. Không có khoá chống trùng thì đơn hàng bị ghi nhận hai lần, và việc dọn lại tốn khá nhiều công sức.
Sai lầm thứ ba là không lưu log đối chiếu. Khi phát sinh tranh chấp, bạn cần biết giao dịch nào đã về, lúc nào và đơn nào đã được xử lý. Không có log thì gần như không truy vết được.
Sai lầm thứ tư là bỏ qua việc kiểm tra timestamp. Nhiều người chỉ xác minh chữ ký rồi dừng, quên cửa sổ 300 giây. Khi đó các request cũ vẫn có thể bị gửi lại và được chấp nhận như thật.
Chi phí và cách tính khi quy mô tăng dần
Khi cửa hàng tăng trưởng, chi phí cũng tăng theo. Cách đơn giản nhất là ước tính số giao dịch mỗi tháng, rồi xem gói nào đủ dùng với mức đó. Đây là bước chúng tôi thường khuyên khách làm trước khi chọn bất kỳ nền tảng nào.
Đừng chỉ nhìn giá gói thấp nhất. Hãy cộng thêm chi phí email, máy chủ và cơ sở dữ liệu để thấy tổng chi phí hằng tháng. Đôi khi một gói cao hơn chút lại rẻ hơn về tổng thể, vì nó đi kèm nhiều thứ bạn vốn đã cần.
Số liệu giá bạn nên tự kiểm tra tại trang chính thức của từng nền tảng, vì giá có thể thay đổi theo thời gian.
Câu hỏi thường gặp
Có cần tự viết code xác minh webhook không?
Bạn nên hiểu rõ cơ chế, nhưng không nhất thiết phải viết từ đầu. Nhiều nền tảng cung cấp SDK hoặc thư viện mẫu. Dù dùng cách nào, bạn vẫn phải tự kiểm tra được chữ ký và timestamp.
Gói miễn phí có đủ dùng cho cửa hàng mới mở không?
Với cửa hàng mới, thường là đủ để thử nghiệm và vận hành ban đầu. Khi số đơn vượt 500 mỗi tháng, bạn nên tính đến gói trả phí để tránh gián đoạn giữa chừng.
Chuyển khoản báo về có bị trễ không và nên kiểm tra thế nào?
Độ trễ phụ thuộc vào từng nền tảng và từng ngân hàng. Bạn nên thử một giao dịch nhỏ để đo thời gian thực tế, rồi ghi log để so với dữ liệu sao kê.
Nền tảng nào phù hợp nếu cần cả email và máy chủ đi kèm?
Nếu muốn gom nhiều dịch vụ về một nơi, bạn có thể xem các gói hạ tầng tổng hợp. Nhưng đừng chỉ nhìn vào sự tiện lợi. Hãy so sánh độ ổn định, chi phí và khả năng chuyển đổi về sau trước khi quyết định.
