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

← Tin tức · Hướng dẫn

16/05/2026 · 12 phút đọc

Tích hợp VNPay vào website Next.js — hướng dẫn step-by-step 2026

VNPay là cổng thanh toán phổ biến nhất Việt Nam — hỗ trợ ATM 40+ ngân hàng, QR code, thẻ tín dụng. Bài này đi qua đăng ký merchant, tạo signature HMAC SHA-512, xử lý IPN callback, test sandbox và go-live production. Bao gồm code mẫu Next.js 14 App Router.

VNPayThanh toánNext.jsHướng dẫn

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

  • Bạn target khách Việt Nam trong nước — VNPay phủ 40+ ngân hàng (Vietcombank, BIDV, Techcombank, MB, ACB, VPBank, etc.)
  • Cần thẻ ATM nội địa (không phải thẻ tín dụng) — VNPay là leader.
  • Đơn hàng > 500k — VNPay hỗ trợ tốt, Momo phù hợp đơn nhỏ hơn.
  • Yêu cầu integrate đa kênh: web + app + POS — VNPay có SDK đầy đủ.

VNPay phí 1.6-2.2% mỗi giao dịch (tuỳ ngân hàng + volume) — đắt hơn Momo (1.1-1.8%) nhưng ổn định + phổ thông hơn. Stripe không cho merchant VN, không phải lựa chọn cho doanh nghiệp trong nước.

2. Bước 1: Đăng ký merchant sandbox

Truy cập sandbox.vnpayment.vn → Đăng ký tài khoản test → Nhận TmnCode (mã merchant) + HashSecret (secret key dùng ký HMAC). Sandbox cấp ngay trong 5 phút, không cần giấy tờ. Sau khi code xong + test đủ 5 scenario, mới xin go-live production cần GPKD + STK doanh nghiệp.

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

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

env
VNPAY_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:

ts
import 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(); const date = new Date(); const pad = (n: number) => String(n).padStart(2, '0'); const createDate = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`; 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, }; 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 }); }

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:

ts
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 responseCode = params.vnp_ResponseCode; if (responseCode === '00') { // Update DB: orderId → status = 'paid' // Send email confirmation } return new Response(JSON.stringify({ RspCode: '00', Message: 'Confirm Success' })); }

7. Bước 6: Test đủ 5 scenario sandbox

  1. Success: thẻ NCB 9704198526191432198 — số CVV bất kỳ, OTP 123456.
  2. Insufficient funds: thẻ NCB 9704195798459170488 — sẽ báo không đủ tiền.
  3. Card not registered: thẻ không kích hoạt Internet Banking.
  4. Wrong OTP: nhập OTP sai 3 lần.
  5. User cancel: bấm "Hủy" giữa flow.

Mỗi case test đủ flow: Return URL hiện đúng status + IPN callback fire đúng + DB update đúng. Log mọi request VNPay vào file để debug nếu cần.

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

Gửi email cho VNPay (sales@vnpay.vn) kèm: GPKD doanh nghiệp + STK ngân hàng + screenshot website + 5 test case sandbox đã pass. Mất 5-7 ngày làm việc duyệt. Sau khi duyệt, đổi VNPAY_URL bỏ "sandbox." + đổi TmnCode + HashSecret production.

9. Các bug phổ biến + cách xử lý

  • "Sai chữ ký" — kiểm tra sort key alphabetic + encodeURIComponent + replace %20 → +.
  • "Giao dịch không hợp lệ" — TmnCode sai hoặc môi trường (sandbox vs production) không khớp.
  • Amount lệch — quên × 100 (VNPay tính theo đồng × 100).
  • IPN không fire — VNPay không gọi được URL của bạn (firewall, IP whitelist, port).
  • Timezone lệch — vnp_CreateDate phải GMT+7, không phải UTC.

Cần Alodev tích hợp VNPay vào website doanh nghiệp? Dịch vụ trọn gói 15-25tr bao gồm code + test + go-live + 3 tháng support. Liên hệ tại /lien-he.

Cần tư vấn?

Founder phản hồi
trong 24 giờ.