npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-vn

Package 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

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.payUrl

Khô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 -u

Kết quả bạn sẽ thấy đúng hai domain: https://test-payment.momo.vnhttps://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:cryptofetch 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 windowsecretKey không thể lọt vào bundle frontend |

Nguyên tắc nền

  1. 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.
  2. 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á.
  3. 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.
  4. 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-vn
import { 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ọi query() để 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, storeId nằ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ẳng payWithATM / payWithCC tạ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ạy npm run example:sandbox với requestType tươ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: false mà không gọi confirm() 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 2

Script 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:sandbox

Chạ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 fetchnode: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ới forRoot/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.