Bỏ qua menu, vào nội dung chính

Sandbox VNPay và tích hợp VNPay vào website Next.js: từng bước từ thẻ test đến go-live

Sandbox VNPay là môi trường thử của VNPAY: đăng ký tài khoản test, nhận TmnCode, HashSecret, thanh toán bằng thẻ test và kiểm IPN trước khi chạy thật. Bài đi qua cả quy trình với code Next.js App Router: ký HMAC SHA512, Return URL, IPN, mã phản hồi và các lỗi hay gặp.

Cập nhật 25/09/202615 phút đọc

Sandbox VNPay là môi trường thử nghiệm của cổng thanh toán VNPAY, chạy tại địa chỉ sandbox.vnpayment.vn. Doanh nghiệp đăng ký tài khoản test, nhận mã website (vnp_TmnCode) và chuỗi bí mật (vnp_HashSecret), rồi cho website gửi giao dịch sang https://sandbox.vnpayment.vn/paymentv2/vpcpay.html và thanh toán bằng thẻ test do VNPAY công bố. Tiền không bị trừ thật. Khi mọi kịch bản chạy đúng, kể cả IPN, mới xin VNPAY cấp thông số môi trường thật.

1. Khi nào dùng VNPay (so với MoMo, ZaloPay, Stripe)

  • Khách chủ yếu trong nước và hay trả bằng thẻ ATM nội địa hoặc ứng dụng ngân hàng. Danh sách ngân hàng hỗ trợ được VNPAY công bố tại sandbox.vnpayment.vn/apis/danh-sach-ngan-hang.
  • Muốn một cổng nhận cả thẻ nội địa, thẻ quốc tế và mã VNPAY-QR trên cùng một luồng.
  • Khách chủ yếu dùng ví MoMo thì nên tích hợp thêm MoMo, không thay VNPay mà chạy song song.

Phí giao dịch do VNPAY báo theo hợp đồng từng doanh nghiệp, nên bài này không nêu con số. Stripe hiện không mở tài khoản cho pháp nhân Việt Nam, vì vậy không phải lựa chọn cho doanh nghiệp bán trong nước.

2. Bước 1: Đăng ký tài khoản sandbox VNPay

Vào trang đăng ký môi trường thử sandbox.vnpayment.vn/devreg, điền thông tin website và email. VNPAY gửi về email thông số test gồm vnp_TmnCode (mã website của bạn trên hệ thống VNPAY) và vnp_HashSecret (khoá dùng tạo chữ ký). Bước này không cần giấy tờ doanh nghiệp. Giấy phép kinh doanh và tài khoản ngân hàng chỉ cần khi xin chạy thật.

3. Bước 2: Cài đặt env + cấu hình

Trong Next.js project, thêm .env.local:

envVNPAY_TMN_CODE=YOUR_TMN_CODE_FROM_SANDBOX
VNPAY_HASH_SECRET=YOUR_HASH_SECRET_FROM_SANDBOX
VNPAY_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
VNPAY_RETURN_URL=https://yoursite.vn/payment/return
VNPAY_IPN_URL=https://yoursite.vn/api/vnpay/ipn

VNPAY_URL cho production: vnpayment.vn/paymentv2/vpcpay.html (bỏ "sandbox.").

4. Bước 3: Tạo URL payment

Tạo file src/app/api/vnpay/create/route.ts:

tsimport crypto from 'crypto';
import { NextResponse } from 'next/server';

function sortObject(obj: Record<string, string>) {
  return Object.keys(obj).sort().reduce<Record<string, string>>((acc, k) => {
    acc[k] = encodeURIComponent(obj[k]).replace(/%20/g, '+');
    return acc;
  }, {});
}

export async function POST(req: Request) {
  const { amount, orderId, orderInfo } = await req.json();
  // VNPAY yeu cau gio Viet Nam (GMT+7), dang yyyyMMddHHmmss, ke ca khi server chay UTC
  const vn = (d: Date) => new Date(d.getTime() + 7 * 3600 * 1000).toISOString().replace(/[-:T]/g, '').slice(0, 14);
  const now = new Date();
  const createDate = vn(now);
  const expireDate = vn(new Date(now.getTime() + 15 * 60 * 1000)); // het han sau 15 phut

  const params: Record<string, string> = {
    vnp_Version: '2.1.0',
    vnp_Command: 'pay',
    vnp_TmnCode: process.env.VNPAY_TMN_CODE!,
    vnp_Amount: String(amount * 100),  // VNPay yêu cầu * 100
    vnp_CurrCode: 'VND',
    vnp_TxnRef: orderId,
    vnp_OrderInfo: orderInfo,
    vnp_OrderType: 'other',
    vnp_Locale: 'vn',
    vnp_ReturnUrl: process.env.VNPAY_RETURN_URL!,
    vnp_IpAddr: req.headers.get('x-forwarded-for') || '127.0.0.1',
    vnp_CreateDate: createDate,
    vnp_ExpireDate: expireDate,
  };

  const sorted = sortObject(params);
  const signData = Object.entries(sorted).map(([k, v]) => `${k}=${v}`).join('&');
  const hmac = crypto.createHmac('sha512', process.env.VNPAY_HASH_SECRET!);
  const signed = hmac.update(Buffer.from(signData, 'utf-8')).digest('hex');

  const paymentUrl = `${process.env.VNPAY_URL}?${signData}&vnp_SecureHash=${signed}`;
  return NextResponse.json({ url: paymentUrl });
}
Bảng tham số tạo URL thanh toán VNPAY, vnp_ExpireDate và vnp_SecureHash đều bắt buộc
Tài liệu Thanh toán PAY của VNPAY: vnp_ExpireDate là tham số bắt buộc, giờ GMT+7, định dạng yyyyMMddHHmmss; vnp_SecureHash là mã kiểm tra, phiên bản hiện tại hỗ trợ SHA256 và HMACSHA512. Ảnh chụp bằng Playwright, 25/09/2026.

Muốn đối chiếu chữ ký bằng tay khi gỡ lỗi, dán chuỗi ký và HashSecret sandbox vào công cụ Hash Generator (chế độ HMAC SHA-512) ở /lab/hash, rồi so với vnp_SecureHash trong URL. Chỉ làm vậy với khoá sandbox, không dán khoá thật vào bất kỳ công cụ nào.

5. Bước 4: Handle Return URL (frontend redirect)

Sau khi user thanh toán xong, VNPay redirect về VNPAY_RETURN_URL với query params chứa kết quả. Tạo src/app/payment/return/page.tsx:

tsx// Verify signature lần nữa ở client để hiện success/fail page
export default function ReturnPage({ searchParams }: { searchParams: Record<string, string> }) {
  const responseCode = searchParams.vnp_ResponseCode;
  const ok = responseCode === '00';
  return (
    <div>
      {ok ? <h1>Thanh toán thành công</h1> : <h1>Thanh toán thất bại</h1>}
      <p>Mã giao dịch: {searchParams.vnp_TxnRef}</p>
      <p>Số tiền: {Number(searchParams.vnp_Amount) / 100} VND</p>
    </div>
  );
}

6. Bước 5: Handle IPN Callback (verify thật)

Tạo src/app/api/vnpay/ipn/route.ts:

tsimport crypto from 'crypto';

export async function GET(req: Request) {
  const url = new URL(req.url);
  const params: Record<string, string> = {};
  url.searchParams.forEach((v, k) => { if (k !== 'vnp_SecureHash' && k !== 'vnp_SecureHashType') params[k] = v; });
  const secureHash = url.searchParams.get('vnp_SecureHash');

  const sorted = Object.keys(params).sort().reduce<Record<string, string>>((a, k) => { a[k] = encodeURIComponent(params[k]).replace(/%20/g, '+'); return a; }, {});
  const signData = Object.entries(sorted).map(([k, v]) => `${k}=${v}`).join('&');
  const hmac = crypto.createHmac('sha512', process.env.VNPAY_HASH_SECRET!);
  const computed = hmac.update(Buffer.from(signData, 'utf-8')).digest('hex');

  if (computed !== secureHash) {
    return new Response(JSON.stringify({ RspCode: '97', Message: 'Invalid signature' }));
  }

  const orderId = params.vnp_TxnRef;
  const order = await db.order.findUnique({ where: { id: orderId } }); // thay bang ORM cua ban
  if (!order) return Response.json({ RspCode: '01', Message: 'Order not found' });
  if (order.amount * 100 !== Number(params.vnp_Amount)) return Response.json({ RspCode: '04', Message: 'Invalid amount' });
  if (order.status !== 'pending') return Response.json({ RspCode: '02', Message: 'Order already confirmed' });

  const ok = params.vnp_ResponseCode === '00' && params.vnp_TransactionStatus === '00';
  await db.order.update({ where: { id: orderId }, data: { status: ok ? 'paid' : 'failed' } });

  return Response.json({ RspCode: '00', Message: 'Confirm Success' });
}

Theo tài liệu của VNPAY, khi website trả RspCode 00 hoặc 02, VNPAY coi như đã ghi nhận xong và dừng gọi lại. Trả 01, 04, 97 hoặc 99 thì VNPAY kích hoạt cơ chế gọi lại IPN, tối đa 10 lần, mỗi lần cách 5 phút. Vì vậy đơn đã xử lý phải trả 02, không được xử lý lần hai, và máy chủ phải ghi nhật ký mọi lần IPN tới.

Mục Cài đặt Code IPN URL trong tài liệu VNPAY: phương thức GET, IPN URL cần có SSL, phản hồi RspCode và Message
Mục Cài đặt Code IPN URL trong tài liệu VNPAY: phương thức GET, IPN URL cần có SSL, website phải kiểm dữ liệu, cập nhật kết quả và trả RspCode kèm Message cho máy chủ VNPAY. Ảnh chụp bằng Playwright, 25/09/2026.

Bên cạnh kiểm chữ ký, nên kiểm cả kiểu và giới hạn của từng tham số IPN trước khi ghi vào đơn hàng; cách kiểm dữ liệu đầu vào bằng Zod được trình bày trên trancongthang.vn.

7. Bước 6: Chạy đủ kịch bản trên sandbox VNPay

Trang demo của VNPAY (sandbox.vnpayment.vn/apis/vnpay-demo) công bố danh sách thẻ test cho môi trường sandbox, gồm thẻ nội địa, thẻ quốc tế có và không có 3DS, cùng các thẻ cố ý báo lỗi: không đủ số dư, chưa kích hoạt, bị khoá, hết hạn. Sandbox chỉ nhận đúng các thẻ trong danh sách đó. Hai thẻ hay dùng nhất:

Thẻ test sandbox VNPay (nguồn: trang demo VNPAY)
LoạiSố thẻChủ thẻNgàyXác thựcKết quả
ATM nội địa, ngân hàng NCB9704198526191432198NGUYEN VAN APhát hành 07/15OTP 123456Thành công
VISA quốc tế (3DS)4456530000001096NGUYEN VAN AHết hạn 12/26CVC 123Thành công

Danh sách thẻ do VNPAY cập nhật, hãy mở trang demo để lấy thẻ báo lỗi mới nhất thay vì chép từ bài viết. Với mỗi kịch bản, kiểm đủ ba điểm: trang Return hiện đúng trạng thái, IPN tới đúng và trả đúng RspCode, đơn trong cơ sở dữ liệu đổi đúng trạng thái.

Trang Demo của cổng thanh toán VNPAY với link demo và bảng thông tin thẻ test ngân hàng NCB
Trang Demo trên sandbox.vnpayment.vn: link demo CreateOrder và bảng thẻ test. Thẻ số 1 (NCB, 9704198526191432198, OTP 123456) cho kết quả thành công, thẻ số 2 báo không đủ số dư, thẻ số 3 báo chưa kích hoạt. Ảnh chụp bằng Playwright, 25/09/2026.
  1. Thanh toán thành công bằng thẻ NCB ở trên.
  2. Thẻ không đủ số dư (vnp_ResponseCode 51).
  3. Thẻ chưa đăng ký Internet Banking hoặc bị khoá.
  4. Nhập sai OTP nhiều lần (79 là vượt số lần nhập sai mật khẩu).
  5. Khách bấm Huỷ giữa chừng (24).
  6. Gửi lại cùng một IPN hai lần: lần hai phải trả RspCode 02, đơn không bị cộng hai lần.
  7. Để link thanh toán quá vnp_ExpireDate rồi mới bấm.

Mã phản hồi vnp_ResponseCode cần xử lý

Một số mã phản hồi theo tài liệu thanh toán của VNPAY
MãÝ nghĩaWebsite nên làm gì
00Giao dịch thành côngĐánh dấu đã thanh toán (chỉ trong IPN)
07Trừ tiền thành công nhưng giao dịch bị nghi ngờGiữ đơn, chờ đối soát trước khi giao hàng
24Khách huỷ giao dịchCho khách thanh toán lại
51Tài khoản không đủ số dưGợi ý phương thức khác
65Vượt hạn mức giao dịch trong ngàyGợi ý phương thức khác
75Ngân hàng đang bảo trìBáo khách thử lại sau
79Nhập sai mật khẩu thanh toán quá số lần quy địnhCho khách thanh toán lại
99Lỗi khácGhi nhật ký, liên hệ VNPAY nếu lặp lại

8. Bước 7: Xin go-live production

Khi các kịch bản trên chạy đúng, liên hệ VNPAY để ký hợp đồng và xin thông số môi trường thật. Hồ sơ thường gồm giấy chứng nhận đăng ký doanh nghiệp, tài khoản ngân hàng nhận tiền và website đã có đủ thông tin chủ sở hữu, chính sách đổi trả, chính sách bảo mật. Danh sách chính xác do VNPAY yêu cầu theo từng trường hợp. Sau khi được duyệt: đổi VNPAY_URL sang địa chỉ production, thay TmnCode và HashSecret mới, khai báo IPN URL production với VNPAY.

9. Sandbox VNPay khác production ở đâu

Hạng mụcSandboxProduction
Địa chỉ thanh toánsandbox.vnpayment.vn/paymentv2/vpcpay.htmlĐịa chỉ do VNPAY cấp khi go-live
TmnCode, HashSecretCấp qua email khi đăng ký testCấp mới sau khi ký hợp đồng
Thẻ thanh toánChỉ thẻ test trong danh sách của VNPAYThẻ, tài khoản thật của khách
TiềnKhông trừ thậtTrừ thật, đối soát theo hợp đồng
IPN URLKhai báo cho môi trường testKhai báo lại cho môi trường thật

10. Các lỗi phổ biến và cách xử lý

  • "Sai chữ ký": kiểm tra sắp xếp tên tham số, mã hoá URL giá trị, đổi %20 thành +, và đúng HashSecret của môi trường đang gọi.
  • "Giao dịch không hợp lệ": TmnCode sai hoặc dùng thông số sandbox trên địa chỉ production (và ngược lại).
  • Số tiền lệch: vnp_Amount phải nhân 100 (10.000 đồng gửi 1000000).
  • IPN không tới: VNPAY không gọi được URL của bạn (tường lửa, chặn IP, cổng lạ, URL localhost). Trên sandbox cần một địa chỉ công khai có HTTPS.
  • Lệch giờ: vnp_CreateDate và vnp_ExpireDate phải theo giờ Việt Nam GMT+7, máy chủ chạy UTC thì phải cộng 7 giờ như code mẫu.

Câu hỏi thường gặp về sandbox VNPay

Đăng ký sandbox VNPay có mất phí hay cần giấy phép kinh doanh không?

Không cần giấy phép kinh doanh ở bước sandbox. Bạn đăng ký tại sandbox.vnpayment.vn/devreg và nhận thông số test qua email. Giấy tờ doanh nghiệp chỉ cần khi ký hợp đồng để chạy thật.

Thẻ test sandbox VNPay là thẻ nào?

Thẻ nội địa hay dùng là thẻ NCB 9704198526191432198, chủ thẻ NGUYEN VAN A, ngày phát hành 07/15, OTP 123456. Danh sách đầy đủ, gồm cả thẻ quốc tế và thẻ báo lỗi, nằm ở trang demo sandbox.vnpayment.vn/apis/vnpay-demo.

Vì sao thanh toán sandbox thành công mà đơn vẫn chưa đổi trạng thái?

Thường do website chỉ cập nhật đơn ở trang Return, hoặc IPN URL chưa khai báo, hoặc máy chủ không nhận được lời gọi từ VNPAY. Trạng thái đơn phải đổi trong IPN, và IPN URL phải là địa chỉ công khai VNPAY gọi tới được.

Có dùng HashSecret sandbox cho production được không?

Không. Mỗi môi trường có TmnCode và HashSecret riêng. Dùng lẫn sẽ bị báo sai chữ ký hoặc giao dịch không hợp lệ. Cất hai bộ khoá ở hai bộ biến môi trường khác nhau.

Next.js App Router có cần thư viện riêng để tích hợp VNPay không?

Không bắt buộc. Module crypto của Node.js đủ để ký HMAC SHA512 như code trong bài. Route tạo URL và route IPN phải chạy ở runtime Node.js, không đặt ở Edge runtime nếu dùng module crypto của Node.

Cần ALODEV tích hợp VNPay, MoMo hoặc nhiều cổng cùng lúc cho website doanh nghiệp, liên hệ qua trang /lien-he để được tư vấn phạm vi và thời gian.

Nhận bài mới

Để lại email để nhận bài mới của series Cẩm nang thực hành, hoặc theo dõi bằng RSS.

RSS Cẩm nang thực hành

Email chỉ dùng để gửi bài mới, xem Chính sách bảo mật.

Trao đổi về dự án

Cần tư vấn?