Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Testing MoMo, ZaloPay sandbox

Mở đầu — vì sao bài này quan trọng

Ở bài trước bạn đã làm quen với VNPay sandbox — cổng thanh toán "quốc dân" gắn liền với thẻ ATM nội địa và QR ngân hàng. Nhưng nếu bạn đang test một ứng dụng thương mại điện tử hay giao đồ ăn ở Việt Nam năm 2026, gần như chắc chắn sản phẩm của bạn sẽ tích hợp thêm ví điện tử: MoMo và ZaloPay. Đây là hai ví có thị phần lớn nhất, và cách chúng vận hành khác VNPay ở vài điểm rất quan trọng mà nếu không nắm, bạn sẽ ngồi debug hàng giờ mà không hiểu tại sao request luôn trả về "chữ ký không hợp lệ".

Điểm khác biệt lớn nhất so với VNPay là cả MoMo và ZaloPay đều dùng HMAC-SHA256 để ký request (VNPay cũng vậy ở v2, nhưng cách xây chuỗi ký hoàn toàn khác nhau), và mỗi ví có một "quy tắc ghép chuỗi trước khi ký" riêng biệt. Sai một dấu &, sai thứ tự field, hay ký nhầm bằng key khác là đủ để cả luồng thanh toán chết. Đây chính là loại lỗi mà con người rất dễ mắc và rất khó phát hiện bằng mắt — nghĩa là nơi Postman toả sáng nhất, vì ta có thể tự động hoá việc tạo chữ ký trong pre-request script và assert kết quả một cách chính xác.

Trong bài này, chúng ta tập trung riêng vào MoMo và ZaloPay sandbox — cách lấy credential test, cách dựng request tạo đơn (create order), cách tự sinh signature bằng crypto-js trong Postman, cách xử lý luồng redirect/callback, và những cái bẫy đặc thù của từng ví. VNPay đã có bài riêng (Bài 36), nên ở đây tôi sẽ không lặp lại phần đó.

Khái niệm cốt lõi

Kiến trúc chung của một luồng thanh toán ví

Cả MoMo và ZaloPay đều theo mô hình ba bên: merchant server → payment gateway → user. Là QA, bạn cần phân biệt rõ hai loại API:

  • API tạo đơn (server-to-server): merchant server gọi tới gateway để tạo một giao dịch, nhận về một payUrl / order_url hoặc deeplink. Đây chính là request bạn test bằng Postman nhiều nhất.
  • API callback / IPN (Instant Payment Notification): sau khi user thanh toán xong, gateway chủ động gọi ngược về server merchant để báo kết quả. Đây là request bạn không tạo ra, mà phải giả lập nhận hoặc kiểm tra logic xác thực chữ ký của nó.
Postman test rất tốt phần tạo đơn. Phần callback thì bạn cần một endpoint công khai (dùng ngrok, webhook.site, hoặc Postman Mock Server) để hứng — chủ đề webhook có bài riêng (Bài 45), nên ở đây tôi chỉ chạm tới mức đủ để bạn hình dung luồng.

MoMo One Time Payment (AIO)

MoMo cung cấp bộ API "All-In-One" (AIO). Endpoint sandbox tạo đơn là:

POST https://test-payment.momo.vn/v2/gateway/api/create

Request body là JSON, các field bắt buộc gồm:

{
  "partnerCode": "MOMO",
  "requestId": "MOMO1717000000001",
  "amount": "50000",
  "orderId": "MOMO1717000000001",
  "orderInfo": "Thanh toan don hang test",
  "redirectUrl": "https://webhook.site/your-uuid",
  "ipnUrl": "https://webhook.site/your-uuid",
  "requestType": "captureWallet",
  "extraData": "",
  "lang": "vi",
  "signature": "<HMAC-SHA256>"
}

Điểm mấu chốt là signature. MoMo yêu cầu bạn ghép các field theo đúng thứ tự alphabet của tên field, dạng key=value nối bằng &, rồi ký HMAC-SHA256 với secretKey. Chuỗi cần ký (raw signature) cho request create có dạng:

accessKey=$accessKey&amount=$amount&extraData=$extraData&ipnUrl=$ipnUrl&orderId=$orderId&orderInfo=$orderInfo&partnerCode=$partnerCode&redirectUrl=$redirectUrl&requestId=$requestId&requestType=$requestType

Credential sandbox công khai MoMo cấp cho mọi người học là: partnerCode = MOMO, accessKey = F8BBA842ECF85, secretKey = K951B6PE1waDMi640xX08PD3vg6EkVlz. Bạn có thể dùng ngay để thử.

ZaloPay create order

ZaloPay dùng endpoint sandbox:

POST https://sb-openapi.zalopay.vn/v2/create

Khác MoMo, ZaloPay gửi dữ liệu dạng form-urlencoded (không phải JSON body thuần), và chuỗi ký của nó ghép theo thứ tự cố định chứ không theo alphabet:

mac = HMAC-SHA256( key1, app_id|app_trans_id|app_user|amount|app_time|embed_data|item )

Dấu phân cách là ký tự | (pipe), không phải &. Đây là điểm cực kỳ hay sai khi chuyển từ test MoMo sang ZaloPay. Ngoài ra app_trans_id của ZaloPay bắt buộc có tiền tố ngày dạng yymmdd_, ví dụ 260627_123456. Nếu thiếu tiền tố này, ZaloPay trả lỗi ngay dù chữ ký đúng.

Credential sandbox ZaloPay công khai: app_id = 2554, key1 = sdngKKJmqEMzvh5QQcdD2A9XBSKUNaYn, key2 = trMrHtvjo6myautxDUiAcYsguKKh... (key2 dùng cho callback).

Tình huống thực tế

Ví dụ 1 — Tiki tích hợp MoMo và cái bẫy field alphabet

Giả sử bạn là QA tại một sàn thương mại điện tử kiểu Tiki. Team backend vừa hoàn thành API POST /checkout/momo gói lại việc gọi MoMo AIO. Bạn viết một collection Postman để test đầu-cuối và gặp lỗi: MoMo trả về resultCode: 20, message "Bad format request" — nhưng đôi khi lại là resultCode: 40, "RequestId already exists".

Điều tra kỹ, bạn phát hiện hai vấn đề. Thứ nhất, dev đã ghép chuỗi ký theo thứ tự field trong JSON body thay vì theo alphabet, khiến MoMo tính ra chữ ký khác. Thứ hai, requestId bị hardcode nên chạy lần thứ hai là trùng. Bài học rút ra: với MoMo, luôn sinh requestId/orderId bằng Date.now() trong pre-request script, và tuyệt đối ghép chuỗi ký theo alphabet. Sau khi tự sinh signature bằng Postman và so sánh với chữ ký của server, bạn chứng minh được lỗi nằm ở server chứ không phải ở MoMo — tiết kiệm cho team cả buổi tranh cãi.

Ví dụ 2 — Baemin/ShopeeFood và app_trans_id sai định dạng của ZaloPay

Một team làm app giao đồ ăn (tạm gọi FoodViet) tích hợp ZaloPay. QA test trên sandbox, chữ ký mac đã đúng, nhưng ZaloPay luôn trả return_code: 2, return_message: "app_trans_id không hợp lệ". Cả team bối rối vì "mã giao dịch nào chả được".

Hoá ra ZaloPay quy định app_trans_id phải bắt đầu bằng ngày hiện tại theo giờ Việt Nam dạng yymmdd_. Test case của QA sinh app_trans_id = "txn_123456" — thiếu tiền tố ngày. Tệ hơn, một test case chạy lúc 23h58 sinh tiền tố 260627_, nhưng khi ZaloPay xử lý đã sang ngày 260628, gây lỗi ngắt quãng khó tái hiện. Bài học: sinh app_trans_id bằng pre-request script dựa trên ngày server ZaloPay (UTC+7), và với các test chạy gần nửa đêm cần chú ý biên ngày. Đây là ví dụ điển hình về bug "phụ thuộc thời gian" mà chỉ Postman với pre-request script động mới bắt được ổn định.

Ví dụ 3 — Xác thực callback bị bỏ qua ở một startup fintech

Một startup fintech tích hợp cả MoMo lẫn ZaloPay. Team dev để endpoint IPN nhận callback và... cập nhật đơn "đã thanh toán" ngay khi nhận request, chưa verify chữ ký. QA phát hiện lỗ hổng nghiêm trọng: bất kỳ ai POST một payload giả tới IPN url đều có thể đánh dấu đơn đã trả tiền.

QA dựng một request Postman giả lập callback MoMo với resultCode: 0 nhưng chữ ký sai/thiếu, gửi thẳng vào IPN url (lấy qua ngrok). Hệ thống vẫn ghi nhận "thành công" — chứng minh lỗ hổng. Sau khi dev thêm bước verify HMAC bằng key2 (ZaloPay) và secretKey (MoMo), QA lặp lại test và xác nhận request giả bị từ chối. Bài học: callback phải luôn được verify chữ ký ở phía server, và QA có trách nhiệm chủ động tấn công chính endpoint đó bằng Postman.

Hướng dẫn từng bước

Tôi sẽ hướng dẫn dựng một request tạo đơn MoMo hoàn chỉnh trong Postman; ZaloPay làm tương tự với khác biệt đã nêu.

Bước 1 — Tạo Environment. Tạo environment momo-sandbox với các biến: momo_endpoint = https://test-payment.momo.vn/v2/gateway/api/create, partnerCode = MOMO, accessKey = F8BBA842ECF85, secretKey = K951B6PE1waDMi640xX08PD3vg6EkVlz. Lưu ý: secretKey nên đặt là biến kiểu secret (Postman có type này) để không lộ khi share.

Bước 2 — Tạo request POST tới {{momo_endpoint}}, header Content-Type: application/json.

Bước 3 — Viết pre-request script để sinh dữ liệu động và ký. Postman sandbox có sẵn CryptoJS:

const requestId = 'REQ' + Date.now();
const orderId   = 'ORD' + Date.now();
const amount    = '50000';
const orderInfo = 'Thanh toan don hang test';
const redirectUrl = 'https://webhook.site/your-uuid';
const ipnUrl      = 'https://webhook.site/your-uuid';
const requestType = 'captureWallet';
const extraData   = '';

const accessKey = pm.environment.get('accessKey'); const secretKey = pm.environment.get('secretKey'); const partnerCode = pm.environment.get('partnerCode');

// Ghép theo ĐÚNG thứ tự alphabet của field const rawSignature = accessKey=${accessKey}&amount=${amount}&extraData=${extraData} + &ipnUrl=${ipnUrl}&orderId=${orderId}&orderInfo=${orderInfo} + &partnerCode=${partnerCode}&redirectUrl=${redirectUrl} + &requestId=${requestId}&requestType=${requestType};

const signature = CryptoJS.HmacSHA256(rawSignature, secretKey).toString();

pm.environment.set('requestId', requestId); pm.environment.set('orderId', orderId); pm.environment.set('signature', signature); // lưu các biến khác để dùng trong body pm.environment.set('amount', amount); pm.environment.set('orderInfo', orderInfo); pm.environment.set('redirectUrl', redirectUrl); pm.environment.set('ipnUrl', ipnUrl); pm.environment.set('requestType', requestType); pm.environment.set('extraData', extraData);

Bước 4 — Body (raw JSON) dùng các biến vừa set:

{
  "partnerCode": "{{partnerCode}}",
  "requestId": "{{requestId}}",
  "amount": "{{amount}}",
  "orderId": "{{orderId}}",
  "orderInfo": "{{orderInfo}}",
  "redirectUrl": "{{redirectUrl}}",
  "ipnUrl": "{{ipnUrl}}",
  "requestType": "{{requestType}}",
  "extraData": "{{extraData}}",
  "lang": "vi",
  "signature": "{{signature}}"
}

Bước 5 — Tests script để assert kết quả:

pm.test('HTTP 200', () => pm.response.to.have.status(200));

const body = pm.response.json(); pm.test('MoMo resultCode = 0', () => { pm.expect(body.resultCode).to.eql(0); }); pm.test('Có payUrl để chuyển hướng', () => { pm.expect(body).to.have.property('payUrl'); pm.environment.set('payUrl', body.payUrl); });

Bước 6 — Chạy và mở payUrl. Copy payUrl ra trình duyệt, bạn sẽ thấy trang thanh toán sandbox MoMo. Để test thành công, dùng tài khoản test MoMo (SĐT 0909000000 hoặc số test MoMo cấp trong tài liệu Developer) hoặc mã QR test. Sau khi "thanh toán", MoMo gọi về ipnUrl — kiểm tra trên webhook.site để thấy payload callback.

Với ZaloPay: đổi endpoint sang https://sb-openapi.zalopay.vn/v2/create, dùng body form-urlencoded, và sửa pre-request script để ghép mac bằng dấu |:

const appId = 2554;
const appUser = 'user_test';
const amount = 50000;
const appTime = Date.now();
const embedData = '{}';
const items = '[]';
const key1 = pm.environment.get('zp_key1');

const d = new Date(); const yy = String(d.getFullYear()).slice(-2); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); const appTransId = ${yy}${mm}${dd}_${appTime};

const data = ${appId}|${appTransId}|${appUser}|${amount}|${appTime}|${embedData}|${items}; const mac = CryptoJS.HmacSHA256(data, key1).toString();

pm.environment.set('app_trans_id', appTransId); pm.environment.set('app_time', appTime); pm.environment.set('mac', mac); pm.environment.set('embed_data', embedData); pm.environment.set('item', items);

Response ZaloPay thành công trả return_code: 1 và một order_url để chuyển hướng.

Lỗi thường gặp & mẹo

1. Sai thứ tự ghép chuỗi ký. Đây là lỗi số một. MoMo ghép theo alphabet + dấu &; ZaloPay ghép theo thứ tự cố định + dấu |. Đừng bao giờ copy-paste logic ký của ví này sang ví kia. Mẹo: in rawSignature/data ra bằng console.log() và soi trong Postman Console (View → Show Postman Console) để so từng ký tự.

2. Ký nhầm key. MoMo dùng secretKey để ký. ZaloPay dùng key1 để ký request create, còn key2 để verify callback. Trộn lẫn hai key là lỗi kinh điển khi làm phần callback.

3. amount sai kiểu. MoMo yêu cầu amount là chuỗi số nguyên (đơn vị VND, không thập phân), tối thiểu 1.000đ, tối đa 50.000.000đ trên sandbox. Gửi 50000.0 hoặc số âm sẽ bị từ chối. Với ZaloPay, amount trong chuỗi ký phải khớp chính xác giá trị gửi lên.

4. requestId/orderId/app_trans_id trùng. Sandbox từ chối giao dịch trùng mã trong ngày. Luôn gắn Date.now(). Với ZaloPay nhớ tiền tố yymmdd_.

5. Quên rằng callback dùng thứ tự ký khác. Khi verify callback MoMo, chuỗi ký lại có tập field khác (gồm cả resultCode, transId, responseTime...), vẫn theo alphabet. Đọc kỹ tài liệu phần "signature for IPN", đừng dùng lại chuỗi ký của create.

6. webhook.site hết hạn hoặc bị chặn. URL webhook.site miễn phí có thời hạn; nếu callback không tới, kiểm tra URL còn sống không. Với môi trường thật, dùng ngrok trỏ về localhost hoặc Postman Mock Server.

Mẹo tổ chức: đặt logic sinh signature vào collection-level pre-request script hoặc một Postman variable dạng hàm, để mọi request MoMo tái sử dụng — tránh copy-paste (chủ đề tái sử dụng code có bài riêng, Bài 23). Với dữ liệu nhạy cảm như secretKey/key1, cân nhắc Postman Vault (Bài 21) thay vì để lộ trong environment share cho team.

Bài tập thực hành

  • Tạo đơn MoMo thành công: Dựng request create theo hướng dẫn với credential sandbox công khai. Assert resultCode === 0 và tồn tại payUrl. Mở payUrl và hoàn tất một giao dịch test, quan sát callback trên webhook.site.
  • Tái hiện lỗi chữ ký: Cố tình đảo thứ tự hai field trong rawSignature (ví dụ để amount sau orderId). Gửi request và ghi lại resultCode/message MoMo trả về. Viết một dòng giải thích tại sao lỗi này khó phát hiện bằng mắt.
  • ZaloPay biên ngày: Viết pre-request script sinh app_trans_id với tiền tố ngày theo giờ Việt Nam (UTC+7). Tạo một test case giả lập chạy lúc 23h59 và lập luận: điều gì xảy ra nếu server ZaloPay xử lý khi đã sang ngày mới?
  • Tấn công callback: Dùng ngrok tạo endpoint local, cấu hình làm ipnUrl. Gửi một request giả lập callback với resultCode: 0 nhưng signature rỗng. Kiểm tra hệ thống của bạn có verify chữ ký hay không, và viết một test đảm bảo request giả bị từ chối.
  • So sánh hai ví: Lập bảng đối chiếu MoMo vs ZaloPay theo 4 tiêu chí: định dạng body (JSON/form), ký tự nối chuỗi ký (&/|), thứ tự field (alphabet/cố định), key dùng để ký. Đây sẽ là "cheat sheet" cứu bạn mỗi lần chuyển ngữ cảnh giữa hai ví.

Tóm tắt

MoMo và ZaloPay là hai ví điện tử bạn gần như chắc chắn phải test khi làm sản phẩm ở Việt Nam, và cả hai đều xoay quanh một kỹ năng cốt lõi: tự sinh chữ ký HMAC-SHA256 chính xác trong Postman pre-request script. MoMo gửi JSON, ghép chuỗi ký theo alphabet nối bằng &, ký bằng secretKey; ZaloPay gửi form-urlencoded, ghép chuỗi ký theo thứ tự cố định nối bằng |, ký bằng key1 (và key2 cho callback), với app_trans_id bắt buộc tiền tố yymmdd_. Ba cái bẫy lớn nhất là sai thứ tự/ký tự nối chuỗi ký, trùng mã giao dịch, và bỏ qua việc verify chữ ký callback — cả ba đều là nơi Postman với script động giúp bạn phát hiện lỗi mà mắt thường bỏ sót. Nắm chắc bài này, bạn đã có nền tảng test toàn bộ hệ sinh thái thanh toán ví Việt Nam, sẵn sàng ghép vào một bộ test suite lớn hơn ở các dự án capstone phía sau.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi