Ví dụ tích hợp thanh toán MoMo Sandbox bằng Node.js, Express, EJS và package
momo-payment.
Ứng dụng minh họa đầy đủ một luồng thanh toán:
- tạo payment request;
- chuyển người dùng sang cổng thanh toán MoMo;
- nhận kết quả qua
redirectUrl; - nhận kết quả bất đồng bộ qua
ipnUrl; - kiểm tra chữ ký IPN;
- kiểm tra
partnerCode,orderIdvàamount; - xử lý idempotency trong phạm vi phiên chạy;
- query lại trạng thái giao dịch từ MoMo;
- hiển thị giao dịch và payload JSON trên giao diện demo;
- expose localhost bằng HTTPS qua
localhost.run, không cần tài khoản hoặc authtoken.
Lưu ý: Đây là project demo. Giao dịch hiện chỉ được lưu trong
Maptrên RAM, vì vậy dữ liệu sẽ mất khi restart app. Ứng dụng production cần database, transaction và idempotency key bền vững.
Browser
|
| POST /payments/momo/create
v
Express application
|
| POST /v2/gateway/api/create
v
MoMo Sandbox
|
| redirectUrl (GET)
+------------------------------> /payments/momo/return
|
| ipnUrl (POST JSON)
+------------------------------> /payments/momo/ipn
|
| verify signature,
| verify amount/order
v
Update transaction
|
| HTTP 204
v
MoMo Sandbox
- Node.js 18 trở lên.
- npm.
- Windows 10/11 có OpenSSH Client để chạy
localhost.run. - Tài khoản MoMo Sandbox và thông tin tích hợp:
MOMO_PARTNER_CODE;MOMO_ACCESS_KEY;MOMO_SECRET_KEY.
Kiểm tra Node.js và SSH:
node --version
npm.cmd --version
ssh -VMở PowerShell tại thư mục project:
cd "C:\Users\peter\Downloads\MoMo NodeJS"
npm.cmd install
Copy-Item .env.example .envMở .env và điền credentials MoMo Sandbox. Không commit file .env.
PORT=3000
MOMO_PARTNER_CODE=MOMxxxx_TEST
MOMO_ACCESS_KEY=your-sandbox-access-key
MOMO_SECRET_KEY=your-sandbox-secret-key
# Chỉ dùng base URL; SDK tự nối /v2/gateway/api/create và /query.
MOMO_ENDPOINT=https://test-payment.momo.vn
# true: tự tạo HTTPS URL bằng localhost.run.
# false: chỉ chạy local, phù hợp khi không cần IPN từ MoMo.
TUNNEL_ENABLED=true
# Để trống khi dùng localhost.run.
# Có thể đặt URL public cố định nếu đã có domain HTTPS riêng.
APP_BASE_URL=
# true để xem log request, callback và IPN; false để giảm log.
MOMO_DEBUG=trueMặc định project dùng:
https://test-payment.momo.vn
Không thêm /v2/gateway/api vào MOMO_ENDPOINT; thư viện tự nối path API.
Chỉ chuyển sang endpoint Production khi đã có credentials Production và đã
kiểm tra đầy đủ cấu hình.
Chạy development mode:
npm.cmd run devHoặc chạy trực tiếp:
node .Khi tunnel khởi động thành công, terminal sẽ hiển thị URL tương tự:
MoMo example listening at http://localhost:3000
localhost.run connected {
publicBaseUrl: 'https://5e0d6f77bd1a44.lhr.life'
}
Public callback base URL: https://5e0d6f77bd1a44.lhr.life
IPN URL: https://5e0d6f77bd1a44.lhr.life/payments/momo/ipn
URL này được dùng tự động trong payment request:
redirectUrl = https://<public-host>/payments/momo/return
ipnUrl = https://<public-host>/payments/momo/ipn
Mỗi lần restart app, localhost.run có thể cấp URL mới. Hãy tạo payment sau
khi thấy dòng IPN URL mới trong terminal.
Invoke-WebRequest `
-Uri "https://<public-host>/health" `
-UseBasicParsingKết quả mong đợi:
{
"ok": true,
"service": "momo-payment-nodejs-example",
"publicBaseUrl": "https://<public-host>",
"ipnUrl": "https://<public-host>/payments/momo/ipn"
}Mở:
http://localhost:3000
Nhập số tiền và nội dung, sau đó chọn Tạo payment và mở MoMo.
Giao diện hiển thị:
- TMN code;
- môi trường Sandbox/Production;
- callback base URL hiện tại;
- form tạo payment;
- danh sách giao dịch trong phiên;
- trạng thái giao dịch;
- nút query transaction;
- nút xem JSON transaction.
Sau khi tạo payment, server gọi API create của MoMo và redirect trình duyệt tới
payUrl.
Ở màn hình MoMo Sandbox, người dùng có thể quét QR hoặc dùng ứng dụng MoMo Sandbox để xác nhận hoặc từ chối giao dịch.
MoMo gửi hai loại callback:
Sau khi người dùng hoàn tất hoặc hủy thanh toán, MoMo chuyển hướng trình duyệt đến:
GET /payments/momo/return
Handler lưu payload vào transaction và hiển thị trang kết quả. Redirect phù hợp để cập nhật giao diện cho người dùng, nhưng không nên là nguồn duy nhất để xác nhận đã nhận tiền vì người dùng có thể đóng trình duyệt.
MoMo gọi server-to-server:
POST /payments/momo/ipn
Content-Type: application/json
Handler IPN thực hiện:
- kiểm tra payload JSON;
- kiểm tra
partnerCode; - kiểm tra chữ ký HMAC-SHA256;
- tìm
orderIdtrong transaction; - đối chiếu
amount; - cập nhật
resultCode,message,transIdvà trạng thái; - phản hồi
HTTP 204 No Content.
MoMo yêu cầu đối tác phản hồi trong vòng 15 giây. Endpoint IPN không trả JSON
thành công mà trả body rỗng với status 204.
Ví dụ log IPN thành công:
[momo-example] MoMo IPN received {
contentType: 'application/json',
signature: { hasSignature: true, signatureLength: 64 },
payload: {
partnerCode: 'MOMOLI4O20260908_TEST',
orderId: 'DEMO_1792029194613_OE24PB',
requestId: 'DEMO_1792029194613_OE24PB',
amount: 1000,
resultCode: 0,
message: 'Thành công.'
}
}
[momo-example] MoMo IPN processed {
orderId: 'DEMO_1792029194613_OE24PB',
resultCode: 0,
status: 'SUCCESS'
}
HTTP request {
method: 'POST',
path: '/payments/momo/ipn',
status: 204
}
Log chỉ hiển thị hasSignature và độ dài chữ ký, không hiển thị secret key
hoặc toàn bộ chữ ký.
Project ánh xạ các result code chính như sau:
resultCode |
Trạng thái demo | Ý nghĩa |
|---|---|---|
0 |
SUCCESS |
Thanh toán thành công |
9000 |
AUTHORIZED |
Đã authorization; cần capture/cancel tùy loại payment |
1000 |
PENDING |
Đang chờ người dùng xác nhận |
7000 |
PENDING |
Giao dịch đang được xử lý |
7002 |
PENDING |
Đang được nhà cung cấp phương thức thanh toán xử lý |
1003 |
FAILED |
Giao dịch bị hủy |
1004 |
FAILED |
Vượt hạn mức thanh toán |
1005 |
FAILED |
URL hoặc QR đã hết hạn |
1006 |
FAILED |
Người dùng từ chối thanh toán |
1007 |
FAILED |
Tài khoản không tồn tại hoặc không hoạt động |
mã khác 0 |
FAILED |
Thanh toán thất bại hoặc lỗi hệ thống |
Ví dụ giao dịch thất bại do người dùng từ chối:
resultCode: 1006
message: 'Giao dịch bị từ chối bởi người dùng.'
status: 'FAILED'
Trong danh sách giao dịch, bấm Query txn để gọi:
POST /transactions/:orderId/query
Server tạo requestId riêng:
QUERY_<orderId>
Sau đó gọi MoMo Query API và cập nhật transaction theo kết quả trả về.
Kết quả query thành công được hiển thị bằng thông báo:
Query <orderId>: Thành công.
Chọn nút JSON trong danh sách giao dịch hoặc gọi:
GET /api/transactions/:orderId
Payload gồm thông tin local transaction, create response, return payload, IPN payload và query payload nếu đã thực hiện.
Trong ảnh, transaction có:
status: "SUCCESS";amount: 1000;resultCode: "0"từ redirect;resultCode: 0từ query;transIddo MoMo cấp;payUrl;returnPayload;queryPayload.
Khi giao dịch Sandbox thành công, ứng dụng MoMo hiển thị mã giao dịch và số tiền đã thanh toán.
| Method | Route | Mục đích |
|---|---|---|
GET |
/ |
Giao diện demo |
POST |
/payments/momo/create |
Tạo payment và redirect tới MoMo |
GET |
/payments/momo/return |
Nhận redirect callback |
POST |
/payments/momo/ipn |
Nhận IPN server-to-server |
POST |
/transactions/:orderId/query |
Query trạng thái từ MoMo |
GET |
/api/transactions/:orderId |
Xem transaction dạng JSON |
GET |
/health |
Kiểm tra app và callback URL |
MoMo hỗ trợ idempotency cho POST request thông qua requestId. Trong project:
- request create dùng
orderIdlàmrequestIdmặc định; - query dùng
QUERY_<orderId>; - order ID được tạo mới cho mỗi payment;
- IPN được kiểm tra chữ ký trước khi cập nhật dữ liệu;
partnerCode,orderIdvàamountđược đối chiếu;- trạng thái cuối (
SUCCESS,FAILED,AUTHORIZED) không bị IPN lặp lại ghi đè; - không log
MOMO_SECRET_KEY; - không commit
.env.
Trong production, cần thay Map bằng database và dùng unique constraint trên
orderId/requestId. Việc cập nhật trạng thái và ghi nhận side effect như xuất
đơn hàng, cộng điểm hoặc giao hàng phải nằm trong transaction hoặc có cơ chế
deduplication riêng.
Kiểm tra lần lượt:
- URL trong terminal có phải URL mới nhất không?
/healthcó truy cập được từ Internet không?TUNNEL_ENABLED=truechưa?- OpenSSH có hoạt động không (
ssh -V)? - Payment được tạo sau khi tunnel đã in ra
IPN URLchưa? - Log có dòng
MoMo IPN receivedkhông?
Nếu localhost.run không cấp URL, chạy:
TUNNEL_ENABLED=falsehoặc đặt APP_BASE_URL bằng một HTTPS URL public khác.
Payload IPN không có trường signature. Kiểm tra endpoint, môi trường MoMo và
payload request thực tế.
Payload có chữ ký nhưng không khớp. Kiểm tra:
MOMO_PARTNER_CODE;MOMO_ACCESS_KEY;MOMO_SECRET_KEY;MOMO_ENDPOINT;- credentials Sandbox/Production có bị trộn hay không.
Kiểm tra log cụ thể:
Partner code mismatch;Missing MoMo signature;Invalid MoMo signature;Unknown orderId;Amount mismatch.
Đổi port trong .env:
PORT=3100Khi đó localhost.run sẽ forward tới port mới.
Để giảm log:
MOMO_DEBUG=falseKhi debug bị tắt, các log request chi tiết sẽ không được ghi. Các lỗi quan trọng như create payment thất bại hoặc tunnel không khởi động vẫn được ghi ra console.
npm.cmd run dev
npm.cmd startnpm.cmd run dev: chạy Node.js watch mode.npm.cmd start: chạy server bình thường.
Project demo phục vụ mục đích học tập và thử nghiệm tích hợp MoMo Sandbox.




