Trang thanh toán: tạo link là thu tiền
Video hướng dẫn
Xem transcript video (9 phần)
Tạo link, xác nhận đúng
Bạn muốn gửi một link để khách quét QR nhưng vẫn cần hệ thống xác nhận đúng đơn, đúng tiền và không tin nhầm trang chuyển hướng. Video này mình tạo trang thanh toán được lưu trữ sẵn, gọi tắt là checkout, nhận checkout_url, xử lý CHECKOUT_PAID và kiểm chữ ký khi khách quay lại. Mình cũng chạy cả ca đủ tiền, thiếu tiền và hết hạn.
Thiết lập hồ sơ thanh toán
Trước tiên thiết lập hồ sơ thanh toán một lần bằng PUT /api/v1/payment-profile, gồm tên shop, tài khoản nhận mặc định và bộ thông tin tạo QR. Response trả return_signature_secret đúng một lần khi tạo hoặc xoay. Nếu chưa có hồ sơ, tạo checkout trả bốn trăm hai mươi hai với payment_profile_missing. Bạn cũng thiết lập được ở dashboard, mục Cài đặt rồi Trang thanh toán.
Tạo checkout cho đơn
Cho từng đơn, gọi POST /api/v1/checkouts với Bearer, X-Client-Secret và Idempotency-Key. Body mẫu gồm amount, order_code, description, return_url, cancel_url, email người trả và expires_in. amount từ một nghìn đến một tỷ đồng. Mã đơn dài tối đa năm mươi ký tự. Hai URL quay về phải dùng HTTPS. expires_in mặc định chín trăm giây.
Đưa khách sang trang QR
Response HTTP hai trăm lẻ một trả checkout_url dạng https://pay.monapay.vn/c/<token>. Bạn chuyển hướng trình duyệt hoặc gửi link qua kênh đang bán hàng. Khách không cần đăng nhập, chỉ thấy VietQR, số tiền, mã đơn và thời gian còn lại. Các endpoint public đọc trạng thái và ảnh QR dùng token ngẫu nhiên ba mươi hai ký tự, không lộ ID nội bộ hay email người trả. API trạng thái còn trả paid_at, expires_at và seconds_left.
Xử lý CHECKOUT_PAID
Nguồn xác nhận đơn là webhook CHECKOUT_PAID hoặc GET /checkouts/{id}, không phải query string khi trình duyệt quay lại. Payload có checkout_id, order_code, status, amount, paid_amount, paid_at và transaction_code. Kiểm X-Mona-Signature trên raw body, lưu transaction_code bằng khóa UNIQUE, rồi gọi GET kiểm status là paid trước khi đổi trạng thái đơn.
Kiểm chữ ký redirect
Khi khách quay lại return_url, MONA Pay nối monapay_checkout, order_code, status, ts và sig sau khoảng ba giây. Chữ ký là HMAC-SHA256 của chuỗi ID, mã đơn, chữ paid và timestamp, ngăn cách bằng dấu gạch đứng. So sig bằng return_signature_secret, từ chối timestamp lệch quá năm phút, rồi vẫn gọi GET để kiểm tra. Redirect hủy về cancel_url không có chữ ký.
Hết hạn và thiếu tiền
Checkout mặc định hết hạn sau mười lăm phút và không tạo QR mới. Nếu khách trả muộn, giao dịch vẫn được ghi nhận và phiên có thể thành paid cùng paid_late bằng true; bạn tự quyết định nhận đơn hay hoàn tiền. Với trả thiếu, status vẫn pending, paid_amount là null và partial_amount là tổng đã nhận. Checkout chỉ thành paid khi tổng nhận lớn hơn hoặc bằng số tiền yêu cầu. Chỉ phát hiện thiếu khi status là pending và partial_amount lớn hơn không.
Thử sandbox và giữ Secret
Muốn thử không chuyển tiền, thêm sandbox bằng true khi tạo phiên rồi dùng giao dịch sandbox nạp đủ hoặc thiếu. Nếu mất return_signature_secret, gọi endpoint reveal với mật khẩu hoặc mã TOTP để xem lại; nếu nghi lộ thì xoay Secret và cập nhật biến môi trường. Luôn dùng Idempotency-Key để request thử lại không tạo hai phiên cho một đơn. Đừng giao hàng chỉ vì URL có status=paid.
Mở docs và chạy thử
Luồng năm bước, cURL, Node, Python và payload mẫu nằm tại monapay.vn/docs/api/trang-thanh-toan. Bạn đăng ký tại my.monapay.vn, thiết lập hồ sơ rồi tạo một checkout sandbox để đi hết luồng trước khi thu tiền thật. Nếu còn vướng chữ ký quay về hoặc trạng thái thiếu tiền, gọi tổng đài một chín không không, sáu ba sáu, sáu bốn tám để đội MONA hỗ trợ.
Muốn thu một khoản tiền, bạn gọi POST /api/v1/checkouts, lấy checkout_url rồi gửi link cho khách hoặc chuyển hướng trình duyệt sang link đó. Khách quét VietQR, tiền vào thẳng tài khoản ngân hàng của bạn. MONA Pay xác nhận giao dịch, gửi webhook CHECKOUT_PAID và đưa khách về return_url.
Webhook hoặc
GET /checkouts/{id}là nguồn sự thật trước khi giao hàng. Redirect về website chỉ phục vụ trải nghiệm người dùng.
Luồng tích hợp
- Thiết lập hồ sơ thanh toán một lần. Gọi
PUT /payment-profilevới tên shop, tài khoản ngân hàng hoặc VA mặc định và bộ thông tin tạo QR. API trảreturn_signature_secretđúng một lần khi tạo hoặc xoay secret. - Tạo phiên cho từng đơn. Gọi
POST /checkoutsvới số tiền, mã đơn vàreturn_url. Gửi cảX-Client-SecretvàIdempotency-Keyđể tránh tạo trùng khi request được thử lại. - Đưa khách sang trang thanh toán. Chuyển hướng trình duyệt hoặc gửi
checkout_urlqua Zalo, Facebook, email. Link có dạnghttps://pay.monapay.vn/c/<token>và không yêu cầu đăng nhập. - Chỉ xác nhận đơn khi nhận
CHECKOUT_PAID. Kiểm chữ ký HMAC webhook trên raw body, chống trùng bằngtransaction_code, so lại mã đơn và số tiền. Có thể gọiGET /checkouts/{id}để đối soát server-side. - Xác minh redirect khi khách quay lại. Kiểm
sigbằngreturn_signature_secret, kiểm thời giants, rồi gọi lạiGET /checkouts/{id}. Không giao hàng chỉ dựa vào query stringstatus=paid.
Phiên mặc định hết hạn sau 900 giây. MONA Pay chỉ đánh dấu paid khi tổng tiền khớp lớn hơn hoặc bằng số tiền của checkout. partial_amount là tổng tiền đã nhận được cho phiên, không chỉ là phần tiền thiếu. Ví dụ, checkout 250.000đ nhận 200.000đ vẫn có status: "pending", paid_amount: null, partial_amount: 200000; checkout 680.000đ nhận đủ có status: "paid", paid_amount: 680000 và partial_amount: 680000. Muốn phát hiện thiếu tiền, hãy kiểm tra status == "pending" && partial_amount > 0, không chỉ kiểm tra partial_amount > 0.
Endpoint
Base API: https://api.monapay.vn/api/v1.
Hồ sơ thanh toán
| Method | Endpoint | Auth | Dùng để |
|---|---|---|---|
GET |
/payment-profile |
Bearer | Đọc hồ sơ và tài khoản nhận tiền mặc định |
PUT |
/payment-profile |
Bearer + X-Client-Secret |
Tạo hoặc cập nhật hồ sơ; secret redirect chỉ hiện một lần |
POST |
/payment-profile/reveal-return-secret |
Bearer + X-Client-Secret |
Xem lại return_signature_secret sau khi xác nhận mật khẩu hoặc 2FA |
POST |
/payment-profile/rotate-return-secret |
Bearer + X-Client-Secret |
Xoay return_signature_secret |
Nếu chưa có hồ sơ, tạo checkout trả HTTP 422 với detail: "payment_profile_missing". Bạn thiết lập ở dashboard, mục Cài đặt → Trang thanh toán, hoặc gọi MCP tool monapay_set_payment_profile.
Mất return_signature_secret?
Gọi POST /api/v1/payment-profile/reveal-return-secret với password hoặc totp_code để xác nhận đúng chủ tài khoản rồi xem lại secret. Nếu không thể xác nhận hoặc nghi secret đã lộ, gọi /payment-profile/rotate-return-secret để xoay; lưu secret mới ngay và cập nhật biến môi trường trước khi xác minh redirect tiếp theo.
Phiên thanh toán của merchant
| Method | Endpoint | Dùng để |
|---|---|---|
POST |
/checkouts |
Tạo checkout, cần Idempotency-Key, trả HTTP 201 |
GET |
/checkouts?status&order_code&from_date&to_date&page&limit |
Lọc và phân trang checkout |
GET |
/checkouts/{id} |
Đọc một checkout theo ID |
POST |
/checkouts/{id}/cancel |
Huỷ phiên đang pending |
POST |
/checkouts/{id}/expire-now |
Hết hạn ngay, chỉ dành cho admin hoặc test |
amount là số nguyên VND từ 1.000 đến 1.000.000.000. order_code dài 1 đến 50 ký tự, chỉ gồm chữ, số, _, -, và không trùng trong các phiên đang chờ. return_url và cancel_url phải dùng HTTPS. expires_in nhận 60 đến 86.400 giây, mặc định 900 giây. metadata tối đa 2 KB.
Muốn thử mà chưa nối ngân hàng hoặc không chuyển tiền thật, thêm sandbox: true; xem Sandbox để tạo giao dịch đủ tiền, thiếu tiền và gửi lại.
Endpoint public cho trang checkout
Các endpoint sau không cần auth, chỉ nhận token ngẫu nhiên 32 ký tự. Response không lộ client ID, ID nội bộ hoặc email người trả.
| Method | Endpoint | Dùng để |
|---|---|---|
GET |
/checkouts/public/{token} |
Dữ liệu hiển thị trang thanh toán |
GET |
/checkouts/public/{token}/status |
Poll trạng thái, paid_at, expires_at, seconds_left |
GET |
/checkouts/public/{token}/qr.png?size=512 |
Ảnh QR PNG, cache 5 phút |
POST |
/checkouts/public/{token}/cancel |
Huỷ phiên đang chờ và về cancel_url |
GET |
/qr/{qr_id}/image.png?size=512 |
Ảnh PNG cho mọi QR đã tạo |
Public API có Cache-Control: no-store, trừ ảnh QR, và giới hạn 60 request mỗi phút trên mỗi IP.
cURL: tạo phiên thanh toán
Ví dụ đã có Bearer token trong $TOKEN và client secret trong $MONAPAY_CLIENT_SECRET:
Node.js: tạo phiên, xử lý CHECKOUT_PAID, kiểm redirect
MONA_WEBHOOK_SECRET là secret của cấu hình webhook. MONAPAY_RETURN_SECRET là return_signature_secret của hồ sơ thanh toán. Đây là hai secret khác nhau.
Python: tạo phiên, xử lý CHECKOUT_PAID, kiểm redirect
Payload CHECKOUT_PAID
Event dùng cùng chữ ký X-Mona-Signature và X-Mona-Timestamp như webhook giao dịch hiện có. Payload chứa dữ liệu checkout rút gọn, order_code, amount, paid_amount, paid_at và transaction_code. Lưu transaction_code bằng unique constraint để việc gửi lại webhook không xử lý đơn hai lần.
Redirect về website
Sau khi checkout chuyển sang paid, trang thanh toán đợi khoảng 3 giây rồi nối các tham số sau vào return_url:
Chữ ký là HMAC-SHA256 dạng hex với message <id>|<order_code>|paid|<ts>. Khi khách huỷ, MONA Pay về cancel_url?monapay_checkout=<id>&status=cancelled; redirect huỷ không có chữ ký.
Câu hỏi thường gặp
Tiền có đi qua MONA Pay không?
Không. Tiền đi thẳng từ tài khoản ngân hàng của khách vào tài khoản ngân hàng của bạn. MONA Pay tạo QR, nhận báo có và xác nhận giao dịch, không giữ tiền.
Checkout hết hạn thì sao?
Phiên mặc định hết hạn sau 15 phút và không tạo QR mới. Nếu khách chuyển sau hạn, MONA Pay vẫn ghi nhận giao dịch, đổi phiên sang paid với paid_late=true; merchant tự quyết định nhận đơn hay hoàn tiền.
Khách chuyển thiếu tiền thì sao?
partial_amount là tổng tiền đã nhận được cho phiên. Khi checkout 250.000đ mới nhận 200.000đ, phiên có status: "pending", paid_amount: null và partial_amount: 200000; khi checkout 680.000đ nhận đủ, cả paid_amount và partial_amount đều là 680000. Vì vậy, hãy phát hiện thiếu tiền bằng status == "pending" && partial_amount > 0, không chỉ bằng partial_amount > 0.
Không có website vẫn tạo link thu tiền được không?
Được. Bạn tạo link trong dashboard hoặc dùng monapay_create_checkout, rồi gửi checkout_url qua Zalo, Facebook hoặc email. Khách mở link, quét QR và thanh toán, không cần tài khoản MONA Pay.