momo-vn
v0.1.1
Published
Zero-dependency TypeScript SDK for MoMo Vietnam payment gateway (AIOv2) — per-endpoint signature allowlist, two-phase capture, IPN verification.
Maintainers
Readme
momo-vn
English | Tiếng Việt
SDK TypeScript cho cổng thanh toán MoMo tại Việt Nam. Không dependency, không telemetry, không giữ dữ liệu của bạn.
npm install momo-vnPackage cộng đồng, không phải sản phẩm chính thức của MoMo / M_Service. Nó chỉ đóng gói lại đúng những gì tài liệu MoMo v3 mô tả, để bạn không phải viết lại phần cấu hình mỗi lần mở project mới.
Mục lục
- Tại sao chọn momo-vn
- Cam kết minh bạch
- Quick start
- Xử lý lỗi
- Test với sandbox
- Câu hỏi thường gặp
- Ủng hộ dự án
Tại sao chọn momo-vn
Nếu bạn từng tích hợp MoMo, bạn biết cảm giác này: mỗi project mới lại mở lại project cũ, copy đoạn tạo chữ ký, sửa tên biến môi trường, chạy thử, nhận signature mismatch, dò từng ký tự trong chuỗi ký.
Đây là code thật mà hầu hết chúng ta đều viết, ở mọi project:
// project 1
const rawSignature = `accessKey=${accessKey}&amount=${amount}&extraData=${extraData}&ipnUrl=${ipnUrl}&orderId=${orderId}&orderInfo=${orderInfo}&partnerCode=${partnerCode}&redirectUrl=${redirectUrl}&requestId=${requestId}&requestType=${requestType}`;
const signature = hmacSHA256(rawSignature, secretKey).toString();
// project 2 — y hệt, chỉ khác tên biến môi trường
// project 3 — y hệt
// ...Rồi tới lúc hoàn tiền, chuỗi ký lại khác hoàn toàn:
const rawSignature = `accessKey=${accessKey}&amount=${amount}&description=${description}&orderId=${orderId}&partnerCode=${partnerCode}&requestId=${requestId}&transId=${transId}`;Mỗi endpoint một công thức. Nhớ nhầm một field, sai một dấu &, là mất nửa buổi.
momo-vn gói đúng phần đó lại. Bạn khai báo key một lần, gọi hàm có type đầy đủ, hết.
const momo = new MoMo({ partnerCode, accessKey, secretKey, ipnUrl, redirectUrl });
const payment = await momo.createPayment({ amount: 50_000 });
// → payment.payUrlKhông có gì "ảo diệu" ở đây. Nó chỉ là phần code bạn vẫn tự viết, đã được viết sẵn, có test, và có người kiểm chứng lại với tài liệu gốc.
Cam kết minh bạch
Đây là package xử lý khoá bí mật thanh toán của bạn. Bạn có quyền nghi ngờ nó. Dưới đây là những cam kết, kèm cách bạn tự kiểm chứng trong 30 giây chứ không cần tin lời tôi.
Package không gửi dữ liệu của bạn đi đâu cả
Toàn bộ source chỉ có đúng một chỗ gọi mạng, và nó chỉ gọi tới MoMo:
# Tự chạy trên máy bạn sau khi npm install:
grep -rn "fetch" node_modules/momo-vn/dist/index.js
grep -rnoE "https?://[a-zA-Z0-9._/-]+" node_modules/momo-vn/dist/index.js | sort -uKết quả bạn sẽ thấy đúng hai domain: https://test-payment.momo.vn và https://payment.momo.vn. Không có domain nào khác. Không analytics, không telemetry, không "phone home".
Package không đọc biến môi trường của bạn
grep -rn "process.env" node_modules/momo-vn/dist/index.js
# → không có kết quảCredential được truyền vào tường minh qua tham số. Package không tự đi tìm MOMO_SECRET_KEY trong môi trường của bạn, nên nó không thể vô tình đọc trúng thứ gì khác.
Package không ghi file, không log, không lưu gì
grep -rnE "writeFile|createWriteStream|child_process|console\." node_modules/momo-vn/dist/index.js
# → không có kết quảKhông ghi đĩa. Không console.log. secretKey của bạn không bao giờ bị in ra terminal hay lọt vào file log — kể cả khi bạn console.log(momo.config), nó hiện [redacted].
Package không có dependency nào
"dependencies": {}Chỉ dùng node:crypto và fetch có sẵn trong Node. Không có package thứ ba nào chạy code trên máy bạn. Không có chuỗi phụ thuộc để ai đó chèn mã độc vào.
Bảng tóm tắt
| Cam kết | Cách tự kiểm chứng |
|---|---|
| Chỉ gọi mạng tới MoMo | grep -rnoE "https?://" dist/index.js → chỉ 2 domain momo.vn |
| Không đọc process.env | grep "process.env" dist/index.js → rỗng |
| Không ghi file, không spawn process | grep -E "writeFile\|child_process" dist/index.js → rỗng |
| Không log ra console | grep "console\." dist/index.js → rỗng |
| Không có dependency | npm ls --prod → chỉ mình nó |
| Chặn chạy ở browser | Constructor throw nếu thấy window — secretKey không thể lọt vào bundle frontend |
Nguyên tắc nền
- Bạn kiểm soát mọi thứ. Package không tự quyết định gì thay bạn. Không tự retry ngầm, không tự gọi thêm API nào ngoài cái bạn gọi.
- Mã nguồn mở, MIT. Đọc được, fork được, sửa được. Không có bản "pro" trả phí, không có tính năng bị khoá.
- Không thu thập, không lưu, không tái sử dụng. Dữ liệu đơn hàng và credential của bạn đi thẳng từ server của bạn tới MoMo. Package chỉ là hàm thuần biến đổi dữ liệu — nó không có nơi nào để lưu, kể cả nếu muốn.
- Mục đích duy nhất là tiết kiệm thời gian cấu hình. Không hơn.
⚠️ Vì package xử lý
secretKey, hãy chỉ dùng nó ở backend. Constructor sẽ throw nếu phát hiện môi trường trình duyệt. Cần type ở frontend thì import từmomo-vn/types-only.
Quick start
1. Bắt đầu trong 5 phút
Lấy credential
Đăng ký merchant tại business.momo.vn. MoMo sẽ cấp cho bạn ba thứ:
| Tên trong tài liệu MoMo | Là gì |
|---|---|
| partnerCode | Mã đối tác, định danh merchant của bạn |
| accessKey | Khoá công khai, đi kèm mọi request |
| secretKey | Khoá bí mật — dùng để ký. Không bao giờ để lộ, không bao giờ đưa lên frontend |
Cài và khởi tạo
npm install momo-vnimport { MoMo } from 'momo-vn';
const momo = new MoMo({
partnerCode: process.env.MOMO_PARTNER_CODE!,
accessKey: process.env.MOMO_ACCESS_KEY!,
secretKey: process.env.MOMO_SECRET_KEY!,
ipnUrl: 'https://api.example.com/momo/ipn', // MoMo gọi về đây khi có kết quả
redirectUrl: 'https://example.com/payment/result', // khách được đưa về đây sau khi trả
env: 'sandbox', // đổi thành 'production' khi lên thật
});env mặc định là 'sandbox' — cố ý, để một biến môi trường bị quên không làm bạn chuyển tiền thật.
Tạo yêu cầu thanh toán
const payment = await momo.createPayment({ amount: 50_000 });
if (payment.isSuccess) {
console.log(payment.payUrl); // đưa link này cho khách
console.log(payment.generated.orderId); // SDK tự sinh orderId, báo lại cho bạn biết
}Chỉ cần amount. orderId, requestId, orderInfo sẽ được sinh tự động đúng ràng buộc của MoMo và báo lại trong payment.generated để bạn lưu vào DB.
Tất nhiên bạn có thể tự truyền:
const payment = await momo.createPayment({
amount: 250_000,
orderId: 'DH20260722001',
orderInfo: 'Thanh toan don hang DH20260722001',
items: [
{ id: 'SKU1', name: 'Ca phe sua da', price: 25_000, quantity: 10, totalPrice: 250_000 },
],
userInfo: { name: 'Nguyen Van A', phoneNumber: '0900000000' },
deliveryInfo: { deliveryAddress: '123 Le Loi, Q1', deliveryFee: '15000', quantity: '1' },
extraData: { userId: 'u_123' }, // tự encode base64 JSON giúp bạn
});Nhận kết quả (IPN)
app.post('/momo/ipn', express.json(), async (req, res) => {
const result = momo.verifyIpn(req.body);
if (!result.isVerified) {
return res.sendStatus(400); // chữ ký sai — có thể là request giả mạo
}
// Chữ ký đúng chỉ chứng minh request đến từ MoMo.
// Vẫn phải đối chiếu với DB xem có đúng đơn hàng đó không.
const order = await db.orders.findOne({ orderId: result.orderId });
if (!order || order.amount !== result.amount) {
return res.sendStatus(400);
}
if (result.isSuccess) {
await db.orders.markPaid(result.orderId, result.transId);
}
res.sendStatus(204);
});Xong. Đó là toàn bộ luồng cơ bản.
2. So sánh với tài liệu gốc của MoMo
Phần này dành cho người muốn biết chính xác SDK làm gì so với tài liệu gốc. Không có gì bị giấu — mỗi dòng dưới đây đều ánh xạ 1-1 với tài liệu.
Endpoint
| Tài liệu MoMo | Hàm trong SDK |
|---|---|
| POST /v2/gateway/api/create | momo.createPayment() |
| POST /v2/gateway/api/query | momo.query() |
| POST /v2/gateway/api/confirm | momo.confirm() |
| POST /v2/gateway/api/refund | momo.refund() |
| POST /v2/gateway/api/refund/query | momo.queryRefund() |
| IPN callback | momo.verifyIpn() |
| POST /v2/gateway/api/pos | (chưa wrap — nhưng SIGNATURE_FIELDS.pos đã có sẵn để bạn tự ký) |
Host https://test-payment.momo.vn / https://payment.momo.vn được chọn tự động theo env.
Tham số của /create
| Field trong tài liệu MoMo | SDK xử lý thế nào |
|---|---|
| partnerCode | Từ config, tự điền |
| accessKey | Từ config — được ký nhưng không gửi trong body, đúng như tài liệu |
| requestId | Bạn truyền, hoặc SDK sinh (≤50 ký tự) |
| amount | Bạn truyền. SDK validate 1.000 – 50.000.000đ |
| orderId | Bạn truyền, hoặc SDK sinh đúng regex ^[0-9a-zA-Z]([-_.]*[0-9a-zA-Z]+)*$, ≤200 ký tự |
| orderInfo | Bạn truyền, hoặc SDK sinh |
| redirectUrl | Từ config hoặc per-call |
| ipnUrl | Từ config, từ service, hoặc per-call |
| requestType | Mặc định captureWallet; type chỉ nhận đúng 5 giá trị hợp lệ |
| extraData | Bạn truyền object → SDK tự JSON.stringify + base64, validate ≤1000 ký tự |
| lang | Mặc định vi |
| autoCapture | Mặc định true. Đặt false để dùng luồng 2 bước |
| orderGroupId, storeId, partnerName, subPartnerCode, referenceId, partnerClientId | Truyền thẳng nếu bạn cung cấp |
| items | Có type đầy đủ. SDK validate tối đa 50 phần tử |
| userInfo, deliveryInfo | Có type đầy đủ |
| signature | SDK tạo hoàn toàn tự động |
Chuỗi ký — phần dễ sai nhất
Tài liệu MoMo cho mỗi endpoint một template chuỗi ký khác nhau. SDK mã hoá tất cả trong SIGNATURE_FIELDS:
| Endpoint | Số field | Field được ký |
|---|---|---|
| /create (request) | 10 | accessKey, amount, extraData, ipnUrl, orderId, orderInfo, partnerCode, redirectUrl, requestId, requestType |
| /create (response) | 9 | accessKey, amount, message, orderId, partnerCode, payUrl, requestId, responseTime, resultCode |
| /query | 4 | accessKey, orderId, partnerCode, requestId |
| /confirm | 7 | accessKey, amount, description, orderId, partnerCode, requestId, requestType |
| /refund | 7 | accessKey, amount, description, orderId, partnerCode, requestId, transId |
| /refund/query | 4 | accessKey, orderId, partnerCode, requestId |
| /pos | 8 | accessKey, amount, extraData, orderId, orderInfo, partnerCode, paymentCode, requestId |
| IPN | 13 | accessKey, amount, extraData, message, orderId, orderInfo, orderType, partnerCode, payType, requestId, responseTime, resultCode, transId |
Bạn xem trực tiếp trong src/constants.ts.
Ba lỗi tài liệu MoMo mà SDK đã né
Trong quá trình đối chiếu, tôi tìm thấy vài chỗ tài liệu MoMo in sai. Copy nguyên văn từ web là hỏng:
| Chỗ | Tài liệu in | Đúng phải là |
|---|---|---|
| Template chữ ký response của /create | payUrl=&payUrl | payUrl=$payUrl |
| Template chữ ký /refund | $acessKey | $accessKey |
| Sample của trang /init | orderId chứa dấu : | Vi phạm chính regex MoMo công bố |
Ngoài ra nodejs/QuickPay.js trong repo sample chính chủ của MoMo có comment copy-paste sai (in template của /create ngay trên chuỗi ký của /pos).
resultCode — SDK dịch lại thế nào
Tài liệu MoMo có cột "Final Status", và ghi rõ: "All the error codes implicates 'Final Status' in the list below are idempotent."
Điều này có nghĩa resultCode không thể quy về true/false. SDK chuẩn hoá thành 4 trạng thái:
| status | resultCode | Nghĩa là gì |
|---|---|---|
| 'success' | 0 | Thành công. Đây là mã thành công duy nhất |
| 'authorized' | 9000 | Tiền đã bị giữ nhưng chưa trừ. Cần gọi confirm() |
| 'pending' | 1000, 7000, 7002 | Chưa xong. Đừng kết luận vội |
| 'failed' | còn lại | Thất bại |
Ngoài ra mỗi kết quả trả về đều có:
isFinal— MoMo coi kết quả này là chung cuộc. Retry cũng vô ích.shouldQuery— nên ngừng retry và gọiquery()để biết chuyện gì thực sự đã xảy ra.
Ví dụ mã 99 ("unknown error") là Final = Yes, nên gửi lại cùng requestId sẽ trả 99 mãi mãi. SDK đánh dấu shouldQuery: true cho trường hợp này.
3. Những việc khó mà SDK làm thay bạn
1. Mỗi endpoint một công thức ký — không phải "sort key rồi ký"
Đây là hiểu lầm phổ biến nhất. Người ta hay viết:
// ❌ SAI — trông rất sạch nhưng không bao giờ chạy đúng
const raw = Object.keys(payload).sort().map(k => `${k}=${payload[k]}`).join('&');Không chạy được, vì hai lý do:
accessKeyđược ký nhưng KHÔNG nằm trong body. Sort body thì không thể tạo ra chuỗi đúng.lang,items,userInfo,deliveryInfo,autoCapture,orderGroupId,partnerName,storeIdnằm trong body nhưng KHÔNG được ký.
Thêm nữa, field optional vẫn phải xuất hiện dưới dạng key= rỗng:
// ❌ Bỏ description vì nó optional → sai chữ ký
// ✓ Phải giữ: accessKey=..&amount=..&description=&orderId=..SDK giữ một danh sách cố định cho từng endpoint, không sort gì cả.
2. requestId là idempotency key — nhưng MoMo từ chối trùng, không trả lại kết quả cũ
Tài liệu MoMo: "All POST requests of AIOv2 accept requestId as idempotency key", hiệu lực tối thiểu 31 ngày.
Nhưng khác với Stripe, gửi trùng requestId không được replay kết quả cũ — nó bị từ chối bằng resultCode 40 hoặc HTTP 422 + 7000.
Hệ quả cực kỳ quan trọng khi hoàn tiền:
const requestId = 'refund-DH001-lan1'; // bạn tự quản lý, lưu vào DB
try {
return await momo.refund({ transId, amount, requestId });
} catch (err) {
// ✓ Retry với CÙNG requestId
// ✗ Sinh requestId mới ở đây = HOÀN TIỀN HAI LẦN
return await momo.refund({ transId, amount, requestId });
}Và khi nhận shouldQuery: true, đừng retry — hãy hỏi lại MoMo:
if (result.shouldQuery) {
const actual = await momo.query({ orderId: result.orderId });
}3. IPN bắt buộc phải xác thực chữ ký
MoMo ghi rõ: "You must validate the IPN signature to verify transaction result."
Nhưng chính sample PHP của MoMo lại so sánh bằng ==. SDK dùng crypto.timingSafeEqual, và tách hai boolean:
const result = momo.verifyIpn(req.body);
result.isVerified // chữ ký hợp lệ → request đúng là từ MoMo
result.isSuccess // resultCode === 0 → khách đã trả tiềnĐây không phải chi tiết nhỏ. Một callback thật từ MoMo báo giao dịch thất bại sẽ có isVerified: true, isSuccess: false. Gộp hai thứ này thành một biến là cách nhanh nhất để ghi nhận nhầm một giao dịch chưa thanh toán.
4. Toàn bộ API
Chọn phương thức: ví, ATM, hay thẻ tín dụng
MoMo dùng field requestType để quyết định khách trả bằng gì. SDK khai type để bạn không gõ nhầm:
| requestType | Khách trả bằng | Trạng thái trong SDK |
|---|---|---|
| captureWallet (mặc định) | Ví MoMo | ✅ Đã dùng thực tế |
| payWithMethod | Trang cho khách tự chọn ví / ATM / thẻ | ✅ Hoạt động đầy đủ |
| payWithATM | Thẻ ATM nội địa (đi thẳng) | ⚠️ Ký đúng, có validate sàn 10.000đ, chưa verify sandbox |
| payWithCC | Thẻ quốc tế (đi thẳng) | ⚠️ Ký đúng, chưa verify sandbox |
| linkWallet | Liên kết ví để lưu token | 🚧 Chưa hỗ trợ luồng token |
Cách phổ biến nhất để nhận cả ATM lẫn thẻ tín dụng là để MoMo hiện trang cho khách tự chọn:
await momo.createPayment({ amount: 50_000, requestType: 'payWithMethod' });Muốn đi thẳng vào một phương thức (bỏ qua bước chọn):
await momo.createPayment({ amount: 50_000, requestType: 'payWithATM' });Lưu ý về sàn tiền theo phương thức: thẻ ATM nội địa tối thiểu 10.000đ (ví chỉ 1.000đ). SDK áp đúng ràng buộc này theo requestType — gọi ATM với 5.000đ sẽ bị chặn ngay tại MomoValidationError thay vì để MoMo trả lỗi:
import { amountBoundsFor } from 'momo-vn';
amountBoundsFor('payWithATM'); // { min: 10_000, max: 50_000_000 }
amountBoundsFor('captureWallet'); // { min: 1_000, max: 50_000_000 }Minh bạch về mức độ kiểm chứng:
payWithMethod(trang chọn) đã hoạt động đầy đủ. Hai luồng đi thẳngpayWithATM/payWithCCtạo request và chữ ký đúng theo tài liệu, nhưng chưa được test với sandbox thật — reference code gốc chỉ chạy luồng ví. Hãy chạynpm run example:sandboxvớirequestTypetương ứng để tự xác nhận trước khi lên production. Luồng lưu thẻ bằng token (linkWallet+ pay-with-token) chưa được implement.
Thanh toán hai bước (giữ tiền trước, trừ sau)
Hữu ích khi bạn cần xác nhận tồn kho / lịch trống trước khi thực sự lấy tiền.
// Bước 1 — giữ tiền, chưa trừ
const auth = await momo.createPayment({ amount: 500_000, autoCapture: false });
// → resultCode 9000, status === 'authorized'
// Bước 2a — chốt, trừ tiền thật
await momo.confirm({ orderId, requestId, amount: 500_000, requestType: 'capture' });
// Bước 2b — hoặc huỷ, trả tiền giữ về cho khách
await momo.confirm({
orderId, requestId, amount: 500_000,
requestType: 'cancel',
description: 'Nha hang het mon',
});Đặt
autoCapture: falsemà không gọiconfirm()sẽ khiến tiền khách bị treo. Đây là lý do endpoint này quan trọng.
Tra cứu trạng thái
const status = await momo.query({ orderId: 'DH20260722001' });
console.log(status.status, status.raw.transId);Hoàn tiền
const refund = await momo.refund({
transId: 2837465920, // transId MoMo cấp khi mua thành công — BẮT BUỘC
amount: 100_000, // nhỏ hơn số đã trả = hoàn một phần
originalOrderId: 'DH20260722001',
description: 'Khach huy mon',
});MoMo yêu cầu orderId của giao dịch hoàn tiền phải khác orderId giao dịch mua, và cho phép hoàn nhiều lần trên cùng transId. Vì vậy SDK sinh orderId kèm nonce — dùng prefix cố định kiểu refund_<orderId> sẽ trùng ngay ở lần hoàn một phần thứ hai.
Cấu trúc kết quả trả về
Object phẳng, kèm raw là nguyên văn MoMo trả về:
{
isSuccess: boolean // resultCode === 0
status: 'success' | 'authorized' | 'pending' | 'failed'
resultCode: number
message: string
isFinal: boolean // MoMo coi đây là kết quả chung cuộc
shouldQuery: boolean // ngừng retry, gọi query() để biết kết quả thật
orderId: string
requestId: string
raw: { ... } // NGUYÊN VĂN body MoMo trả về, không bị chỉnh sửa
generated: { orderId?, requestId?, orderInfo? } // field nào do SDK sinh
httpStatus: number
elapsedMs: number
}raw luôn có mặt, để bạn không bao giờ bị SDK che mất thứ gì MoMo gửi về.
Nhiều merchant account
const topup = new MoMo({ partnerCode: process.env.MOMO_TOPUP_PARTNER_CODE!, /* ... */ });
const delivery = new MoMo({ partnerCode: process.env.MOMO_DELIVERY_PARTNER_CODE!, /* ... */ });Credential nằm trên instance, không phải biến toàn cục.
5. Nhiều luồng thanh toán, mỗi luồng một IPN
Project chỉ có một hình thức thanh toán thì đặt ipnUrl ở config là đủ.
Nhưng khi có nhiều dịch vụ — nạp điểm, mua item, đặt lịch — mỗi cái ghi nhận vào một bảng khác nhau, nên cần IPN riêng. Khai báo chúng dưới dạng service:
const momo = new MoMo({
partnerCode: process.env.MOMO_PARTNER_CODE!,
accessKey: process.env.MOMO_ACCESS_KEY!,
secretKey: process.env.MOMO_SECRET_KEY!,
redirectUrl: 'https://app.example.com/payment/done',
services: {
topup: { ipnUrl: 'https://api.example.com/momo/ipn/topup' },
order: { ipnUrl: 'https://api.example.com/momo/ipn/order' },
booking: {
ipnUrl: 'https://api.example.com/momo/ipn/booking',
redirectUrl: 'https://app.example.com/booking/done', // riêng cho luồng này
},
},
});
await momo.createPayment({ amount: 50_000, service: 'booking' });Khi đã khai services, service trở thành tham số bắt buộc và được kiểm tra tên ngay lúc compile:
await momo.createPayment({ amount: 50_000 });
// ✗ Property 'service' is missing but required in type
// '{ service: "topup" | "order" | "booking" }'
await momo.createPayment({ amount: 50_000, service: 'bookig' });
// ✗ Type '"bookig"' is not assignable to type '"topup" | "order" | "booking"'.
// Did you mean '"booking"'?Vì sao không cho khai cả ipnUrl lẫn services
Constructor sẽ throw nếu bạn khai cả hai. Lý do rất cụ thể: một root ipnUrl nằm sau service map chính là cấu hình khiến lỗi trở nên vô hình. Call nào quên service sẽ âm thầm nhận URL mặc định → callback đặt lịch chạy vào handler nạp điểm → handler không tìm thấy orderId trong bảng của nó → trả 204 → giao dịch biến mất không để lại log.
Bắt buộc chọn service khiến lỗi xuất hiện lúc compile, thay vì lúc mất tiền.
redirectUrl thì vẫn cho fallback về config chung, vì redirect sai chỉ đưa khách tới trang sai — nhìn thấy ngay, không âm thầm.
Xử lý lỗi
Nguyên tắc: input sai thì throw, kết quả nghiệp vụ thì return.
| Tình huống | Hành vi |
|---|---|
| Thiếu credential, khai cả ipnUrl lẫn services | throw MomoConfigError |
| amount ngoài khoảng, orderId sai regex, quá 50 items, sai tên service | throw MomoValidationError |
| Timeout, mất mạng, body không phải JSON | throw MomoHttpError |
| Chạy trong trình duyệt | throw MomoEnvironmentError |
| resultCode khác 0 | return với isSuccess: false |
| Chữ ký IPN sai | return với isVerified: false |
resultCode khác 0 là câu trả lời hợp lệ từ cổng thanh toán, không phải ngoại lệ. Bắt nó bằng try/catch sẽ khiến bạn nuốt mất thông tin.
Test với sandbox
Repo có sẵn hai script.
Dò đúng thứ tự field của chữ ký IPN
Đây là phần rủi ro nhất khi tích hợp MoMo: tài liệu không công bố dứt khoát thứ tự field dùng để ký IPN, và các repo sample của chính MoMo tồn tại hai biến thể khác nhau.
Thay vì đoán, hãy bắt một callback thật và để script tự chẩn đoán:
ngrok http 3000 # terminal 1
MOMO_ACCESS_KEY=... MOMO_SECRET_KEY=... npm run example:ipn # terminal 2Script thử 4 giả thuyết và in ra đúng dòng config bạn cần:
✓ MATCH aiov2 (SDK default)
ipnSignatureFields: ["accessKey","amount","extraData",...]Nếu merchant account của bạn dùng thứ tự khác, truyền ipnSignatureFields vào config là xong.
Đã lưu payload rồi thì chạy offline: npm run example:ipn -- ipn.json
Chạy thử toàn bộ luồng
MOMO_PARTNER_CODE=... MOMO_ACCESS_KEY=... MOMO_SECRET_KEY=... \
MOMO_IPN_URL=https://<ngrok>.ngrok.io npm run example:sandboxChạy lần lượt create → query → gửi lại trùng requestId để kiểm chứng cơ chế idempotency.
Câu hỏi thường gặp
Package có gửi thông tin đơn hàng của tôi đi đâu không?
Không. Chỉ có đúng một chỗ gọi mạng trong toàn bộ source, và nó chỉ gọi tới host MoMo. Xem Cam kết minh bạch để tự kiểm chứng bằng grep.
Tôi có phải trả phí không? Không. MIT license, miễn phí vĩnh viễn, không có bản trả phí.
Đây có phải SDK chính thức của MoMo không? Không. Đây là package cộng đồng, không liên kết với M_Service. Tài liệu chính thức luôn là developers.momo.vn.
Dùng được với NestJS / Express / Fastify / Hono không? Được hết. Package không phụ thuộc framework nào — chỉ là một class bình thường.
Node bao nhiêu trở lên?
Node 20+. Dùng fetch và node:crypto có sẵn.
CommonJS có dùng được không?
Được. Package build cả ESM lẫn CJS, và đã kiểm tra bằng attw.
Nếu MoMo đổi API thì sao?
Toàn bộ field allowlist nằm trong src/constants.ts, một chỗ duy nhất. Bạn sửa được ngay, hoặc mở issue. Tài liệu được đối chiếu vào tháng 7/2026.
Tôi tìm thấy chỗ SDK làm sai so với tài liệu MoMo?
Rất mong bạn mở issue. Kèm tên endpoint và chuỗi ký bạn mong đợi (đừng kèm secretKey).
Ủng hộ dự án
Package này viết ra để giải quyết một việc lặp đi lặp lại mà nhiều dev Việt Nam đều gặp. Nếu nó giúp bạn tiết kiệm được một buổi chiều dò chuỗi ký, đây là những cách ủng hộ — tất cả đều miễn phí:
⭐ Cho một star
github.com/chienpv-dgh/momo-vn — star giúp package dễ được tìm thấy hơn, và đó là động lực thật.
🐛 Báo lỗi
Gặp signature mismatch ở endpoint nào? resultCode lạ? Tài liệu MoMo mâu thuẫn? Mở issue. Mỗi báo cáo giúp người sau đỡ mất thời gian.
Khi báo lỗi, tuyệt đối không dán secretKey, accessKey, hay chữ ký thật. Tên field và thông báo lỗi là đủ.
🔧 Đóng góp code
Những phần đang cần người:
- Xác nhận thứ tự chữ ký IPN bằng callback thật từ sandbox — đây là ẩn số lớn nhất còn lại
- Endpoint POS / QuickPay (
SIGNATURE_FIELDS.posđã có, chỉ thiếu hàm wrap) - Tokenization / binding (
linkWallet, thanh toán bằng token) nestjs-momo— adapter NestJS vớiforRoot/forRootAsync
git clone https://github.com/chienpv-dgh/momo-vn.git
cd momo-vn && npm install
npm test # 65 test, chạy trong ~120ms
npm run typecheck
npm run build && npm run attw📢 Chia sẻ
Biết ai đang vật lộn với tích hợp MoMo? Gửi link giúp họ. Đó là cách ủng hộ hiệu quả nhất.
☕ Mời cà phê
Nếu bạn muốn ủng hộ bằng vật chất — hoàn toàn tuỳ tâm, và không ảnh hưởng gì tới việc package luôn miễn phí và mở:
(đang cập nhật)
Đóng góp & phát triển
Mọi PR đều được hoan nghênh. Yêu cầu duy nhất: có test. Package này xử lý tiền của người khác, nên mỗi thay đổi liên quan tới chữ ký hay resultCode cần một test chứng minh nó đúng.
Giấy phép
MIT — dùng thoải mái, kể cả cho mục đích thương mại.
Miễn trừ trách nhiệm
Package cộng đồng, không liên kết với MoMo / M_Service. Toàn bộ field allowlist và bảng resultCode được đối chiếu với tài liệu developers.momo.vn/v3 vào tháng 7/2026. MoMo có thể thay đổi API — hãy tự kiểm chứng trên sandbox trước khi lên production. Tác giả không chịu trách nhiệm với thiệt hại phát sinh từ việc sử dụng package này.
