# MONA Pay — toàn văn tài liệu tích hợp > MONA Pay là cổng thanh toán và API ngân hàng của The MONA Group, giúp doanh nghiệp Việt Nam nhận và xác nhận tiền chuyển khoản theo thời gian thực qua tài khoản ảo (VA), VietQR, webhook và Telegram — thiết kế để cả lập trình viên lẫn AI agent tích hợp trong vài phút. Base URL API: https://api.monapay.vn · Dashboard: https://my.monapay.vn · Hotline 1900 636 648 · info@themona.global --- # Tài liệu webhook và API ngân hàng MONA Pay > MONA Pay báo tiền vào tài khoản ACB theo thời gian thực qua webhook, Telegram và API. Cách hoạt động, tính năng và trang tài liệu tương ứng. > URL: https://monapay.vn/docs · Markdown: https://monapay.vn/docs.md MONA Pay đọc thông báo giao dịch từ ngân hàng ACB ngay khi tiền vào tài khoản của anh chị, rồi bắn webhook về máy chủ của anh chị hoặc nhắn vào nhóm Telegram. Tiền không đi qua MONA Pay, vẫn vào thẳng tài khoản ngân hàng như bình thường. Tích hợp cơ bản mất khoảng 5 phút nếu anh chị đã có tài khoản ACB và một URL nhận webhook. > MONA Pay là cổng thanh toán và API ngân hàng của The MONA Group, giúp doanh nghiệp Việt Nam nhận và xác nhận tiền chuyển khoản theo thời gian thực qua tài khoản ảo (VA), VietQR, webhook và Telegram — thiết kế để cả lập trình viên lẫn AI agent tích hợp trong vài phút. ## MONA Pay hoạt động thế nào Toàn bộ luồng chỉ có 4 bước. Anh chị không cần thay đổi gì ở phía ngân hàng ngoài việc đăng ký nhận thông báo (làm một lần, có OTP của ACB). ```text [1] Khách chuyển khoản [2] ACB báo giao dịch [3] MONA Pay ghi nhận [4] Hệ thống của anh chị vào tài khoản ACB ───▶ cho MONA Pay ───▶ và gửi thông báo ───▶ tự xác nhận đơn hàng (VA hoặc quét VietQR) (theo thời gian thực) webhook / Telegram (không cần người canh app) ``` 1. **Khách chuyển khoản.** Khách chuyển vào số tài khoản ảo (VA) gắn với đơn hàng, hoặc quét mã VietQR đã có sẵn số tiền và nội dung. 2. **ACB báo cho MONA Pay.** Ngay khi tiền vào, ACB gửi thông báo giao dịch sang MONA Pay. Đây là kết nối chính thức, anh chị đăng ký một lần trong dashboard bằng OTP ACB gửi về số điện thoại. 3. **MONA Pay ghi nhận và gửi thông báo.** Giao dịch được lưu vào dashboard, sau đó MONA Pay POST một gói JSON tới URL webhook của anh chị (ký HMAC-SHA256 nếu bật) và nhắn vào Telegram nếu có cấu hình. 4. **Hệ thống của anh chị tự xử lý.** Web bán hàng, phần mềm quản lý hay file Google Sheet nhận webhook rồi tự đổi trạng thái đơn sang "đã thanh toán". Không ai phải mở app ngân hàng để dò từng khoản. Máy chủ của anh chị chỉ cần trả HTTP 200, 201 hoặc 202 trong vòng 10 giây là MONA Pay tính lần gửi đó thành công. Mỗi lần gửi đều có log riêng để anh chị xem lại. ## Tính năng và trang tài liệu tương ứng | Tính năng | Dùng khi nào | Trang tài liệu | |---|---|---| | Tài khoản ảo (VA) | Mỗi đơn hàng hoặc mỗi khách có một số tài khoản riêng để khớp tiền tự động | [Tài khoản ảo (VA)](/docs/api/tai-khoan-ao-va) | | VietQR động | Tạo mã QR đã có sẵn số tiền và nội dung, khách quét là chuyển đúng | [QR thanh toán](/docs/api/qr-thanh-toan) | | Webhook | Máy chủ của anh chị nhận thông báo tiền vào theo thời gian thực | [Tích hợp webhook](/docs/webhooks/tich-hop-webhook) | | Chữ ký HMAC | Xác minh webhook đúng là do MONA Pay gửi, chặn giả mạo và gửi lại gói cũ | [Bảo mật webhook](/docs/webhooks/bao-mat) | | Gửi lại và log | Xem từng lần gửi, lý do lỗi, gửi lại thủ công | [Gửi lại và xử lý lỗi](/docs/webhooks/gui-lai-va-xu-ly-loi) | | Đối soát | Kéo danh sách giao dịch theo trang để so với sổ của anh chị | [Đối soát giao dịch](/docs/webhooks/doi-soat) | | Telegram | Nhóm kế toán, chủ shop nhận tin nhắn tiền vào ngay trên điện thoại | [Telegram](/docs/telegram) | | API keys | Tạo và thu hồi khoá để gọi API từ máy chủ | [API keys](/docs/api/api-keys) | | Xác thực API | Đăng nhập lấy Bearer token, cách gửi X-Client-Secret | [Xác thực](/docs/api/xac-thuc) | | Địa chỉ IP | Mở tường lửa cho máy chủ gửi webhook của MONA Pay | [Địa chỉ IP](/docs/dia-chi-ip) | | So với cổng quốc tế | Vì sao PayPal, Stripe không hợp thu tiền trong nước | [Cổng thanh toán quốc tế](/cong-thanh-toan-quoc-te) | | AI agent | Prompt và hướng dẫn để Claude Code, Codex, Cursor tự tích hợp | [Dành cho AI agent](/docs/ai-agent) | ## Bắt đầu từ đâu - Chưa có tài khoản: đọc [Bắt đầu nhanh (5 phút)](/docs/bat-dau-nhanh). Trang này dẫn từ lúc đăng ký tới lúc nhận được webhook đầu tiên bằng tiền thật. - Chưa quen các khái niệm VA, VietQR, webhook, HMAC: đọc [Khái niệm](/docs/khai-niem) trước, viết cho cả người không làm kỹ thuật. - Đang cân nhắc PayPal, Stripe: đọc [So với cổng thanh toán quốc tế](/cong-thanh-toan-quoc-te) trước khi chọn. - Là AI agent hoặc muốn giao cho AI agent làm: mở [Dành cho AI agent](/docs/ai-agent), hoặc nạp thẳng `https://monapay.vn/llms-full.txt`. ## Tài liệu cho máy đọc Mọi trang docs đều có bản markdown thô: thêm đuôi `.md` vào URL (ví dụ `https://monapay.vn/docs/webhooks/tich-hop-webhook.md`). Ngoài ra có `https://monapay.vn/llms.txt` (mục lục), `https://monapay.vn/llms-full.txt` (toàn văn) và `https://monapay.vn/openapi.json` (đặc tả API v1). Agent nạp một trong các file này là đủ ngữ cảnh để viết code tích hợp. ## Thông tin hệ thống | Hạng mục | Giá trị | |---|---| | Base URL API | `https://api.monapay.vn` (alias cũ `https://ipn.mona.host` vẫn chạy) | | Dashboard | `https://my.monapay.vn` | | Ngân hàng hỗ trợ | ACB đang hoạt động; MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang đăng ký kết nối, bảng trạng thái tại [/ngan-hang](/ngan-hang) | | Điều kiện webhook thành công | HTTP 200, 201 hoặc 202 trong 10 giây | | Chữ ký webhook | HMAC-SHA256, header `X-Mona-Signature` và `X-Mona-Timestamp`, chống replay 5 phút | | IP máy chủ gửi webhook | `103.168.55.14` (kiểm 28/08/2026) | | Hỗ trợ | Tổng đài 1900 636 648, email info@themona.global | MONA Pay là sản phẩm của The MONA Group, thành lập năm 2016, đã làm 14.000+ dự án web và phần mềm. Hệ thống này tụi em tự xây để thu tiền cho chính mình, rồi từ năm 2022 tới nay hơn 6.000 khách hàng mới của MONA đã thu tiền qua đây trong web, phần mềm MONA giao; năm 2026 mở cho mọi doanh nghiệp. ## Câu hỏi nhanh **Tiền có đi qua MONA Pay không?** Không. Tiền vào thẳng tài khoản ACB của anh chị. MONA Pay chỉ nhận thông báo giao dịch từ ngân hàng rồi báo lại cho anh chị. **Cần biết lập trình mới dùng được không?** Không bắt buộc. Nếu chỉ cần biết tiền vào, anh chị bật thông báo Telegram trong dashboard là xong. Webhook và API dành cho web bán hàng hoặc phần mềm cần tự xác nhận đơn. **Tài khoản mới dùng được ngay chưa?** Được. Đăng ký xong đăng nhập ngay, tự tạo API key, không cần ai duyệt. Chỉ bước nối ACB cần OTP gửi về số điện thoại đăng ký với ngân hàng. MONA Pay miễn phí hoàn toàn, không giới hạn giao dịch (xem [bảng giá](/bang-gia)). **Có hỗ trợ ngân hàng ngoài ACB không?** ACB đang hoạt động; MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang trong quá trình đăng ký kết nối, trạng thái cập nhật tại [ngân hàng hỗ trợ](/ngan-hang). Webhook, Telegram, API dùng chung mọi ngân hàng, payload có `bank_name`, nối thêm ngân hàng không phải sửa tích hợp. ## Công cụ cho lập trình viên - [OpenAPI 3.1](/openapi.json) — import vào Postman, Insomnia, hoặc sinh SDK. - [Postman collection](/monapay.postman_collection.json) — 20 request sẵn biến `baseUrl`, `accessToken`, `clientSecret`. - [API reference tương tác](/docs/api-reference) — thử endpoint ngay trên trình duyệt. - SDK: [Node.js](https://github.com/themonagroup/monapay-node) · [Python](https://github.com/themonagroup/monapay-python) · [PHP](https://github.com/themonagroup/monapay-php) · [MCP server](https://github.com/themonagroup/monapay-mcp) · [CLI](https://github.com/themonagroup/monapay-cli) · [WooCommerce](https://github.com/themonagroup/woocommerce-monapay) · [Ví dụ 8 framework](https://github.com/themonagroup/monapay-examples) ## Deploy app có thu tiền bằng AI agent Cùng The MONA Group có [VibeCloud](https://vibecloud.vn): VPS và database cho AI agent tự tạo và deploy qua API (`https://api.vibecloud.vn`), tính tiền theo giờ. Luồng gọn: agent deploy app lên VibeCloud → cắm MONA Pay thu tiền → xong trong một phiên làm việc. Hướng dẫn máy đọc của VibeCloud: [vibecloud.vn/llms.txt](https://vibecloud.vn/llms.txt). --- # Bắt đầu nhanh với MONA Pay trong 5 phút > Từ đăng ký tài khoản tới nhận webhook đầu tiên bằng tiền thật. 6 bước, có code cURL, PHP, Node để dán vào dùng ngay. > URL: https://monapay.vn/docs/bat-dau-nhanh · Markdown: https://monapay.vn/docs/bat-dau-nhanh.md Để nhận thông báo tiền vào tài khoản ACB theo thời gian thực, anh chị cần 6 bước: đăng ký tài khoản (dùng ngay, không cần duyệt), đăng nhập lấy token, tạo API key, nối tài khoản ACB bằng OTP, khai báo URL webhook, rồi chuyển một khoản nhỏ để kiểm tra. Nếu đã có tài khoản ACB và một máy chủ nhận webhook, phần thao tác mất khoảng 5 phút. ## Chuẩn bị - Tài khoản ngân hàng ACB đứng tên anh chị hoặc doanh nghiệp, kèm số điện thoại đã đăng ký với ACB (để nhận OTP). - Một URL HTTPS trên máy chủ của anh chị để nhận webhook, ví dụ `https://ten-mien-cua-anh-chi.vn/webhook/monapay`. Chưa có máy chủ thì vẫn dùng được thông báo Telegram, xem trang [Telegram](/docs/telegram). - Công cụ gọi API: cURL, Postman, hoặc để AI agent gọi giùm theo [prompt có sẵn](/docs/ai-agent). ## Bước 1. Đăng ký tài khoản Vào `https://my.monapay.vn/auth`, chọn tab Đăng ký, điền tên đăng nhập, mật khẩu, tên và email. Anh chị cũng gọi được API: ```bash curl -X POST https://api.monapay.vn/api/v1/client/register-client \ -H 'Content-Type: application/json' \ -d '{"username":"shop-cua-toi","password":"mat-khau-manh","name":"Shop của tôi"}' ``` Tài khoản tạo xong dùng được ngay: đăng nhập liền ở bước 2, không cần ai duyệt. MONA Pay miễn phí hoàn toàn, không giới hạn giao dịch (xem [bảng giá](/bang-gia)). ## Bước 2. Đăng nhập lấy token ```bash curl -X POST https://api.monapay.vn/api/v1/client/login \ -H 'Content-Type: application/json' \ -d '{"username":"shop-cua-toi","password":"mat-khau-manh"}' ``` Kết quả trả về theo khung chung của mọi API: ```json { "success": true, "message": "Đăng nhập thành công", "data": { "access_token": "eyJhbGciOi...", "expires_in": 86400 } } ``` Từ đây mọi lệnh gọi API đều gửi kèm `Authorization: Bearer `. Chi tiết ở trang [Xác thực](/docs/api/xac-thuc). ## Bước 3. Tạo API key Các request POST, PUT, DELETE cần thêm header `X-Client-Secret`. Tạo khoá trong dashboard mục API Keys, hoặc: ```bash curl -X POST https://api.monapay.vn/api/v1/client-keys/generate \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"may-chu-production"}' ``` `client_secret` chỉ hiện đúng 1 lần trong response. Lưu vào biến môi trường ngay, mất thì tạo khoá mới và thu hồi khoá cũ. Xem [API keys](/docs/api/api-keys). ## Bước 4. Nối tài khoản ACB (4 bước, có OTP) Làm trong dashboard, mục Ngân hàng & VA, nút Thêm tài khoản: 1. Nhập số tài khoản ACB, số điện thoại đăng ký với ACB, loại khách hàng (cá nhân hoặc doanh nghiệp). 2. ACB gửi OTP về điện thoại. Nhập OTP để xác thực và tạo tài khoản ảo (VA) đầu tiên. 3. Đăng ký nhận thông báo giao dịch. ACB gửi OTP lần 2, nhập tiếp. 4. Xong. Từ lúc này tiền vào VA hoặc tài khoản là MONA Pay nhận được thông báo. Nếu muốn làm bằng API thay vì dashboard, xem [Tài khoản ảo (VA)](/docs/api/tai-khoan-ao-va), luồng cũng đúng 4 lệnh gọi này. ## Bước 5. Khai báo URL webhook Vào dashboard mục Webhooks, bấm Thêm webhook, điền URL, chọn kiểu xác thực HMAC_SHA256 và đặt secret. Hoặc gọi API: ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks \ -H "Authorization: Bearer $TOKEN" \ -H "X-Client-Secret: $CLIENT_SECRET" \ -H 'Content-Type: application/json' \ -d '{ "name": "Web bán hàng", "webhook_url": "https://ten-mien-cua-anh-chi.vn/webhook/monapay", "auth_type": "HMAC_SHA256", "secret_key": "secret_hmac_cua_anh_chi", "payload_format": "application/json" }' ``` Không truyền `virtual_account_id` thì webhook nhận mọi giao dịch của mọi tài khoản. Truyền id của một VA thì chỉ nhận giao dịch của VA đó. Phía máy chủ của anh chị, endpoint chỉ cần làm 3 việc: kiểm chữ ký, trả HTTP 200 ngay, rồi xử lý đơn sau. Code mẫu dán vào dùng được: ```php " // 1. Chặn replay: timestamp lệch quá 5 phút thì từ chối if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit('timestamp qua han'); } // 2. Verify chữ ký: HMAC-SHA256(secret, ".") $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret); if (!hash_equals($expected, $signature)) { // hash_equals chống timing attack http_response_code(401); exit('sai chu ky'); } // 3. Trả 200 ngay, việc nặng đẩy sang xử lý sau (queue/cron) http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); $data = json_decode($raw, true); // $data['amount'], $data['description'], $data['transaction_code'], ... // TODO: ghi nhận giao dịch + xử lý đơn hàng của anh chị tại đây ``` ```js // webhook-monapay.js: nhận webhook MONA Pay bằng Express const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.MONA_WEBHOOK_SECRET; // secret HMAC đặt khi bật HMAC // Phải đọc raw body thì chữ ký mới khớp (đừng dùng express.json() ở route này) app.post('/webhook/monapay', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.header('X-Mona-Timestamp') || ''; const signature = req.header('X-Mona-Signature') || ''; // "sha256=" const rawBody = req.body.toString('utf8'); // 1. Chặn replay: timestamp lệch quá 5 phút thì từ chối if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.status(400).send('timestamp qua han'); } // 2. Verify chữ ký: HMAC-SHA256(secret, ".") const expected = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(timestamp + '.' + rawBody) .digest('hex'); const hopLe = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!hopLe) return res.status(401).send('sai chu ky'); // 3. Trả 200 ngay, xử lý đơn hàng async sau đó res.status(200).send('OK'); const data = JSON.parse(rawBody); // data.amount, data.description, data.transaction_code, ... // TODO: ghi nhận giao dịch + xử lý đơn hàng của anh chị tại đây }); app.listen(3000, () => console.log('Webhook MONA Pay chạy ở cổng 3000')); ``` ## Bước 6. Gửi thử rồi chuyển tiền thật Trước hết bấm Gửi thử trong dashboard (mục Webhooks) hoặc gọi `POST /api/v1/client-webhooks/test`. MONA Pay bắn một gói mẫu tới URL của anh chị, kết quả hiện ngay trong Lịch sử gửi kèm mã HTTP và thời gian phản hồi. Muốn kiểm tra tại chỗ không cần chờ MONA Pay, anh chị giả lập chính xác gói MONA Pay gửi bằng cURL: ```bash # Giả lập MONA Pay bắn webhook vào endpoint của anh chị để test tại chỗ SECRET='secret_hmac_cua_anh_chi' # secret HMAC đặt khi bật HMAC trong dashboard URL='https://ten-mien-cua-anh-chi.vn/webhook/monapay' TS=$(date +%s) BODY='{"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"1234567890","bank_name":"ACB","type":"income"}' # Chữ ký = HMAC-SHA256(secret, "."), dùng printf để không dính ký tự xuống dòng SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST "$URL" \ -H 'Content-Type: application/json' \ -H "X-Mona-Timestamp: $TS" \ -H "X-Mona-Signature: sha256=$SIG" \ --data "$BODY" ``` Cuối cùng chuyển một khoản nhỏ vào số VA vừa tạo, ví dụ 10.000đ, từ app ngân hàng bất kỳ. Trong vài giây anh chị sẽ thấy giao dịch trong dashboard và webhook về máy chủ. ## Sau khi chạy được - Đọc [Định dạng payload](/docs/webhooks/dinh-dang-payload) để biết từng trường dữ liệu và dùng `transaction_code` làm khoá chống trùng. - Đọc [Gửi lại và xử lý lỗi](/docs/webhooks/gui-lai-va-xu-ly-loi) để hiểu log, nhãn lỗi và cách gửi lại. - Mở tường lửa cho IP `103.168.55.14` nếu máy chủ của anh chị chặn kết nối lạ, xem [Địa chỉ IP](/docs/dia-chi-ip). - Muốn tạo mã QR có sẵn số tiền cho từng đơn: [QR thanh toán](/docs/api/qr-thanh-toan). ## Lỗi thường gặp **Đăng nhập báo sai tài khoản dù mật khẩu đúng.** Kiểm tra lại username (phân biệt hoa thường) và mật khẩu. Tài khoản mới đăng ký là đăng nhập được ngay, không có bước chờ kích hoạt; vẫn kẹt thì gọi 1900 636 648. **Gọi POST bị từ chối dù đã có Bearer token.** Thiếu header `X-Client-Secret`. Mọi POST, PUT, DELETE nên gửi kèm khoá này (GET thì không); bản máy chủ ép buộc đang chờ đưa lên, gửi sẵn từ giờ thì không phải sửa sau. **Gửi thử báo TIMEOUT.** Máy chủ của anh chị không trả lời trong 10 giây. Trả HTTP 200 trước, xử lý đơn sau như code mẫu ở trên. **Chữ ký không khớp.** Thường do framework đã parse JSON rồi mới ký lại. Phải ký trên raw body đúng từng byte MONA Pay gửi, xem [Bảo mật webhook](/docs/webhooks/bao-mat). **Chuyển tiền rồi mà không thấy giao dịch.** Kiểm tra bước 4 đã hoàn tất cả 2 lần OTP chưa. Thiếu OTP lần 2 (đăng ký nhận thông báo) thì ACB không báo giao dịch sang MONA Pay. --- # Webhook là gì, VietQR là gì, tài khoản ảo là gì (giải thích dễ hiểu) > Giải thích 7 khái niệm gặp trong MONA Pay bằng ngôn ngữ người không làm kỹ thuật, kèm phần dành cho lập trình viên ở mỗi mục. > URL: https://monapay.vn/docs/khai-niem · Markdown: https://monapay.vn/docs/khai-niem.md Khi dùng MONA Pay anh chị sẽ gặp 7 từ: tài khoản ảo (VA), VietQR, webhook, Telegram, HMAC, API key và đối soát. Trang này giải thích từng từ theo cách chủ shop hay kế toán đọc là hiểu, mỗi mục có thêm vài dòng cho lập trình viên. Đọc xong trang này, anh chị đủ hiểu để cấu hình dashboard mà không cần hỏi dev. ## Tài khoản ảo (VA) **Cho người dùng.** Tài khoản ảo là một số tài khoản phụ do ACB cấp, tiền chuyển vào đó vẫn về tài khoản chính của anh chị. Điểm hay là anh chị tạo được nhiều số ảo khác nhau, mỗi số gắn cho một đơn hàng hoặc một khách. Khách chuyển vào số nào, hệ thống biết ngay tiền của đơn nào, không cần khách gõ đúng nội dung chuyển khoản. Ví dụ đơn hàng số 1052 được gắn VA `MONA1052...`, khách chuyển vào đó là đơn 1052 tự đổi sang đã thanh toán. **Cho lập trình viên.** VA được tạo theo đầu số (prefix) anh chị đăng ký với ACB, quản lý trong dashboard mục Ngân hàng & VA hoặc qua API `POST /api/v1/acb/virtual-account/registration`. Webhook cấu hình được theo từng VA, nên anh chị tách luồng tiền học phí, tiền hosting, tiền đơn lẻ vào các endpoint khác nhau. Chi tiết ở [Tài khoản ảo (VA)](/docs/api/tai-khoan-ao-va). ## VietQR **Cho người dùng.** VietQR là mã QR chuyển khoản chuẩn của các ngân hàng Việt Nam, app ngân hàng nào cũng quét được. MONA Pay tạo mã QR "động": trong mã đã có sẵn số tài khoản, số tiền và nội dung. Khách quét là màn hình chuyển khoản hiện đủ thông tin, chỉ bấm xác nhận, không gõ sai được. Đơn 350.000đ thì QR ghi đúng 350.000đ. **Cho lập trình viên.** Gọi `POST /api/v1/acb/qr-payment/generate` với `orderId`, `amount` (số nguyên VND, tối đa 1.000.000.000), `description` (tối đa 255 ký tự) và thông tin tài khoản. Khách quét và chuyển xong, ACB báo về, MONA Pay bắn webhook y như giao dịch qua VA. Hủy mã bằng `DELETE /api/v1/acb/qr-payment/{qr_code_id}/cancellation`. Xem [QR thanh toán](/docs/api/qr-thanh-toan). ## Webhook **Cho người dùng.** Webhook là cách MONA Pay "gọi điện" cho phần mềm của anh chị mỗi khi có tiền vào. Anh chị cho MONA Pay một địa chỉ web (URL), có giao dịch là MONA Pay gửi một gói thông tin tới địa chỉ đó: số tiền, nội dung, thời gian, mã giao dịch. Phần mềm nhận được thì tự làm việc tiếp, ví dụ đổi trạng thái đơn, gửi email cho khách, mở khoá khoá học. **Cho lập trình viên.** MONA Pay POST JSON tới URL của anh chị, mặc định `Content-Type: application/json`. Endpoint trả HTTP 200, 201 hoặc 202 trong 10 giây là thành công. Payload 7 trường, khoá chống trùng là `transaction_code`. Xem [Tích hợp webhook](/docs/webhooks/tich-hop-webhook) và [Định dạng payload](/docs/webhooks/dinh-dang-payload). ## Telegram **Cho người dùng.** Nếu anh chị chỉ cần biết tiền vào, không cần phần mềm xử lý gì, thì thông báo Telegram là đủ. Thêm bot MONA Pay vào nhóm Telegram của công ty, mỗi lần tiền vào là cả nhóm nhận một tin: ngân hàng, số tài khoản, số tiền, thời gian, nội dung. Kế toán ngồi đâu cũng thấy, không phải đăng nhập app ngân hàng. **Cho lập trình viên.** Cấu hình trong dashboard mục Telegram: nhập `group_id` (và `topic_id` nếu nhóm chia chủ đề), chọn VA hoặc mọi tài khoản, sửa mẫu tin nhắn, có nút gửi thử. API tại `/api/v1/telegram-configs`. Xem [Telegram](/docs/telegram). ## HMAC (chữ ký webhook) **Cho người dùng.** Webhook là một địa chỉ web công khai, về lý thuyết ai biết địa chỉ cũng gửi được một gói giả "có tiền vào". HMAC là chữ ký chống giả: anh chị và MONA Pay giữ chung một mật khẩu bí mật (secret), mỗi gói MONA Pay gửi đều kèm một chữ ký tính từ secret đó. Phần mềm của anh chị tính lại chữ ký, khớp thì tin, không khớp thì bỏ. Kẻ gian không có secret nên không làm giả được. **Cho lập trình viên.** Header `X-Mona-Signature: sha256=` với hex = HMAC-SHA256(secret, `"."`). Timestamp là unix giây, lệch quá 5 phút thì từ chối để chặn gửi lại gói cũ (replay). Phải ký trên raw body đúng từng byte. Xem [Bảo mật webhook](/docs/webhooks/bao-mat). ## API key và Bearer token **Cho người dùng.** API key là chìa khoá để phần mềm của anh chị gọi vào MONA Pay (tạo VA, tạo QR, xem giao dịch). Tạo trong dashboard mục API Keys, mỗi khoá đặt một tên để biết khoá nào đang dùng ở đâu, lộ khoá nào thì thu hồi khoá đó mà không ảnh hưởng chỗ khác. **Cho lập trình viên.** Hai lớp: đăng nhập `POST /api/v1/client/login` lấy `access_token` gửi dạng Bearer cho mọi request; các request POST, PUT, DELETE thêm header `X-Client-Secret` là `client_secret` sinh từ `POST /api/v1/client-keys/generate`. `client_secret` chỉ hiện 1 lần. Mọi response chung khung `{"success": true, "message": "...", "data": {...}}`. Xem [Xác thực](/docs/api/xac-thuc) và [API keys](/docs/api/api-keys). ## Đối soát **Cho người dùng.** Đối soát là việc so sổ của anh chị với danh sách giao dịch ngân hàng để chắc không sót khoản nào. Webhook lo phần thời gian thực, nhưng máy chủ của anh chị có lúc bảo trì hay mất mạng đúng lúc tiền vào. Đối soát định kỳ (mỗi giờ hoặc cuối ngày) là lưới đỡ cho những lúc đó. **Cho lập trình viên.** Kéo danh sách bằng `GET /api/v1/acb/virtual-account/transactions?virtual_account_number=...&page=1&limit=100`, tối đa 100 giao dịch mỗi trang, so `transaction_code` với bảng giao dịch của anh chị và bổ sung bản ghi thiếu. Xem [Đối soát giao dịch](/docs/webhooks/doi-soat). ## Ghép các khái niệm lại Một web bán hàng điển hình dùng như sau: mỗi đơn tạo một mã VietQR động (hoặc gắn một VA), khách quét và chuyển. ACB báo cho MONA Pay, MONA Pay bắn webhook có chữ ký HMAC tới web, web kiểm chữ ký rồi đổi đơn sang đã thanh toán. Nhóm Telegram của kế toán nhận tin cùng lúc. Cuối ngày một cron gọi API đối soát để chắc không sót. Toàn bộ chuỗi này không có người nào phải mở app ngân hàng. ## Câu hỏi nhanh **Tiền có nằm ở MONA Pay lúc nào không?** Không. VA là số phụ của chính tài khoản ACB của anh chị, tiền vào là nằm trong tài khoản ngân hàng của anh chị ngay. MONA Pay chỉ nhận thông báo. **Không có VA thì webhook có chạy không?** Có. Sau khi nối tài khoản ACB và đăng ký nhận thông báo (2 lần OTP), mọi giao dịch vào tài khoản đều báo về. VA chỉ giúp khớp đơn tự động. **Nên dùng VA hay VietQR?** Dùng cả hai. VietQR động là cách khách thanh toán tiện nhất, còn VA là cách hệ thống phân biệt tiền của đơn nào. Mã QR động của MONA Pay đã gắn sẵn số tiền và nội dung nên khớp đơn rất chắc. **HMAC có bắt buộc không?** Không bắt buộc nhưng tụi em khuyên bật luôn từ đầu. Chỉ tốn khoảng 10 dòng code ở đầu nhận, đổi lại không ai giả được thông báo tiền vào. --- # Tích hợp webhook thanh toán: nhận tiền vào ACB trong 5 phút > Cách khai báo URL webhook, viết endpoint nhận thông báo tiền vào ACB, điều kiện thành công 200/201/202 trong 10 giây, chống trùng bằng transaction_code. > URL: https://monapay.vn/docs/webhooks/tich-hop-webhook · Markdown: https://monapay.vn/docs/webhooks/tich-hop-webhook.md Webhook MONA Pay là một request HTTP POST gửi tới URL của anh chị mỗi khi có tiền vào tài khoản ACB đã nối. Để tích hợp, anh chị khai báo URL trong dashboard hoặc qua API, viết một endpoint nhận JSON, kiểm chữ ký, trả HTTP 200 trong 10 giây rồi xử lý đơn hàng. Phần này mất khoảng 30 dòng code, có mẫu PHP và Node dán vào dùng ngay. ## Luồng một webhook 1. Tiền vào tài khoản ACB (qua VA, VietQR hoặc chuyển khoản thường). 2. ACB báo giao dịch cho MONA Pay, MONA Pay lưu vào dashboard. 3. MONA Pay tìm các cấu hình webhook khớp: cấu hình "mọi tài khoản" luôn được gửi, cấu hình gắn một VA chỉ gửi khi giao dịch thuộc VA đó. 4. Với mỗi cấu hình, MONA Pay POST payload tới URL, kèm chữ ký nếu bật HMAC, chờ tối đa 10 giây. 5. Máy chủ của anh chị trả HTTP 200, 201 hoặc 202: ghi log thành công. Mã khác hoặc quá 10 giây: ghi log thất bại kèm nhãn lỗi, anh chị gửi lại từ dashboard. ## Khai báo URL webhook ### Trong dashboard Vào mục Webhooks, bấm Thêm webhook, điền: | Trường | Ý nghĩa | |---|---| | Tên | Đặt cho dễ nhớ, ví dụ "Web bán hàng", "Phần mềm học phí" | | URL | Địa chỉ HTTPS nhận webhook trên máy chủ của anh chị | | Kiểu xác thực | `HMAC_SHA256` (khuyên dùng), `API_KEY` hoặc `NONE` | | Secret | Chuỗi bí mật dùng để ký (HMAC) hoặc gửi kèm header (API key) | | Định dạng gửi | `application/json` (mặc định), `application/x-www-form-urlencoded`, `multipart/form-data` | | Tài khoản áp dụng | Mọi tài khoản, hoặc chọn một VA cụ thể | ### Qua API ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks \ -H "Authorization: Bearer $TOKEN" \ -H "X-Client-Secret: $CLIENT_SECRET" \ -H 'Content-Type: application/json' \ -d '{ "name": "Web bán hàng", "webhook_url": "https://ten-mien-cua-anh-chi.vn/webhook/monapay", "auth_type": "HMAC_SHA256", "secret_key": "secret_hmac_cua_anh_chi", "payload_format": "application/json", "virtual_account_id": null }' ``` Các lệnh liên quan: `GET /api/v1/client-webhooks` xem danh sách, `PUT /api/v1/client-webhooks/{config_id}` sửa, `DELETE /api/v1/client-webhooks/{config_id}` xoá, `POST /api/v1/client-webhooks/test` gửi gói mẫu. Chi tiết tham số ở [Cấu hình webhook qua API](/docs/api/webhook-configs). ## Payload MONA Pay gửi ```json { "amount": 2500000, "description": "noi dung chuyen khoan", "transfer_date": "10:30:00 28/08/2026", "transaction_code": "FT26240001234", "account_number": "1234567890", "bank_name": "ACB", "type": "income" } ``` `transaction_code` giữ nguyên qua mọi lần gửi lại, dùng làm khoá chống trùng. `account_number` là số VA hoặc số tài khoản nhận tiền. Từng trường giải thích ở [Định dạng payload](/docs/webhooks/dinh-dang-payload). ## Viết endpoint nhận webhook Endpoint tốt làm đúng 3 việc theo thứ tự: kiểm chữ ký, trả 200 ngay, xử lý sau. Việc nặng (cập nhật đơn, gửi email, gọi API khác) đẩy vào hàng đợi hoặc làm sau khi đã trả lời, để không bao giờ chạm mốc 10 giây. ```bash # Giả lập MONA Pay bắn webhook vào endpoint của anh chị để test tại chỗ SECRET='secret_hmac_cua_anh_chi' # secret HMAC đặt khi bật HMAC trong dashboard URL='https://ten-mien-cua-anh-chi.vn/webhook/monapay' TS=$(date +%s) BODY='{"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"1234567890","bank_name":"ACB","type":"income"}' # Chữ ký = HMAC-SHA256(secret, "."), dùng printf để không dính ký tự xuống dòng SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST "$URL" \ -H 'Content-Type: application/json' \ -H "X-Mona-Timestamp: $TS" \ -H "X-Mona-Signature: sha256=$SIG" \ --data "$BODY" ``` ```php " // 1. Chặn replay: timestamp lệch quá 5 phút thì từ chối if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit('timestamp qua han'); } // 2. Verify chữ ký: HMAC-SHA256(secret, ".") $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret); if (!hash_equals($expected, $signature)) { // hash_equals chống timing attack http_response_code(401); exit('sai chu ky'); } // 3. Trả 200 ngay, việc nặng đẩy sang xử lý sau (queue/cron) http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); $data = json_decode($raw, true); // 4. Chống trùng: transaction_code là khoá duy nhất $pdo = new PDO(getenv('DB_DSN'), getenv('DB_USER'), getenv('DB_PASS')); $stmt = $pdo->prepare( 'INSERT IGNORE INTO giao_dich (transaction_code, amount, description, transfer_date, account_number) VALUES (?, ?, ?, ?, ?)' ); $stmt->execute([$data['transaction_code'], $data['amount'], $data['description'], $data['transfer_date'], $data['account_number']]); if ($stmt->rowCount() === 0) exit; // đã xử lý rồi, gói này là gửi lại // TODO: khớp đơn hàng theo account_number (VA) hoặc description, đổi trạng thái đơn ``` ```js // webhook-monapay.js: nhận webhook MONA Pay bằng Express const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.MONA_WEBHOOK_SECRET; // secret HMAC đặt khi bật HMAC // Phải đọc raw body thì chữ ký mới khớp (đừng dùng express.json() ở route này) app.post('/webhook/monapay', express.raw({ type: 'application/json' }), async (req, res) => { const timestamp = req.header('X-Mona-Timestamp') || ''; const signature = req.header('X-Mona-Signature') || ''; // "sha256=" const rawBody = req.body.toString('utf8'); // 1. Chặn replay: timestamp lệch quá 5 phút thì từ chối if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.status(400).send('timestamp qua han'); } // 2. Verify chữ ký: HMAC-SHA256(secret, ".") const expected = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(timestamp + '.' + rawBody) .digest('hex'); const hopLe = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!hopLe) return res.status(401).send('sai chu ky'); // 3. Trả 200 ngay, xử lý đơn hàng async sau đó res.status(200).send('OK'); const data = JSON.parse(rawBody); // 4. Chống trùng: transaction_code là khoá duy nhất (ví dụ với bảng có UNIQUE(transaction_code)) // const inserted = await db.insertIgnore('giao_dich', { transaction_code: data.transaction_code, ... }); // if (!inserted) return; // gói gửi lại, đã xử lý trước đó // TODO: khớp đơn hàng theo data.account_number (VA) hoặc data.description, đổi trạng thái đơn }); app.listen(3000, () => console.log('Webhook MONA Pay chạy ở cổng 3000')); ``` ## Chống trùng giao dịch Cùng một giao dịch có thể tới endpoint của anh chị nhiều lần: gửi lại thủ công từ dashboard, hai cấu hình webhook cùng trỏ một URL, hoặc máy chủ trả lỗi rồi được gửi lại. Cách xử lý chuẩn là tạo cột `transaction_code` có ràng buộc UNIQUE trong bảng giao dịch và dùng `INSERT IGNORE` (MySQL) hoặc `ON CONFLICT DO NOTHING` (PostgreSQL). Gói nào chèn không vào là gói đã xử lý, bỏ qua. Đừng dựa vào cặp số tiền + thời gian, hai khách chuyển cùng số tiền trong cùng phút là chuyện bình thường. ## Kiểm tra trước khi lên production - Endpoint chạy HTTPS với chứng chỉ hợp lệ. Chứng chỉ tự ký hoặc hết hạn sẽ ra nhãn lỗi `SSL`. - Tường lửa cho phép IP `103.168.55.14` (xem [Địa chỉ IP](/docs/dia-chi-ip)). - Không có redirect ở URL webhook (ví dụ từ `http` sang `https` hoặc thêm dấu `/` cuối). Redirect ra nhãn `HTTP_3XX` và tính là thất bại. - Bấm Gửi thử trong dashboard, xem Lịch sử gửi có mã 200 và thời gian phản hồi dưới 1 giây. - Chuyển thử một khoản nhỏ vào VA để chạy luồng thật. ## Lỗi thường gặp **Webhook về nhưng chữ ký không khớp.** Framework (Laravel, Express, Next.js) đã parse JSON rồi mới lấy body để ký. Phải lấy raw body nguyên bản. Xem cách lấy raw body từng framework ở [Bảo mật webhook](/docs/webhooks/bao-mat). **Nhãn TIMEOUT dù endpoint chạy được.** Endpoint xử lý đơn xong mới trả lời, vượt 10 giây khi hệ thống bận. Trả 200 trước, xử lý sau. **Nhãn HTTP_4XX với mã 404 hoặc 405.** Sai đường dẫn, hoặc route chỉ nhận GET. Webhook luôn là POST. **Nhận được 2 lần cùng một giao dịch.** Bình thường. Chống trùng bằng `transaction_code` như mục trên. **Không nhận được gì dù tiền đã vào.** Cấu hình webhook đang gắn một VA nhưng khách chuyển vào tài khoản chính hoặc VA khác. Tạo thêm một cấu hình "mọi tài khoản" để không sót, hoặc kiểm tra Lịch sử gửi trong dashboard xem MONA Pay có gửi không. --- # Định dạng payload webhook MONA Pay > 7 trường trong gói JSON MONA Pay gửi khi có tiền vào, 3 kiểu Content-Type, header kèm theo, gói gửi thử và định dạng tương thích đang triển khai. > URL: https://monapay.vn/docs/webhooks/dinh-dang-payload · Markdown: https://monapay.vn/docs/webhooks/dinh-dang-payload.md Mỗi webhook MONA Pay gửi là một gói JSON 7 trường: số tiền, nội dung, thời gian, mã giao dịch, số tài khoản nhận, tên ngân hàng và loại giao dịch. Anh chị chọn được 1 trong 3 kiểu đóng gói (JSON, form-urlencoded, multipart). MONA Pay đang triển khai thêm tuỳ chọn định dạng tương thích các cổng phổ biến tại Việt Nam, xem mục cuối trang. ## Payload định dạng `monapay` (mặc định) ```json { "amount": 2500000, "description": "noi dung chuyen khoan", "transfer_date": "10:30:00 28/08/2026", "transaction_code": "FT26240001234", "account_number": "1234567890", "bank_name": "ACB", "type": "income" } ``` | Trường | Kiểu | Ý nghĩa | Ghi chú | |---|---|---|---| | `amount` | số nguyên | Số tiền giao dịch, đơn vị VND | Không có phần thập phân. 2.500.000đ gửi là `2500000` | | `description` | chuỗi | Nội dung chuyển khoản khách gõ hoặc nội dung trong mã QR | Dùng để khớp đơn khi không dùng VA | | `transfer_date` | chuỗi | Thời điểm giao dịch, định dạng `YYYY-MM-DD HH:MM:SS`, giờ Việt Nam | Lấy từ thông báo của ACB | | `transaction_code` | chuỗi | Mã giao dịch phía ngân hàng | Giữ nguyên qua mọi lần gửi lại. Dùng làm khoá chống trùng | | `account_number` | chuỗi | Số VA hoặc số tài khoản nhận tiền | Khớp đơn theo VA thì so trường này | | `bank_name` | chuỗi | Tên ngân hàng | Hiện luôn là `ACB` | | `type` | chuỗi | Loại giao dịch | Hiện chỉ có `income` (tiền vào) | Ba điều nên biết khi dùng: - **`transaction_code` là khoá duy nhất.** Gửi lại 10 lần thì cả 10 gói cùng một `transaction_code`. Bảng giao dịch của anh chị nên có ràng buộc UNIQUE trên cột này. - **`account_number` cho biết tiền vào VA nào.** Nếu anh chị gắn mỗi đơn một VA, chỉ cần so trường này là biết đơn nào đã trả, không cần đọc `description`. - **Trường sẽ được thêm, không bị đổi tên.** Khi MONA Pay mở thêm ngân hàng hoặc loại giao dịch, tụi em thêm giá trị mới (ví dụ `bank_name` khác `ACB`, `type` khác `income`) chứ không đổi tên 7 trường này. Code của anh chị nên bỏ qua trường lạ thay vì báo lỗi. ## Ba kiểu Content-Type Chọn trong dashboard hoặc qua trường `payload_format` khi tạo cấu hình: | `payload_format` | MONA Pay gửi thế nào | Dùng khi | |---|---|---| | `application/json` | Body là chuỗi JSON như trên, không khoảng trắng thừa | Mặc định. Mọi framework hiện đại đều nhận được | | `application/x-www-form-urlencoded` | Body dạng `amount=2500000&description=...` như form HTML | Hệ thống cũ chỉ đọc `$_POST` | | `multipart/form-data` | Từng trường là một phần của form nhiều phần | Hệ thống chỉ nhận multipart | Với HMAC, chuỗi được ký là raw body đúng như MONA Pay gửi: JSON không khoảng trắng với kiểu JSON, chuỗi urlencoded với kiểu form. Chi tiết ở [Bảo mật webhook](/docs/webhooks/bao-mat). ## Header đi kèm | Header | Khi nào có | Giá trị | |---|---|---| | `Content-Type` | Luôn | Theo `payload_format` đã chọn | | `X-Mona-Timestamp` | Kiểu xác thực `HMAC_SHA256` | Unix giây lúc gửi | | `X-Mona-Signature` | Kiểu xác thực `HMAC_SHA256` | `sha256=` | | Header tên tuỳ chọn (mặc định `X-Webhook-Secret`) | Kiểu xác thực `API_KEY` | Secret anh chị đặt | ## Định dạng tương thích (đang triển khai) MONA Pay đang triển khai thêm tuỳ chọn định dạng payload tương thích với các cổng phổ biến tại Việt Nam để đổi nhà cung cấp không phải sửa code; công bố tại trang này khi lên. Trong lúc chờ, 7 trường ở trên đủ để anh chị viết một lớp chuyển đổi khoảng 10 dòng ở đầu nhận: đổi tên trường theo cấu trúc code cũ, giữ `transaction_code` làm khoá chống trùng, rồi gọi lại hàm xử lý sẵn có. ## Gói gửi thử Khi bấm Gửi thử trong dashboard hoặc gọi `POST /api/v1/client-webhooks/test`, MONA Pay gửi một gói có đúng 7 trường này với dữ liệu mẫu. Endpoint của anh chị nên phân biệt gói thử với giao dịch thật bằng cách kiểm tra `transaction_code` có tồn tại trong danh sách giao dịch (qua [API đối soát](/docs/webhooks/doi-soat)) trước khi đổi trạng thái đơn quan trọng, hoặc đơn giản là chỉ gửi thử vào môi trường staging. ## Câu hỏi nhanh **Số tiền có bao giờ là số thập phân không?** Không. VND không có phần lẻ, `amount` luôn là số nguyên. **Múi giờ của `transfer_date`?** Giờ Việt Nam (UTC+7), đúng như ACB báo. **Có gửi giao dịch tiền ra không?** Hiện chưa. `type` luôn là `income`. Khi có thêm loại khác tụi em giữ nguyên tên trường. **Tôi nhận webhook bằng Google Apps Script hoặc n8n được không?** Được, miễn URL nhận POST và trả 200 trong 10 giây. Với chữ ký HMAC, cần lấy raw body để tính lại, xem [Bảo mật webhook](/docs/webhooks/bao-mat). ## Lưu ý định dạng `transfer_date` `transfer_date` là chuỗi `HH:MM:SS dd/mm/YYYY` theo giờ Việt Nam (UTC+7), ví dụ `10:30:00 28/08/2026`. Không phải ISO 8601, nên đừng `new Date()` thẳng; tách theo khoảng trắng rồi ghép lại, hoặc lưu nguyên chuỗi để đối soát. --- # Bảo mật webhook: chữ ký HMAC-SHA256 và chống replay > 3 kiểu xác thực webhook MONA Pay (HMAC_SHA256, API_KEY, NONE), cách tính và kiểm chữ ký X-Mona-Signature, cửa sổ 5 phút chống replay, code cURL, PHP, Node. > URL: https://monapay.vn/docs/webhooks/bao-mat · Markdown: https://monapay.vn/docs/webhooks/bao-mat.md Webhook MONA Pay có 3 kiểu xác thực: `HMAC_SHA256` (ký từng gói bằng secret chung, khuyên dùng), `API_KEY` (gửi secret trong một header) và `NONE` (không xác thực). Với HMAC, mỗi gói kèm header `X-Mona-Timestamp` và `X-Mona-Signature`; anh chị tính lại chữ ký từ raw body, khớp thì nhận, và từ chối gói có timestamp lệch quá 5 phút. ## Vì sao phải xác thực URL webhook là địa chỉ công khai. Ai đoán được địa chỉ đó đều gửi được một gói JSON giả "có 50.000.000đ vào tài khoản". Nếu endpoint tin ngay, đơn hàng được đánh dấu đã trả dù không có đồng nào. Xác thực giúp endpoint chỉ tin gói do MONA Pay gửi. Trong 3 kiểu, HMAC mạnh nhất vì secret không bao giờ đi trên đường truyền, chỉ có chữ ký tính từ secret. ## Kiểu 1: `HMAC_SHA256` (khuyên dùng) ### MONA Pay ký thế nào 1. Lấy `timestamp` = unix giây lúc gửi. 2. Lấy `raw_body` = chuỗi body đúng như sẽ gửi: JSON không khoảng trắng thừa (kiểu `application/json`) hoặc chuỗi urlencoded (kiểu form). 3. Tính `signature = HMAC-SHA256(secret, timestamp + "." + raw_body)`, xuất hex chữ thường. 4. Gửi kèm 2 header: ```text X-Mona-Timestamp: 1756355400 X-Mona-Signature: sha256=3f2a9c...e71b ``` Secret là chuỗi anh chị đặt khi tạo cấu hình webhook (trường Secret trong dashboard hoặc `secret_key` qua API). Đặt dài ít nhất 32 ký tự ngẫu nhiên, mỗi cấu hình một secret riêng. ### Anh chị kiểm thế nào 1. Đọc raw body nguyên bản (chưa parse). 2. Kiểm `|now - timestamp| <= 300` giây. Lệch hơn 5 phút thì trả 400 và bỏ, đây là chống replay: kẻ gian bắt được một gói hợp lệ cũng không dùng lại được sau 5 phút. 3. Tính lại `sha256=` + HMAC-SHA256(secret, `timestamp + "." + raw_body`). 4. So với `X-Mona-Signature` bằng hàm so sánh thời gian cố định (`hash_equals`, `crypto.timingSafeEqual`). Khớp thì xử lý, không thì trả 401. ```bash # Giả lập MONA Pay bắn webhook vào endpoint của anh chị để test tại chỗ SECRET='secret_hmac_cua_anh_chi' # secret HMAC đặt khi bật HMAC trong dashboard URL='https://ten-mien-cua-anh-chi.vn/webhook/monapay' TS=$(date +%s) BODY='{"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"1234567890","bank_name":"ACB","type":"income"}' # Chữ ký = HMAC-SHA256(secret, "."), dùng printf để không dính ký tự xuống dòng SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST "$URL" \ -H 'Content-Type: application/json' \ -H "X-Mona-Timestamp: $TS" \ -H "X-Mona-Signature: sha256=$SIG" \ --data "$BODY" ``` ```php " // 1. Chặn replay: timestamp lệch quá 5 phút thì từ chối if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit('timestamp qua han'); } // 2. Verify chữ ký: HMAC-SHA256(secret, ".") $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret); if (!hash_equals($expected, $signature)) { // hash_equals chống timing attack http_response_code(401); exit('sai chu ky'); } // 3. Trả 200 ngay, việc nặng đẩy sang xử lý sau (queue/cron) http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); $data = json_decode($raw, true); // $data['amount'], $data['description'], $data['transaction_code'], ... // TODO: ghi nhận giao dịch + xử lý đơn hàng của anh chị tại đây ``` ```js // webhook-monapay.js: nhận webhook MONA Pay bằng Express const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.MONA_WEBHOOK_SECRET; // secret HMAC đặt khi bật HMAC // Phải đọc raw body thì chữ ký mới khớp (đừng dùng express.json() ở route này) app.post('/webhook/monapay', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.header('X-Mona-Timestamp') || ''; const signature = req.header('X-Mona-Signature') || ''; // "sha256=" const rawBody = req.body.toString('utf8'); // 1. Chặn replay: timestamp lệch quá 5 phút thì từ chối if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.status(400).send('timestamp qua han'); } // 2. Verify chữ ký: HMAC-SHA256(secret, ".") const expected = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(timestamp + '.' + rawBody) .digest('hex'); const hopLe = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!hopLe) return res.status(401).send('sai chu ky'); // 3. Trả 200 ngay, xử lý đơn hàng async sau đó res.status(200).send('OK'); const data = JSON.parse(rawBody); // data.amount, data.description, data.transaction_code, ... // TODO: ghi nhận giao dịch + xử lý đơn hàng của anh chị tại đây }); app.listen(3000, () => console.log('Webhook MONA Pay chạy ở cổng 3000')); ``` ### Lấy raw body ở từng framework Chữ ký tính trên từng byte của body. Framework nào parse JSON rồi `json_encode` lại là hỏng, vì thứ tự khoá, khoảng trắng hay escape unicode có thể khác. Cách lấy raw body: | Framework | Cách lấy raw body | |---|---| | PHP thuần, WordPress | `file_get_contents('php://input')` | | Laravel | `$request->getContent()` | | Express | `express.raw({ type: 'application/json' })` ở route webhook, rồi `req.body.toString('utf8')` | | Next.js (App Router) | `await request.text()` thay vì `request.json()` | | Django | `request.body` (bytes) | | FastAPI | `await request.body()` | | Google Apps Script | `e.postData.contents` | Trong WordPress, đăng ký route qua `register_rest_route` với `methods => 'POST'` và đọc `$request->get_body()`. ## Kiểu 2: `API_KEY` MONA Pay gửi secret nguyên văn trong một header. Tên header anh chị đặt khi tạo cấu hình (`api_key_name`), mặc định `X-Webhook-Secret`. Endpoint so header với secret đã lưu, khớp thì nhận. ```php Điều kiện webhook thành công (200/201/202 trong 10 giây), 9 nhãn lỗi trong log và cách sửa, gửi lại thủ công từ dashboard, thống kê tỷ lệ thành công và P95. > URL: https://monapay.vn/docs/webhooks/gui-lai-va-xu-ly-loi · Markdown: https://monapay.vn/docs/webhooks/gui-lai-va-xu-ly-loi.md MONA Pay tính một lần gửi webhook là thành công khi máy chủ của anh chị trả HTTP 200, 201 hoặc 202 trong vòng 10 giây. Mọi lần gửi đều ghi log với mã HTTP, thời gian phản hồi và một nhãn lỗi. Lần gửi thất bại được gửi lại thủ công từ dashboard; gửi lại tự động theo lịch (tối đa 7 lần) đang được triển khai. ## Thế nào là thành công | Điều kiện | Giá trị | |---|---| | Mã HTTP chấp nhận | 200, 201, 202 | | Thời gian chờ tối đa | 10 giây tính từ lúc MONA Pay bắt đầu gửi | | Body phản hồi | Không bắt buộc, MONA Pay không đọc nội dung | | Redirect | Không đi theo. 301/302 tính là thất bại | Điều này nghĩa là endpoint của anh chị chỉ cần trả `200 OK` với body rỗng. Đừng trả 204 (không nằm trong danh sách) và đừng để URL webhook có redirect. ## Log từng lần gửi Mỗi lần MONA Pay gửi, kể cả gửi thử và gửi lại, đều có một dòng trong dashboard mục Webhooks, tab Lịch sử gửi. Cùng dữ liệu có ở API `GET /api/v1/webhook-logs`. | Trường log | Ý nghĩa | |---|---| | Thời điểm gửi | Lúc MONA Pay bắt đầu request | | Mã HTTP | Mã máy chủ của anh chị trả, trống nếu không kết nối được | | `duration_ms` | Thời gian từ lúc gửi tới lúc nhận phản hồi, mili giây | | `error_label` | Nhãn phân loại kết quả, xem bảng dưới | | Payload | Gói đã gửi, để anh chị đối chiếu | | Phản hồi | Vài trăm ký tự đầu body máy chủ trả về, tiện tìm lỗi | ## 9 nhãn lỗi và cách sửa | `error_label` | Nghĩa | Nguyên nhân hay gặp | Cách sửa | |---|---|---|---| | `OK` | Thành công | Máy chủ trả 200/201/202 trong 10 giây | Không cần làm gì | | `HTTP_3XX` | Máy chủ trả redirect | URL thiếu `https`, thiếu hoặc thừa dấu `/` cuối, chuyển hướng www | Khai URL đích cuối cùng, không qua redirect | | `HTTP_4XX` | Máy chủ từ chối | 401/403 sai secret hoặc tường lửa ứng dụng chặn; 404 sai đường dẫn; 405 route không nhận POST; 422 endpoint đòi trường không có | Kiểm secret, đường dẫn, method. Với WAF (Cloudflare, ModSecurity) cho phép IP `103.168.55.14` | | `HTTP_5XX` | Máy chủ lỗi | Code ném exception, hết bộ nhớ, database không kết nối được | Xem log máy chủ của anh chị đúng thời điểm `duration_ms` | | `TIMEOUT` | Quá 10 giây không phản hồi | Endpoint xử lý xong đơn mới trả lời, hoặc máy chủ quá tải | Trả 200 trước, xử lý sau | | `SSL` | Bắt tay TLS thất bại | Chứng chỉ hết hạn, tự ký, sai tên miền, thiếu chuỗi trung gian | Kiểm bằng `openssl s_client -connect ten-mien:443`, cài lại chứng chỉ đầy đủ | | `DNS` | Không phân giải được tên miền | Gõ sai tên miền, tên miền hết hạn, bản ghi DNS chưa lan | Kiểm `dig ten-mien` từ máy ngoài | | `CONNECTION` | Không mở được kết nối | Tường lửa chặn cổng 443, máy chủ tắt, cổng sai | Mở cổng 443 cho IP `103.168.55.14`, kiểm máy chủ đang chạy | | `ERROR` | Lỗi khác | Trường hợp không xếp được vào các nhãn trên | Xem phản hồi trong log, gọi 1900 636 648 nếu không rõ | ## Gửi lại thủ công Trong Lịch sử gửi, mỗi dòng thất bại có nút Gửi lại. Bấm là MONA Pay gửi lại đúng payload đó tới đúng URL đó, ngay lập tức, và ghi thêm một dòng log mới. `transaction_code` không đổi nên endpoint của anh chị phân biệt được đây là gói cũ. Anh chị cũng gửi lại bằng API: `POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry` với body `{"target_type": "WEBHOOK"}` (hoặc `"TELEGRAM"`, kèm `target_id` là id cấu hình muốn gửi lại), header `Authorization: Bearer` + `X-Client-Secret` như mọi lệnh ghi. Xem thêm [Cấu hình webhook qua API](/docs/api/webhook-configs). Gửi lại có 2 tình huống dùng: - **Sửa xong lỗi ở máy chủ.** Ví dụ endpoint bị 500 vì thiếu bảng, tạo bảng xong bấm gửi lại từng dòng thất bại. - **Kiểm tra sau khi đổi code.** Gửi lại một giao dịch cũ để xem code mới xử lý đúng không, thay vì phải chuyển tiền thật. ## Gửi lại tự động (đang triển khai) Cấu hình webhook có trường `max_retries` mặc định 7, dành cho cơ chế gửi lại tự động theo lịch giãn dần khi máy chủ của anh chị trả lỗi. Cơ chế này đang được triển khai và chưa chạy trên production; hiện tại mọi lần gửi lại đều do anh chị bấm. Khi lên, tụi em công bố lịch cụ thể tại trang này. Trong lúc chờ, anh chị nên chạy [đối soát định kỳ](/docs/webhooks/doi-soat) để không sót giao dịch nào lúc máy chủ gặp sự cố. ## Thống kê tỷ lệ thành công và P95 Dashboard mục Webhooks có tab thống kê, dữ liệu lấy từ `GET /api/v1/webhook-logs/stats`: tổng số lần gửi, tỷ lệ thành công, thời gian phản hồi trung bình và P95 (95% lần gửi nhanh hơn mốc này), phân bố theo nhãn lỗi. Vài mốc tham khảo: - Tỷ lệ thành công dưới 99% trong ngày: có gì đó không ổn ở máy chủ, xem phân bố nhãn lỗi. - P95 trên 3.000 ms: endpoint đang làm việc nặng trước khi trả lời, sớm muộn sẽ dính `TIMEOUT` khi tải tăng. - Nhãn `HTTP_4XX` tăng đột ngột sau khi deploy: thường là đổi secret hoặc đổi đường dẫn mà quên cập nhật cấu hình. ```bash curl "https://api.monapay.vn/api/v1/webhook-logs/stats" \ -H "Authorization: Bearer $TOKEN" ``` ## Xử lý phía máy chủ của anh chị Ba nguyên tắc để endpoint không bao giờ lỗi vì tải: 1. **Trả lời trước, làm việc sau.** Kiểm chữ ký xong là trả 200 ngay. Việc cập nhật đơn, gửi email, gọi API bên thứ ba đẩy vào hàng đợi (Redis, database, cron). Mẫu PHP dùng `fastcgi_finish_request()`, mẫu Node gọi `res.send()` trước rồi xử lý tiếp, xem [Tích hợp webhook](/docs/webhooks/tich-hop-webhook). 2. **Chống trùng bằng `transaction_code`.** Gửi lại tay, hai cấu hình cùng URL, hoặc gửi lại tự động sau này đều sinh gói trùng. UNIQUE trên `transaction_code` là đủ. 3. **Ghi log ở phía anh chị.** Lưu raw body và header của mọi gói nhận được, ít nhất 30 ngày. Khi cần đối chiếu với log MONA Pay, hai bên cùng nhìn một `transaction_code`. ## Lỗi thường gặp **Log báo `OK` nhưng đơn không đổi trạng thái.** Endpoint trả 200 rồi phần xử lý sau đó lỗi. Xem log ứng dụng của anh chị, vì MONA Pay chỉ biết mã HTTP. **Bấm gửi lại vẫn ra cùng nhãn lỗi.** Lỗi chưa được sửa ở máy chủ. Thử gói giả lập bằng cURL ở trang [Bảo mật webhook](/docs/webhooks/bao-mat) từ máy khác để tái hiện. **`TIMEOUT` chỉ xảy ra vào giờ cao điểm.** Máy chủ quá tải, xử lý đồng bộ. Chuyển sang trả 200 trước. **Tất cả webhook ra `CONNECTION` từ một thời điểm.** Thường do đổi tường lửa hoặc đổi máy chủ. Mở lại cổng 443 cho IP `103.168.55.14`. --- # Đối soát giao dịch bằng API MONA Pay > Kéo danh sách giao dịch theo trang (tối đa 100 mỗi trang) bằng API, so với sổ của anh chị bằng transaction_code, lịch cron gợi ý mỗi giờ hoặc mỗi ngày. > URL: https://monapay.vn/docs/webhooks/doi-soat · Markdown: https://monapay.vn/docs/webhooks/doi-soat.md Đối soát là bước so danh sách giao dịch MONA Pay ghi nhận với bảng giao dịch trong hệ thống của anh chị, để bổ sung khoản nào webhook chưa tới (máy chủ bảo trì, mất mạng, lỗi code). Anh chị gọi `GET /api/v1/acb/virtual-account/transactions` theo trang, mỗi trang tối đa 100 giao dịch, và so bằng `transaction_code`. Nên chạy mỗi giờ hoặc ít nhất mỗi ngày. ## Vì sao cần đối soát dù đã có webhook Webhook là kênh thời gian thực nhưng phụ thuộc máy chủ của anh chị đang sống lúc tiền vào. Một lần deploy 2 phút, một lần hết hạn chứng chỉ SSL, một lần database khoá là có khoản bị lỡ. Hiện MONA Pay gửi lại thủ công (gửi lại tự động đang triển khai, xem [Gửi lại và xử lý lỗi](/docs/webhooks/gui-lai-va-xu-ly-loi)), nên đối soát định kỳ là lưới đỡ cần có ngay từ ngày đầu. ## API danh sách giao dịch ```text GET https://api.monapay.vn/api/v1/acb/virtual-account/transactions ``` | Tham số query | Bắt buộc | Ý nghĩa | |---|---|---| | `virtual_account_number` | có | Số VA cần lấy giao dịch | | `page` | không | Trang, bắt đầu từ 1 | | `limit` | không | Số giao dịch mỗi trang, tối đa 100 | Request là GET nên chỉ cần `Authorization: Bearer `, không cần `X-Client-Secret`. ```bash curl "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=1234567890&page=1&limit=100" \ -H "Authorization: Bearer $TOKEN" ``` Response theo khung chung `{"success": true, "message": "...", "data": {...}}`, trong đó `data` là khối phân trang (kiểm code BE 28/08/2026): `data.data` là mảng giao dịch, kèm `current_page`, `per_page`, `total`, `last_page`. Mỗi giao dịch trong `data.data` mang cùng thông tin với payload webhook: mã giao dịch, số tiền, thời gian, nội dung, số tài khoản nhận. Dừng vòng lặp khi `current_page >= last_page`. Chi tiết trường xem `https://monapay.vn/openapi.json` (mục `/api/v1/acb/virtual-account/transactions`). Hiện API chưa có tham số `since_id` hay lọc theo khoảng ngày. Cách làm việc là kéo theo trang từ trang 1, giao dịch mới nhất ở đầu, dừng khi gặp `transaction_code` đã có trong bảng của anh chị và trang đó không còn mã nào mới. ## Thuật toán đối soát 1. Với mỗi VA đang dùng, gọi trang 1 với `limit=100`. 2. Với mỗi giao dịch, kiểm `transaction_code` đã có trong bảng của anh chị chưa. 3. Chưa có: chèn vào bảng và chạy đúng logic xử lý như khi nhận webhook (đổi trạng thái đơn, gửi email...). Nhớ đây là cùng một hàm xử lý, để đơn được cập nhật y hệt dù tới bằng đường nào. 4. Nếu cả trang đều đã có, dừng. Nếu còn mã mới, gọi trang tiếp theo. 5. Ghi lại thời điểm chạy và số giao dịch bổ sung, để anh chị biết webhook có đang hụt không. ```bash # Đối soát nhanh bằng shell: liệt kê transaction_code của trang 1 để so bằng mắt curl -s "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=1234567890&page=1&limit=100" \ -H "Authorization: Bearer $TOKEN" | python3 -c ' import sys, json d = json.load(sys.stdin)["data"] items = d if isinstance(d, list) else d.get("data") or [] # data.data = mảng giao dịch for t in items: print(t.get("transaction_code"), t.get("amount"), t.get("transaction_date") or t.get("transfer_date")) ' ``` ```php prepare('SELECT 1 FROM giao_dich WHERE transaction_code = ?'); for ($page = 1; $page <= 50; $page++) { $url = "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=$va&page=$page&limit=100"; $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]); $res = json_decode(curl_exec($ch), true); curl_close($ch); $items = $res['data']['data'] ?? []; // data.data = mảng giao dịch, kèm current_page/last_page if (!$items) break; $moi = 0; foreach ($items as $t) { $check->execute([$t['transaction_code']]); if ($check->fetch()) continue; // đã có, webhook đã tới xu_ly_giao_dich($t); // cùng hàm xử lý với webhook, chống trùng bằng UNIQUE $moi++; } if ($moi === 0) break; // cả trang đều đã có, dừng } ``` ```js // doi-soat.js: chạy bằng cron mỗi giờ (node doi-soat.js) const TOKEN = process.env.MONA_ACCESS_TOKEN; const VA = '1234567890'; async function doiSoat() { for (let page = 1; page <= 50; page++) { const url = `https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=${VA}&page=${page}&limit=100`; const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } }); const body = await res.json(); let items = body.data?.data ?? []; // data.data = mảng giao dịch, kèm current_page/last_page if (items.length === 0) break; let moi = 0; for (const t of items) { const daCo = await db.exists('giao_dich', { transaction_code: t.transaction_code }); if (daCo) continue; await xuLyGiaoDich(t); // cùng hàm xử lý với webhook moi++; } if (moi === 0) break; } } doiSoat().catch((e) => { console.error(e); process.exit(1); }); ``` Lưu ý `access_token` có hạn dùng (`expires_in` khi đăng nhập). Script cron nên đăng nhập lại khi nhận 401, xem [Xác thực](/docs/api/xac-thuc). ## Lịch chạy gợi ý | Loại hệ thống | Tần suất | Lý do | |---|---|---| | Web bán hàng giao ngay, ví bán vé, khoá học mở tức thì | Mỗi 15 phút | Khách chờ ngay sau khi chuyển | | Phần mềm quản lý, thu học phí, thu phí dịch vụ | Mỗi giờ | Đủ nhanh, ít tốn tài nguyên | | Kế toán cuối ngày | 23:30 mỗi ngày | Chốt sổ, in báo cáo | Kết hợp cả hai: mỗi giờ chạy đối soát nhanh trang 1, cuối ngày chạy đủ mọi trang. ## Đối soát bằng dashboard Không cần code, anh chị vào dashboard mục Giao dịch, lọc theo VA và khoảng ngày, xuất CSV rồi so với sổ bằng Excel: cột `transaction_code` là khoá, dùng VLOOKUP hoặc đối chiếu bằng Power Query. Cách này hợp với kế toán chốt sổ tuần hoặc tháng. ## Lỗi thường gặp **Gọi API trả 401.** Token hết hạn. Đăng nhập lại lấy token mới. **Trang 1 trống dù có giao dịch.** Sai `virtual_account_number`, hoặc giao dịch vào tài khoản chính chứ không vào VA đó. Kiểm số VA trong dashboard mục Ngân hàng & VA. **Đối soát chèn trùng giao dịch.** Bảng chưa có UNIQUE trên `transaction_code`, hoặc hàm xử lý webhook và hàm đối soát ghi vào hai bảng khác nhau. Dùng chung một hàm và một bảng. **Script chạy quá lâu.** Đang kéo hết mọi trang mỗi lần. Dừng ngay khi gặp trang không còn mã mới, như thuật toán trên. --- # Xác thực API ngân hàng MONA Pay: Bearer token và X-Client-Secret > Đăng nhập lấy Bearer token (hạn 24 giờ), kèm X-Client-Secret cho POST/PUT/DELETE. Envelope success/message/data, mã lỗi 401/422 và code mẫu cURL, PHP, Node. > URL: https://monapay.vn/docs/api/xac-thuc · Markdown: https://monapay.vn/docs/api/xac-thuc.md Mọi lệnh gọi API MONA Pay cần header `Authorization: Bearer `. Token lấy từ `POST /api/v1/client/login` bằng username và mật khẩu của tài khoản my.monapay.vn, hạn dùng 86.400 giây (24 giờ). Các lệnh ghi dữ liệu (POST, PUT, DELETE) gửi thêm header `X-Client-Secret` với secret sinh ở mục [API keys](/docs/api/api-keys). Mọi response đều bọc trong một khung chung `{"success": true, "message": "...", "data": ...}`. ## Base URL | Môi trường | URL | |---|---| | Production | `https://api.monapay.vn` | | Alias cũ (vẫn chạy, cho tích hợp trước 2026) | `https://ipn.mona.host` | Tài khoản đăng ký xong dùng được ngay: đăng nhập, tạo API key, không cần ai duyệt. Sandbox tách riêng (dữ liệu giả lập, không đụng ngân hàng thật) đang triển khai. ## Khung response chung Mọi endpoint trả về cùng một khung, kể cả khi lỗi: ```json { "success": true, "message": "Success", "data": { } } ``` | Trường | Kiểu | Ý nghĩa | |---|---|---| | `success` | boolean | `true` khi xử lý xong, `false` khi lỗi nghiệp vụ | | `message` | string | Thông điệp ngắn để log | | `data` | object / array / null | Dữ liệu trả về, `null` nếu không có | Lỗi validate (thiếu trường, sai kiểu) trả HTTP 422 theo chuẩn FastAPI với `detail[]` liệt kê từng trường sai. ## Hai lớp xác thực | Lớp | Header | Dùng khi | Lấy ở đâu | |---|---|---|---| | Bearer token | `Authorization: Bearer ` | Mọi request (trừ đăng ký, đăng nhập) | `POST /api/v1/client/login` | | Client secret | `X-Client-Secret: ` | POST, PUT, DELETE | `POST /api/v1/client-keys/generate` | Giữ token và secret ở biến môi trường, không ghi cứng trong code, không commit lên git. ## POST /api/v1/client/register-client Tạo tài khoản mới. Không cần xác thực. Tài khoản tạo xong dùng được ngay, đăng nhập liền không cần duyệt. Đăng ký qua form tại [my.monapay.vn/auth](https://my.monapay.vn/auth) (tab Đăng ký) cũng đi qua đúng endpoint này. | Trường | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `username` | string (1-255) | có | Tên đăng nhập, dùng để login | | `password` | string (1-255) | có | Mật khẩu | | `name` | string (1-255) | có | Tên doanh nghiệp / tên hiển thị | ```bash curl -X POST https://api.monapay.vn/api/v1/client/register-client \ -H 'Content-Type: application/json' \ -d '{"username":"shopabc","password":"MatKhauManh#2026","name":"Shop ABC"}' ``` Response 200/201: ```json { "success": true, "message": "Client registered successfully", "data": {} } ``` ## POST /api/v1/client/login Đổi username + mật khẩu lấy `access_token`. Không cần xác thực. | Trường | Kiểu | Bắt buộc | |---|---|---| | `username` | string | có | | `password` | string | có | Response 200: ```json { "success": true, "message": "Login successful", "data": { "access_token": "Zx9...64-ky-tu-url-safe", "expires_in": 86400, "token_type": "Bearer" } } ``` `expires_in` tính bằng giây. Hết hạn thì gọi login lại, không có refresh token. Nếu tài khoản bật 2FA trong dashboard, bước đăng nhập trên web sẽ hỏi thêm mã OTP; gọi API trực tiếp thì dùng tài khoản chưa bật 2FA hoặc liên hệ tụi em. **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/client/login \ -H 'Content-Type: application/json' \ -d '{"username":"shopabc","password":"MatKhauManh#2026"}' ``` **PHP** ```php [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => json_encode(['username' => getenv('MONA_USER'), 'password' => getenv('MONA_PASS')]), ], ])), true); if (empty($res['success'])) { throw new RuntimeException('Đăng nhập MONA Pay thất bại: ' . ($res['message'] ?? 'không rõ')); } $accessToken = $res['data']['access_token']; // lưu lại, dùng 24 giờ ``` **Node** ```js const res = await fetch('https://api.monapay.vn/api/v1/client/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: process.env.MONA_USER, password: process.env.MONA_PASS }), }); const json = await res.json(); if (!json.success) throw new Error('Đăng nhập MONA Pay thất bại: ' + json.message); const accessToken = json.data.access_token; // lưu lại, dùng 24 giờ ``` ## GET /api/v1/client/me Thông tin tài khoản đang đăng nhập. Cần Bearer. ```bash curl https://api.monapay.vn/api/v1/client/me \ -H "Authorization: Bearer $MONA_TOKEN" ``` Response: `data` gồm thông tin client (id, username, name, trạng thái kích hoạt, thời điểm tạo). ## PUT /api/v1/client/change-password Đổi mật khẩu của chính mình. Cần Bearer + `X-Client-Secret`. | Trường | Kiểu | Bắt buộc | |---|---|---| | `old_password` | string | có | | `new_password` | string | có | ```bash curl -X PUT https://api.monapay.vn/api/v1/client/change-password \ -H "Authorization: Bearer $MONA_TOKEN" \ -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"old_password":"MatKhauManh#2026","new_password":"MatKhauMoi#2026"}' ```
**Trạng thái ép buộc (kiểm 28/08/2026):** máy chủ production hiện chưa từ chối lệnh ghi thiếu `X-Client-Secret`; bản cập nhật ép buộc đã viết xong, đang chờ đưa lên. Anh chị gửi header này ngay từ bây giờ để khi bật lên không phải sửa gì.
Đổi mật khẩu xong thì token cũ vẫn dùng được tới khi hết hạn; muốn cắt ngay thì đăng nhập lại và thu hồi key ở mục API keys. ## Lỗi thường gặp | HTTP | Nguyên nhân | Cách xử lý | |---|---|---| | 401 | Sai username/mật khẩu hoặc token hết hạn | Kiểm tra lại thông tin (tài khoản mới đăng nhập được ngay, không có bước chờ duyệt); token hết hạn thì login lại | | 401 `Authorization scheme must be Bearer` | Header sai định dạng | Đúng dạng `Authorization: Bearer `, có khoảng trắng sau chữ Bearer | | 422 | Thiếu trường bắt buộc hoặc sai kiểu | Đọc `detail[]` trong body, sửa đúng tên trường | | 500 | Lỗi phía MONA Pay | Thử lại sau vài giây; lặp lại thì báo tụi em kèm `message` | ## Bước tiếp theo 1. [Tạo API key](/docs/api/api-keys) để có `X-Client-Secret`. 2. [Nối tài khoản ACB, tạo tài khoản ảo](/docs/api/tai-khoan-ao-va). 3. [Cấu hình webhook](/docs/api/webhook-configs) để nhận thông báo tiền vào. --- # API keys: tạo, liệt kê, thu hồi X-Client-Secret > Sinh client_secret (hiện 1 lần duy nhất), liệt kê và thu hồi key. Secret gửi qua header X-Client-Secret cho mọi POST/PUT/DELETE. > URL: https://monapay.vn/docs/api/api-keys · Markdown: https://monapay.vn/docs/api/api-keys.md API key của MONA Pay là chuỗi `client_secret` sinh bằng `POST /api/v1/client-keys/generate`. Hệ thống chỉ hiện secret đúng 1 lần lúc tạo, sau đó lưu dạng băm nên không xem lại được. Anh chị gửi secret qua header `X-Client-Secret` ở mọi request POST, PUT, DELETE (kèm Bearer token). Mất secret thì tạo key mới rồi thu hồi key cũ, mỗi tài khoản tạo được nhiều key. ## Khi nào cần key | Việc | Cần Bearer | Cần X-Client-Secret | |---|---|---| | Đăng ký, đăng nhập | không | không | | Đọc dữ liệu (GET): giao dịch, VA, log webhook | có | không | | Ghi dữ liệu (POST, PUT, DELETE): tạo VA, tạo QR, cấu hình webhook/Telegram, đổi mật khẩu | có | có | Cách làm sạch: mỗi hệ thống tích hợp (web bán hàng, phần mềm kế toán, bot) dùng 1 key riêng, đặt tên theo hệ thống đó. Hệ thống nào bị lộ thì thu hồi đúng key đó, hệ thống khác không ảnh hưởng. ## POST /api/v1/client-keys/generate Cần Bearer. | Trường | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `name` | string | không | Tên gợi nhớ, mặc định `Default Key` | Response 200: ```json { "success": true, "message": "Key generated successfully", "data": { "id": "0190a1b2-...", "client_id": "0190a0f0-...", "client_secret": "mps_...chuoi-bi-mat-chi-hien-1-lan", "name": "Web ban hang", "is_active": true, "created_at": "2026-08-28T10:30:00" } } ``` Lưu `client_secret` ngay vào biến môi trường (`MONA_SECRET`). Đóng màn hình là không lấy lại được. **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/client-keys/generate \ -H "Authorization: Bearer $MONA_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"Web ban hang"}' ``` **PHP** ```php ['method' => 'POST', 'header' => $headers, 'content' => json_encode($body), 'ignore_errors' => true], ])); return json_decode($raw, true) ?? ['success' => false, 'message' => 'Không đọc được response']; } $res = monaPost('/api/v1/client-keys/generate', ['name' => 'Web ban hang'], getenv('MONA_TOKEN')); $clientSecret = $res['data']['client_secret'] ?? null; // ghi vào .env, chỉ hiện 1 lần ``` **Node** ```js const res = await fetch('https://api.monapay.vn/api/v1/client-keys/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}` }, body: JSON.stringify({ name: 'Web ban hang' }), }); const { success, data, message } = await res.json(); if (!success) throw new Error(message); console.log('Ghi vào .env:', data.client_secret); // chỉ hiện 1 lần ``` ## GET /api/v1/client-keys/list Danh sách key của tài khoản, không kèm secret. Cần Bearer. ```bash curl https://api.monapay.vn/api/v1/client-keys/list \ -H "Authorization: Bearer $MONA_TOKEN" ``` ```json { "success": true, "message": "Keys retrieved", "data": [ { "id": "0190a1b2-...", "client_id": "0190a0f0-...", "name": "Web ban hang", "is_active": true, "created_at": "2026-08-28T10:30:00" } ] } ``` ## DELETE /api/v1/client-keys/destroy/{key_id} Thu hồi key. Cần Bearer. Request đang dùng secret của key này sẽ bị từ chối ngay sau khi thu hồi. ```bash curl -X DELETE https://api.monapay.vn/api/v1/client-keys/destroy/0190a1b2-... \ -H "Authorization: Bearer $MONA_TOKEN" ``` Response: `{"success": true, "message": "Key destroyed", "data": null}`. Key không tồn tại hoặc không thuộc tài khoản trả 404. ## Dùng secret trong request ghi ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks \ -H "Authorization: Bearer $MONA_TOKEN" \ -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"name":"Web ban hang","webhook_url":"https://shop.vn/webhook/monapay","auth_type":"HMAC_SHA256","secret_key":"hmac-cua-anh-chi"}' ``` Lưu ý phân biệt 2 loại secret: `client_secret` (X-Client-Secret) là để anh chị gọi API MONA Pay; `secret_key` trong cấu hình webhook là để MONA Pay ký payload gửi sang server anh chị (xem [Bảo mật webhook](/docs/webhooks/bao-mat)). Hai chuỗi này nên khác nhau.
**Trạng thái ép buộc (kiểm 28/08/2026):** máy chủ production hiện chưa từ chối lệnh ghi thiếu `X-Client-Secret`; bản cập nhật ép buộc đã viết xong, đang chờ đưa lên. Anh chị gửi header này ngay từ bây giờ để khi bật lên không phải sửa gì.
## Lỗi thường gặp | HTTP | Nguyên nhân | Cách xử lý | |---|---|---| | 401 | Thiếu hoặc sai Bearer token | Login lại lấy token mới | | 404 | `key_id` không tồn tại hoặc không thuộc tài khoản | Gọi `/client-keys/list` lấy đúng id | | 422 | `key_id` không đúng định dạng UUID | Copy nguyên id từ danh sách | Thao tác tạo và thu hồi key cũng làm được trên dashboard tại mục API Keys, có sẵn khối "copy prompt cho AI agent" để dán vào Claude Code, Codex hoặc Cursor. --- # Tài khoản ngân hàng ảo (virtual account) ACB: tạo VA qua API > Đăng ký VA ACB 4 bước qua API: gửi yêu cầu, xác thực OTP, đăng ký nhận thông báo, xác thực OTP lần 2. Kèm truy vấn, hủy VA và code mẫu cURL, PHP, Node. > URL: https://monapay.vn/docs/api/tai-khoan-ao-va · Markdown: https://monapay.vn/docs/api/tai-khoan-ao-va.md Tài khoản ảo (VA) là số tài khoản phụ do ACB cấp dưới tài khoản thật của anh chị, theo đầu số (prefix) đã đăng ký. Tiền chuyển vào VA vẫn nằm trong tài khoản ACB thật, nhưng mỗi VA gắn được với 1 đơn hàng hoặc 1 khách nên MONA Pay khớp tiền tự động, không cần đọc nội dung chuyển khoản. Tạo VA qua API gồm 4 bước: gửi yêu cầu đăng ký, nhập OTP ACB gửi về số điện thoại, đăng ký nhận thông báo giao dịch, nhập OTP lần 2. Điều kiện: tài khoản ACB đứng tên anh chị và số điện thoại đang đăng ký với ACB. Anh chị không muốn gọi API thì dashboard my.monapay.vn có sẵn wizard 4 bước y hệt, tại mục Ngân hàng & VA. ## Luồng 4 bước | Bước | Endpoint | Ai làm gì | |---|---|---| | 1 | `POST /api/v1/acb/virtual-account/registration` | Gửi số tài khoản, số điện thoại, đầu số VA. ACB nhận yêu cầu, gửi OTP về điện thoại | | 2 | `POST /api/v1/acb/{acb_request_id}/virtual-account/verification` | Nhập OTP. ACB tạo VA, trả số VA | | 3 | `POST /api/v1/acb/{virtual_account_id}/notification/registration` | Đăng ký nhận thông báo giao dịch tức thì. ACB gửi OTP lần 2 | | 4 | `POST /api/v1/acb/{acb_request_id}/notification/verification` | Nhập OTP lần 2. Từ đây tiền vào là có thông báo về MONA Pay | Bỏ bước 3 và 4 thì VA có tồn tại nhưng ACB không báo giao dịch về, webhook và Telegram sẽ im lặng. Đây là lỗi tụi em gặp nhiều nhất khi khách tự làm. Mọi request dưới đây cần `Authorization: Bearer` và `X-Client-Secret` (xem [Xác thực](/docs/api/xac-thuc)). ## Bước 1: POST /api/v1/acb/virtual-account/registration | Trường | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `bank_account_id` | uuid | không | Dùng khi tài khoản ACB đã nối trước đó (lấy từ `GET /api/v1/client/bank-accounts`). Có trường này thì bỏ `account_number`, `phone_number` | | `customer_type` | string | không | Loại khách hàng theo mã ACB, ví dụ `PERS` cho cá nhân | | `account_number` | integer | không | Số tài khoản thanh toán ACB (số thật) | | `phone_number` | string | không | Số điện thoại đăng ký với ACB, nhận OTP | | `virtual_account_info.virtual_account_prefix_code` | string | có | Đầu số VA đã đăng ký với ACB | | `virtual_account_info.virtual_account_content` | string | không | Nội dung định danh gắn với VA (mã đơn, mã khách) | | `virtual_account_info.virtual_account_explain` | string | không | Diễn giải khi đăng ký | | `virtual_account_info.beneficiary_name_rule` | integer | không | Cách hiển thị tên đơn vị hưởng theo quy ước ACB | | `user_agreement` | boolean | không | Khách đồng ý điều khoản dịch vụ, nên gửi `true` | Body mẫu (lần đầu, chưa có bank_account_id): ```json { "customer_type": "PERS", "account_number": 123456789, "phone_number": "0901234567", "virtual_account_info": { "virtual_account_prefix_code": "MONA", "virtual_account_content": "DH10234", "virtual_account_explain": "Don hang 10234" }, "user_agreement": true } ``` Response 200: `data` là tài khoản ngân hàng vừa ghi nhận, kèm `acb_request` chứa `id` cần cho bước 2. ```json { "success": true, "message": "Success", "data": { "id": "0190b0c1-...", "account_number": "123456789", "acb_request": { "id": "0190b0c2-...", "status": "PENDING", "created_at": "2026-08-28T10:31:00" } } } ``` **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/acb/virtual-account/registration \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"customer_type":"PERS","account_number":123456789,"phone_number":"0901234567","virtual_account_info":{"virtual_account_prefix_code":"MONA","virtual_account_content":"DH10234"},"user_agreement":true}' ``` ## Bước 2: POST /api/v1/acb/{acb_request_id}/virtual-account/verification | Trường | Kiểu | Bắt buộc | |---|---|---| | `code` | string | có, là OTP ACB gửi về điện thoại | ```bash curl -X POST https://api.monapay.vn/api/v1/acb/0190b0c2-.../virtual-account/verification \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' -d '{"code":"123456"}' ``` Response 200: `data` là VA vừa tạo. ```json { "success": true, "message": "Success", "data": { "id": "0190b0c3-...", "bank_account_id": "0190b0c1-...", "prefix_code": "MONA", "content": "DH10234", "explain": "Don hang 10234", "beneficiary_name_rule": null, "virtual_account_number": "MONA0000010234", "created_at": "2026-08-28T10:32:00", "updated_at": null } } ``` `virtual_account_number` là số anh chị đưa cho khách chuyển tiền (hoặc đưa vào QR). `id` dùng cho bước 3. ## Bước 3: POST /api/v1/acb/{virtual_account_id}/notification/registration | Trường | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `receive_noti_realtime` | boolean | có | `true` = báo ngay từng giao dịch (cần cho webhook). `false` = ACB gộp báo cuối ngày | | `username` | string (≤50) | không | Thông tin đăng nhập theo yêu cầu ACB, để trống nếu không được ACB yêu cầu | ```bash curl -X POST https://api.monapay.vn/api/v1/acb/0190b0c3-.../notification/registration \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' -d '{"receive_noti_realtime":true}' ``` Response 200/201: `data` chứa `acb_request.id` cho bước 4. ## Bước 4: POST /api/v1/acb/{acb_request_id}/notification/verification Body `{"code": ""}`, giống bước 2. Thành công là xong: từ giao dịch kế tiếp, ACB báo về MONA Pay, MONA Pay bắn [webhook](/docs/webhooks/tich-hop-webhook) hoặc [Telegram](/docs/telegram) theo cấu hình của anh chị. ## Ví dụ PHP: chạy trọn bước 1 và 2 ```php true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]); $res = json_decode(curl_exec($ch), true) ?? []; curl_close($ch); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Lỗi không rõ'); return $res['data']; } // Bước 1: gửi yêu cầu, ACB gửi OTP về điện thoại $reg = call("$base/api/v1/acb/virtual-account/registration", $headers, [ 'customer_type' => 'PERS', 'account_number' => 123456789, 'phone_number' => '0901234567', 'virtual_account_info' => ['virtual_account_prefix_code' => 'MONA', 'virtual_account_content' => 'DH10234'], 'user_agreement' => true, ]); $requestId = $reg['acb_request']['id']; // Bước 2: nhập OTP (đọc từ form của anh chị) $va = call("$base/api/v1/acb/$requestId/virtual-account/verification", $headers, ['code' => $_POST['otp']]); echo 'Số VA: ' . $va['virtual_account_number']; ``` ## Ví dụ Node: bước 3 và 4 ```js const base = 'https://api.monapay.vn'; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET, }; async function call(path, body) { const r = await fetch(base + path, { method: 'POST', headers, body: JSON.stringify(body) }); const j = await r.json(); if (!j.success) throw new Error(j.message); return j.data; } // Bước 3: đăng ký nhận thông báo tức thì, ACB gửi OTP lần 2 const noti = await call(`/api/v1/acb/${vaId}/notification/registration`, { receive_noti_realtime: true }); // Bước 4: xác thực OTP lần 2 await call(`/api/v1/acb/${noti.acb_request.id}/notification/verification`, { code: otpLan2 }); ``` ## Truy vấn và quản lý | Endpoint | Việc | |---|---| | `GET /api/v1/client/bank-accounts?page=1&limit=10` | Danh sách tài khoản ACB đã nối (lấy `bank_account_id`) | | `GET /api/v1/acb/{bank_account_id}/virtual-account/retrieve?virtual_account_number=&page=1&limit=10` | Danh sách VA của một tài khoản, lọc theo số VA, `limit` tối đa 100 | | `GET /api/v1/acb/{virtual_account_id}/notification/details` | Xem đăng ký thông báo của VA | | `POST /api/v1/acb/{acb_notification_id}/notification/modification` | Đổi kiểu nhận thông báo (body như bước 3), cần OTP xác thực lại qua bước 4 | | `POST /api/v1/acb/{acb_notification_id}/notification/delete` | Hủy nhận thông báo | | `POST /api/v1/acb/{virtual_account_id}/virtual-account/delete` | Hủy VA, không có body. ACB có thể yêu cầu OTP xác thực qua endpoint verification | ## Lỗi thường gặp | Tình huống | Nguyên nhân | Cách xử lý | |---|---|---| | 400 sau bước 1 | Số tài khoản không phải ACB, sai số điện thoại đăng ký với ACB, hoặc đầu số VA chưa được ACB cấp | Kiểm tra lại với ACB; đầu số VA cần đăng ký trước với ACB | | 400 `code` sai ở bước 2/4 | OTP nhập sai hoặc hết hạn | Làm lại bước trước để ACB gửi OTP mới | | VA tạo xong nhưng không thấy giao dịch | Chưa làm bước 3 và 4 | Gọi `notification/registration` rồi xác thực OTP lần 2 | | 401 | Token hết hạn hoặc thiếu X-Client-Secret | Login lại, kiểm tra header | | 422 | Thiếu `virtual_account_info.virtual_account_prefix_code` | Trường này bắt buộc | --- # API tạo mã QR ngân hàng ACB (VietQR động) theo đơn hàng > Tạo mã VietQR ACB gắn mã đơn và số tiền qua POST /acb/qr-payment/generate, nhận qr_data_url để hiển thị; hủy QR chưa thanh toán. Code cURL, PHP, Node. > URL: https://monapay.vn/docs/api/qr-thanh-toan · Markdown: https://monapay.vn/docs/api/qr-thanh-toan.md QR thanh toán của MONA Pay là mã VietQR động do ACB tạo: đã điền sẵn số tài khoản (hoặc VA), số tiền và mã đơn, khách mở app ngân hàng bất kỳ quét là chuyển đúng số tiền, đúng nội dung, không gõ tay. Gọi `POST /api/v1/acb/qr-payment/generate` với `orderId`, `amount` (VND, số nguyên, tối đa 1.000.000.000) và đầu số VA, MONA Pay trả về `qr_data_url` để anh chị hiển thị ngay trên trang thanh toán. Khi khách trả xong, ACB báo về và MONA Pay bắn [webhook](/docs/webhooks/tich-hop-webhook) y như tiền vào VA. QR tạo nhầm thì hủy bằng endpoint cancellation trước khi khách quét. Không muốn gọi API thì dashboard có mục Tạo QR: chọn VA, nhập số tiền, tải ảnh QR về in hoặc gửi khách. ## POST /api/v1/acb/qr-payment/generate Cần Bearer + `X-Client-Secret`. Điều kiện: tài khoản ACB đã nối và có VA (xem [Tài khoản ảo](/docs/api/tai-khoan-ao-va)). | Trường | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `ownerNumber` | string | có | Số tài khoản ACB nhận tiền | | `ownerType` | `PER` / `ORG` | có | `PER` cá nhân, `ORG` doanh nghiệp | | `merchantId` | string | có | Mã merchant do ACB cấp khi mở dịch vụ QR. Giá trị đúng hiển thị ở dashboard mục Tạo QR | | `terminalId` | string | có | Mã điểm bán do ACB cấp, cùng chỗ với `merchantId` | | `orderId` | string | có | Mã đơn của anh chị, duy nhất cho mỗi lần thanh toán | | `virtualAccountPrefix` | string (1-10) | có | Đầu số VA | | `beneficiaryName` | string (1-100) | có | Tên người/đơn vị hưởng hiện trên app ngân hàng của khách | | `amount` | integer | có | Số tiền VND, 0 đến 1.000.000.000. `0` = khách tự nhập số tiền | | `description` | string (≤255) | không | Nội dung chuyển khoản hiện sẵn cho khách | | `traceNumber` | string | không | Mã theo dõi riêng của anh chị, nên đặt để tra soát | | `userId` | string | không | Mã khách trong hệ thống của anh chị | | `voucherCode`, `loyaltyCode` | string | không | Mã giảm giá / tích điểm nếu anh chị cần lưu kèm | | `additionalInfo` | array `[{key, value}]` | không | Thông tin phụ dạng khoá-giá trị | Body mẫu: ```json { "ownerNumber": "123456789", "ownerType": "ORG", "merchantId": "MC00012345", "terminalId": "TM0001", "orderId": "DH10234", "virtualAccountPrefix": "MONA", "beneficiaryName": "CONG TY ABC", "amount": 2500000, "description": "Thanh toan DH10234", "traceNumber": "DH10234-20260828" } ``` Response 200: `data` là bản ghi QR, quan trọng nhất là `qr_data_url` và `virtual_account_number`. ```json { "success": true, "message": "Success", "data": { "id": "0190c0d1-...", "virtual_account_id": "0190b0c3-...", "owner_number": "123456789", "owner_type": "ORG", "merchant_id": "MC00012345", "terminal_id": "TM0001", "user_id": null, "order_id": "DH10234", "virtual_account_prefix": "MONA", "beneficiary_name": "CONG TY ABC", "amount": 2500000, "voucher_code": null, "loyalty_code": null, "description": "Thanh toan DH10234", "additional_info": null, "virtual_account_number": "MONA0000010234", "trace_number": "DH10234-20260828", "qr_data_url": "00020101021238...6304ABCD", "created_at": "2026-08-28T10:40:00", "updated_at": null } } ``` `qr_data_url` là chuỗi dữ liệu QR chuẩn VietQR (EMVCo). Anh chị đưa chuỗi này vào thư viện vẽ QR bất kỳ (ví dụ `qrcode` của Node, `endroid/qr-code` của PHP) để ra ảnh. Lưu `id` để hủy khi cần. **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/acb/qr-payment/generate \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"ownerNumber":"123456789","ownerType":"ORG","merchantId":"MC00012345","terminalId":"TM0001","orderId":"DH10234","virtualAccountPrefix":"MONA","beneficiaryName":"CONG TY ABC","amount":2500000,"description":"Thanh toan DH10234"}' ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('MONA_TOKEN'), 'X-Client-Secret: ' . getenv('MONA_SECRET')], CURLOPT_POSTFIELDS => json_encode([ 'ownerNumber' => '123456789', 'ownerType' => 'ORG', 'merchantId' => getenv('ACB_MERCHANT_ID'), 'terminalId' => getenv('ACB_TERMINAL_ID'), 'orderId' => $order->code, 'virtualAccountPrefix' => 'MONA', 'beneficiaryName' => 'CONG TY ABC', 'amount' => (int) $order->total, 'description' => 'Thanh toan ' . $order->code, ]), ]); $res = json_decode(curl_exec($ch), true); curl_close($ch); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Tạo QR thất bại'); $qrString = $res['data']['qr_data_url']; // đưa vào thư viện vẽ QR $qrId = $res['data']['id']; // lưu để hủy nếu đơn bị huỷ ``` **Node** ```js import QRCode from 'qrcode'; // npm i qrcode const r = await fetch('https://api.monapay.vn/api/v1/acb/qr-payment/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET }, body: JSON.stringify({ ownerNumber: '123456789', ownerType: 'ORG', merchantId: process.env.ACB_MERCHANT_ID, terminalId: process.env.ACB_TERMINAL_ID, orderId: order.code, virtualAccountPrefix: 'MONA', beneficiaryName: 'CONG TY ABC', amount: order.total, description: `Thanh toan ${order.code}`, }), }); const { success, data, message } = await r.json(); if (!success) throw new Error(message); const pngDataUrl = await QRCode.toDataURL(data.qr_data_url); // ``` ## DELETE /api/v1/acb/qr-payment/{qr_code_id}/cancellation Hủy QR đã tạo nhưng khách chưa thanh toán (đơn bị hủy, đổi số tiền). Cần Bearer + `X-Client-Secret`. Body bắt buộc gửi lại thông tin khớp với lúc tạo: | Trường | Kiểu | Bắt buộc | |---|---|---| | `ownerNumber` | string | có | | `ownerType` | `PER` / `ORG` | có | | `orderId` | string | có | | `amount` | integer | có | | `traceNumber` | string | không | ```bash curl -X DELETE https://api.monapay.vn/api/v1/acb/qr-payment/0190c0d1-.../cancellation \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"ownerNumber":"123456789","ownerType":"ORG","orderId":"DH10234","amount":2500000}' ``` Response: `{"success": true, "message": "Success", "data": null}`. ## Khách quét xong thì sao ACB ghi nhận tiền vào, báo cho MONA Pay, MONA Pay bắn webhook tới URL anh chị cấu hình với payload y như giao dịch VA: ```json {"amount":2500000,"description":"Thanh toan DH10234","transfer_date":"10:45:12 28/08/2026","transaction_code":"FT26240001234","account_number":"MONA0000010234","bank_name":"ACB","type":"income"} ``` Khớp đơn bằng `account_number` (số VA gắn với QR) hoặc `description` chứa `orderId`. Nhớ so `amount` với số tiền đơn trước khi đổi trạng thái đã thanh toán. ## Lỗi thường gặp | Tình huống | Nguyên nhân | Cách xử lý | |---|---|---| | 400 khi tạo | Tài khoản chưa đăng ký VA với đầu số này, hoặc `merchantId`/`terminalId` sai | Kiểm tra ở dashboard mục Tạo QR, tạo thử 1 mã trên web trước | | 422 | Thiếu trường bắt buộc, `amount` vượt 1.000.000.000, `virtualAccountPrefix` dài quá 10 ký tự | Sửa theo `detail[]` | | Khách quét được nhưng không thấy webhook | VA chưa đăng ký nhận thông báo (bước 3, 4 khi tạo VA) | Xem [Tài khoản ảo](/docs/api/tai-khoan-ao-va) | | QR hết hạn trên app ngân hàng | Mỗi QR động có thời hạn theo quy định ACB | Tạo QR mới với cùng `orderId` sau khi hủy mã cũ | --- # API giao dịch: tra cứu tiền vào theo tài khoản ảo > Truy vấn giao dịch theo số VA (phân trang, tối đa 100/trang), cấu trúc bản ghi, đối soát bằng transaction_code và endpoint gửi lại thông báo cho 1 giao dịch. > URL: https://monapay.vn/docs/api/giao-dich · Markdown: https://monapay.vn/docs/api/giao-dich.md Mọi giao dịch ACB báo về đều được MONA Pay lưu lại, kể cả khi webhook tới server anh chị thất bại. Truy vấn bằng `GET /api/v1/acb/virtual-account/transactions?virtual_account_number=&page=1&limit=100` (Bearer token, `limit` tối đa 100). Mỗi bản ghi có `transaction_code` là mã tham chiếu ổn định của ngân hàng, dùng làm khoá chống trùng khi đối soát. Giao dịch nào lỡ không tới được server thì gọi endpoint retry để MONA Pay gửi lại webhook hoặc tin Telegram cho đúng giao dịch đó. ## GET /api/v1/acb/virtual-account/transactions Cần Bearer. Hệ thống tìm VA theo số, kiểm tra VA thuộc tài khoản của anh chị rồi mới trả giao dịch. | Tham số (query) | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `virtual_account_number` | string | có | Số VA, ví dụ `MONA0000010234` | | `page` | integer ≥1 | không | Mặc định 1 | | `limit` | integer 1-100 | không | Mặc định 10 | ```bash curl "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=MONA0000010234&page=1&limit=100" \ -H "Authorization: Bearer $MONA_TOKEN" ``` Response 200: ```json { "success": true, "message": "Success", "data": { "data": [ { "id": "0190d0e1-...", "acb_callback_request_id": "0190d0e0-...", "transaction_status": "SUCCESS", "transaction_channel": "IBFT", "transaction_date": "2026-08-28T10:45:12", "effective_date": "2026-08-28T10:45:12", "debit_or_credit": "credit", "amount": 2500000, "transaction_content": "Thanh toan DH10234", "transaction_code": "FT26240001234", "account_number": "123456789", "va_prefix_cd": "MONA", "va_nbr": "MONA0000010234", "attributes": { "remitter_name": "NGUYEN VAN A", "remitter_account_number": "9876543210", "issuer_bank_name": "Vietcombank", "reference_number": "FT26240001234" } } ], "current_page": 1, "per_page": 100, "total": 1, "last_page": 1, "start": 1, "end": 1, "has_next": false, "has_prev": false } } ``` ### Ý nghĩa từng trường | Trường | Ý nghĩa | |---|---| | `id` | ID giao dịch trong MONA Pay, dùng cho endpoint retry | | `transaction_status` | Trạng thái ACB trả về (`SUCCESS` là tiền đã vào) | | `transaction_channel` | Kênh giao dịch theo ACB (chuyển khoản nhanh, QR...) | | `transaction_date` | Thời điểm giao dịch, giờ Việt Nam | | `effective_date` | Ngày hiệu lực ghi sổ | | `debit_or_credit` | `credit` = tiền vào, `debit` = tiền ra. Webhook hiện chỉ bắn cho tiền vào | | `amount` | Số tiền VND, số nguyên | | `transaction_content` | Nội dung chuyển khoản khách gõ (hoặc QR điền sẵn) | | `transaction_code` | Mã tham chiếu ngân hàng, ổn định qua mọi lần gửi lại, khoá chống trùng | | `account_number` | Số tài khoản ACB thật nhận tiền | | `va_prefix_cd`, `va_nbr` | Đầu số và số VA khớp giao dịch, `null` nếu tiền vào thẳng tài khoản chính | | `attributes` | Thông tin phụ ACB gửi kèm: tên và số tài khoản người chuyển, ngân hàng chuyển, số tham chiếu, các trường `custom1`..`custom10` nếu có | Ngoài ra, cùng bản ghi này là thứ webhook gửi sang anh chị dưới dạng rút gọn (xem [Định dạng payload](/docs/webhooks/dinh-dang-payload)). ## Đối soát bằng API Hiện chưa có tham số `since_id`; đối soát theo cách sau (cron mỗi 15-30 phút hoặc cuối ngày): 1. Với mỗi VA đang dùng, gọi trang 1 với `limit=100`, đọc tiếp khi `has_next` là `true`. 2. Với mỗi bản ghi, tra `transaction_code` trong bảng giao dịch của anh chị. Có rồi thì bỏ qua, chưa có thì ghi thêm và xử lý đơn như khi nhận webhook. 3. Dừng khi gặp `transaction_code` đã có và `transaction_date` cũ hơn mốc đối soát lần trước (danh sách sắp xếp mới trước). Chi tiết và code mẫu ở [Đối soát giao dịch](/docs/webhooks/doi-soat). **PHP: đọc hết các trang** ```php ['header' => 'Authorization: Bearer ' . getenv('MONA_TOKEN') . "\r\n", 'ignore_errors' => true]]); return json_decode(file_get_contents($url, false, $ctx), true) ?? []; } $va = 'MONA0000010234'; $page = 1; do { $res = monaGet("https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=$va&page=$page&limit=100"); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Lỗi truy vấn'); foreach ($res['data']['data'] as $tx) { if ($tx['debit_or_credit'] !== 'credit') continue; // INSERT ... ON DUPLICATE KEY (UNIQUE transaction_code) → chỉ ghi bản chưa có ghiNhanGiaoDich($tx['transaction_code'], $tx['amount'], $tx['transaction_content'], $tx['transaction_date']); } $page++; } while (!empty($res['data']['has_next'])); ``` **Node** ```js async function* monaTransactions(va) { for (let page = 1; ; page++) { const r = await fetch(`https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=${va}&page=${page}&limit=100`, { headers: { Authorization: `Bearer ${process.env.MONA_TOKEN}` }, }); const j = await r.json(); if (!j.success) throw new Error(j.message); yield* j.data.data; if (!j.data.has_next) break; } } for await (const tx of monaTransactions('MONA0000010234')) { if (tx.debit_or_credit !== 'credit') continue; await db.upsertByTransactionCode(tx.transaction_code, tx); // UNIQUE(transaction_code) } ``` ## POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry Gửi lại thông báo cho một giao dịch cụ thể (server anh chị lúc đó bị lỗi, hoặc cấu hình webhook sai rồi sửa lại). Cần Bearer + `X-Client-Secret`. Gửi lại tự động theo lịch đang triển khai, hiện là thao tác chủ động này hoặc nút "Gửi lại" trong dashboard. | Trường | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `target_type` | `WEBHOOK` / `TELEGRAM` | có | Kênh cần gửi lại | | `target_id` | uuid | không | ID cấu hình webhook hoặc Telegram cụ thể. Bỏ trống = gửi lại theo mọi cấu hình đang bật khớp giao dịch | ```bash curl -X POST https://api.monapay.vn/api/v1/acb/virtual-account/transactions/0190d0e1-.../retry \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' -d '{"target_type":"WEBHOOK"}' ``` Payload gửi lại y hệt lần đầu, cùng `transaction_code`, nên server anh chị phải chống trùng (xem [Gửi lại và xử lý lỗi](/docs/webhooks/gui-lai-va-xu-ly-loi)). ## Lỗi thường gặp | HTTP | Nguyên nhân | Cách xử lý | |---|---|---| | 400 | Số VA không tồn tại hoặc không thuộc tài khoản | Lấy đúng số VA từ `GET /api/v1/acb/{bank_account_id}/virtual-account/retrieve` | | 401 | Token hết hạn | Login lại | | 422 | `limit` > 100 hoặc thiếu `virtual_account_number` | Sửa tham số | | 404 (retry) | `transaction_id` không tồn tại | Lấy `id` từ danh sách giao dịch | --- # Cấu hình webhook qua API > Tạo, sửa, xoá cấu hình webhook qua /api/v1/client-webhooks, gửi thử payload giả lập, đọc lịch sử gửi và thống kê tỷ lệ thành công, P95 qua /webhook-logs. > URL: https://monapay.vn/docs/api/webhook-configs · Markdown: https://monapay.vn/docs/api/webhook-configs.md Mọi thứ làm được ở dashboard mục Webhooks đều có API tương ứng: `POST /api/v1/client-webhooks` tạo cấu hình (URL nhận, kiểu xác thực HMAC_SHA256 / API_KEY / NONE, định dạng payload, gắn theo 1 VA hoặc mọi tài khoản), `PUT` và `DELETE /api/v1/client-webhooks/{config_id}` để sửa, xoá, `POST /api/v1/client-webhooks/test` bắn 1 payload giả lập 500.000đ để thử endpoint trước khi có tiền thật. Lịch sử từng lần gửi (mã HTTP, thời gian phản hồi `duration_ms`, nhãn lỗi) đọc ở `GET /api/v1/webhook-logs`, thống kê 7-30 ngày ở `GET /api/v1/webhook-logs/stats`. Cách MONA Pay ký và gửi payload xem ở [Tích hợp webhook](/docs/webhooks/tich-hop-webhook) và [Bảo mật](/docs/webhooks/bao-mat); trang này là tham chiếu API. ## Đối tượng cấu hình webhook | Trường | Kiểu | Bắt buộc khi tạo | Ghi chú | |---|---|---|---| | `name` | string | có | Tên gợi nhớ ("Web bán hàng", "Phần mềm kế toán") | | `webhook_url` | string | có | URL HTTPS nhận POST của anh chị | | `auth_type` | `NONE` / `API_KEY` / `HMAC_SHA256` | không, mặc định `NONE` | Nên dùng `HMAC_SHA256` | | `secret_key` | string | cần khi `auth_type` khác `NONE` | Secret để ký HMAC hoặc gửi trong header API key | | `api_key_name` | string | không, mặc định `X-Webhook-Secret` | Tên header mang secret khi `auth_type` = `API_KEY` | | `payload_format` | `application/json` / `application/x-www-form-urlencoded` / `multipart/form-data` | không, mặc định `application/json` | Kiểu body MONA Pay gửi | | `virtual_account_id` | uuid | không | Gắn cấu hình với 1 VA. Bỏ trống = mọi tài khoản, mọi VA | | `is_active` | boolean | chỉ khi PUT | Tắt tạm không cần xoá | Mỗi giao dịch tiền vào sẽ bắn tới mọi cấu hình đang bật khớp điều kiện: cấu hình "mọi tài khoản" luôn nhận, cấu hình gắn VA chỉ nhận giao dịch của VA đó. Giữ số cấu hình gọn (dưới 20) để dễ theo dõi log. ## GET /api/v1/client-webhooks Danh sách cấu hình của tài khoản. Cần Bearer. ```bash curl https://api.monapay.vn/api/v1/client-webhooks -H "Authorization: Bearer $MONA_TOKEN" ``` ```json { "success": true, "message": "Success", "data": [ { "id": "0190e0f1-...", "name": "Web ban hang", "webhook_url": "https://shop.vn/webhook/monapay", "auth_type": "HMAC_SHA256", "api_key_name": "X-Webhook-Secret", "payload_format": "application/json", "virtual_account_id": null, "is_active": true, "max_retries": 7, "created_at": "2026-08-28T11:00:00" } ] } ``` `secret_key` không trả về trong danh sách. `max_retries` là số lần gửi lại tối đa dành cho cơ chế gửi lại tự động đang triển khai; hiện chưa có tác dụng. ## POST /api/v1/client-webhooks Cần Bearer + `X-Client-Secret`. **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"name":"Web ban hang","webhook_url":"https://shop.vn/webhook/monapay","auth_type":"HMAC_SHA256","secret_key":"hmac-secret-dai-va-ngau-nhien","payload_format":"application/json"}' ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('MONA_TOKEN'), 'X-Client-Secret: ' . getenv('MONA_SECRET')], CURLOPT_POSTFIELDS => json_encode([ 'name' => 'Web ban hang', 'webhook_url' => 'https://shop.vn/webhook/monapay', 'auth_type' => 'HMAC_SHA256', 'secret_key' => getenv('MONA_WEBHOOK_SECRET'), // cùng secret server nhận webhook dùng để verify ]), ]); $res = json_decode(curl_exec($ch), true); curl_close($ch); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Tạo webhook thất bại'); $configId = $res['data']['id']; ``` **Node** ```js const r = await fetch('https://api.monapay.vn/api/v1/client-webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET }, body: JSON.stringify({ name: 'Web ban hang', webhook_url: 'https://shop.vn/webhook/monapay', auth_type: 'HMAC_SHA256', secret_key: process.env.MONA_WEBHOOK_SECRET, // cùng secret server nhận webhook dùng để verify }), }); const { success, data, message } = await r.json(); if (!success) throw new Error(message); console.log('config id', data.id); ``` ## PUT /api/v1/client-webhooks/{config_id} Sửa một phần, chỉ gửi trường cần đổi. Cần Bearer + `X-Client-Secret`. ```bash curl -X PUT https://api.monapay.vn/api/v1/client-webhooks/0190e0f1-... \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' -d '{"is_active":false}' ``` ## DELETE /api/v1/client-webhooks/{config_id} Xoá cấu hình. Log cũ vẫn giữ để tra cứu. ```bash curl -X DELETE https://api.monapay.vn/api/v1/client-webhooks/0190e0f1-... \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" ``` ## POST /api/v1/client-webhooks/test Bắn 1 payload giả lập tới URL bất kỳ, không cần tạo cấu hình trước. Dùng để thử endpoint và code verify HMAC. Cần Bearer + `X-Client-Secret`. | Trường | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `webhook_url` | string | có | URL cần thử | | `auth_type` | string | không, mặc định `NONE` | Thử đúng kiểu anh chị sẽ dùng | | `secret_key` | string | khi auth khác NONE | | | `api_key_name` | string | không | | | `payload_format` | string | không | | | `is_dummy` | boolean | không | Gửi payload giao dịch giả lập (nên đặt `true`) | Payload giả lập MONA Pay gửi: ```json {"amount":500000,"description":"DUMMY TRANSACTION MONAPAY","transfer_date":"11:05:00 28/08/2026","transaction_code":"DUMMY123","account_number":"1900636648","bank_name":"ACB","type":"income"} ``` Server anh chị nên nhận ra `transaction_code` = `DUMMY123` để không tạo đơn thật. ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks/test \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"webhook_url":"https://shop.vn/webhook/monapay","auth_type":"HMAC_SHA256","secret_key":"hmac-secret-dai-va-ngau-nhien","is_dummy":true}' ``` Response: `success` là `true` khi server anh chị trả 200/201/202 trong 10 giây; `false` kèm `message` mô tả lỗi (timeout, SSL, 4xx/5xx). ## GET /api/v1/webhook-logs Lịch sử từng lần gửi, mới nhất trước. Cần Bearer. | Tham số (query) | Kiểu | Ghi chú | |---|---|---| | `page` | integer ≥1 | mặc định 1 | | `limit` | integer 1-100 | mặc định 20 | | `status` | `success` / `failed` | `success` = mã 2xx | | `from_date`, `to_date` | `YYYY-MM-DD` | Lọc theo ngày tạo (UTC) | ```bash curl "https://api.monapay.vn/api/v1/webhook-logs?status=failed&from_date=2026-08-01&limit=50" \ -H "Authorization: Bearer $MONA_TOKEN" ``` ```json { "success": true, "message": "Webhook logs retrieved successfully", "data": { "items": [ { "id": "0190f0a1-...", "client_id": "0190a0f0-...", "event_type": "webhook", "endpoint_url": "https://shop.vn/webhook/monapay", "request_payload": "{\"amount\":2500000,...}", "request_headers": "{\"X-Mona-Timestamp\":\"1756350312\",\"X-Mona-Signature\":\"sha256=...\"}", "status_code": 500, "response_text": "Internal Server Error", "duration_ms": 842, "error_label": "HTTP_5XX", "created_at": "2026-08-28T10:45:13+00:00" } ], "total": 3, "page": 1, "limit": 50 } } ``` `error_label` nhận một trong: `OK`, `HTTP_3XX`, `HTTP_4XX`, `HTTP_5XX`, `TIMEOUT`, `SSL`, `DNS`, `CONNECTION`, `ERROR`. `event_type` là `webhook` cho giao dịch thật, `test` cho lần gửi thử. Ý nghĩa từng nhãn và cách sửa ở [Gửi lại và xử lý lỗi](/docs/webhooks/gui-lai-va-xu-ly-loi). ## GET /api/v1/webhook-logs/stats Thống kê theo số ngày gần nhất (tham số `days`, ví dụ 7 hoặc 30). Cần Bearer. ```bash curl "https://api.monapay.vn/api/v1/webhook-logs/stats?days=7" -H "Authorization: Bearer $MONA_TOKEN" ``` ```json { "success": true, "message": "Success", "data": { "days": 7, "total": 412, "success": 409, "failed": 3, "success_rate": 99.27, "p95_duration_ms": 610, "daily": [ { "date": "2026-08-22", "total": 58, "failed": 0 } ], "errors": [ { "label": "HTTP_5XX", "count": 2 }, { "label": "TIMEOUT", "count": 1 } ] } } ``` `p95_duration_ms` là thời gian server anh chị phản hồi ở phân vị 95; con số này vượt 5.000 là nên xem lại endpoint, vì MONA Pay cắt ở 10.000 ms. ## Lỗi thường gặp | HTTP | Nguyên nhân | Cách xử lý | |---|---|---| | 401 | Token hết hạn hoặc thiếu `X-Client-Secret` ở POST/PUT/DELETE | Login lại, thêm header | | 422 `auth_type` | Giá trị ngoài `NONE` / `API_KEY` / `HMAC_SHA256` | Viết hoa đúng | | 422 `payload_format` | Ngoài 3 giá trị cho phép | Dùng `application/json` | | `test` trả `success: false` | Server anh chị không phản hồi 2xx trong 10 giây, hoặc SSL/DNS lỗi | Đọc `message`, thử `curl` chính URL đó từ máy khác | --- # Thông báo biến động số dư qua Telegram: cấu hình bot MONA Pay > Nhận tin tiền vào ACB trong nhóm Telegram của công ty: thêm bot, lấy group_id, sửa mẫu tin với biến {{amount}}, {{transaction_content}}; API /telegram-configs. > URL: https://monapay.vn/docs/telegram · Markdown: https://monapay.vn/docs/telegram.md Kênh Telegram của MONA Pay gửi 1 tin nhắn vào nhóm Telegram của anh chị mỗi khi tài khoản ACB (hoặc VA đã chọn) có tiền vào, không cần viết dòng code nào. Thiết lập trong dashboard mục Telegram gồm 3 việc: thêm bot MONA Pay vào nhóm, dán `group_id` của nhóm (thêm `topic_id` nếu nhóm chia chủ đề), bấm "Gửi thử" để thấy tin đầu tiên. Mẫu tin sửa được bằng các biến như `{{amount}}`, `{{transaction_content}}`. Kế toán, chủ shop, nhân viên giao hàng đều thấy tiền về cùng lúc, thay cho việc một người ôm điện thoại canh app ngân hàng rồi nhắn lại cho cả nhóm. ## Thiết lập trong dashboard (không cần code) 1. **Tạo nhóm Telegram** (hoặc dùng nhóm sẵn có của công ty). Thêm bot MONA Pay vào nhóm với quyền gửi tin nhắn; tên bot hiển thị ngay trong dashboard, mục Telegram. 2. **Lấy `group_id`**: mở nhóm trên web.telegram.org, ID nhóm là dãy số trong địa chỉ (nhóm thường bắt đầu bằng dấu trừ, ví dụ `-1001234567890`); hoặc dùng một bot lấy ID bất kỳ. Nhóm bật "Chủ đề" thì lấy thêm `topic_id` (số ở cuối đường dẫn của chủ đề) để tin rơi đúng chủ đề. 3. **Tạo cấu hình** trong dashboard: đặt tên, dán `group_id` (+ `topic_id`), chọn nhận cho mọi tài khoản hay chỉ 1 VA, giữ mẫu tin mặc định hoặc sửa. 4. **Gửi thử**: nút "Gửi thử" có 2 kiểu, tin xác nhận kết nối và tin giao dịch giả lập theo đúng mẫu đã đặt. Thấy tin trong nhóm là xong. Một tài khoản tạo được nhiều cấu hình: nhóm kế toán nhận mọi giao dịch, nhóm bán hàng chỉ nhận VA của cửa hàng đó. ## Mẫu tin nhắn Mẫu mặc định (Telegram hiểu thẻ `` in đậm): ``` 💰 Biến động số dư Tên ngân hàng: {{bank_name}} STK: {{account_number}} Loại giao dịch: tiền {{vao_hay_ra}} Số tiền: {{cong_hay_tru}}{{amount}}đ Thời gian: {{transaction_date}} Nội dung: {{transaction_content}} ``` | Biến | Giá trị khi gửi | |---|---| | `{{bank_name}}` | `ACB` | | `{{account_number}}` | Số VA khớp giao dịch, hoặc số tài khoản nhận | | `{{vao_hay_ra}}` | `VÀO` với tiền vào, `RA` với tiền ra | | `{{cong_hay_tru}}` | `+` hoặc `-` | | `{{amount}}` | Số tiền định dạng có dấu chấm, ví dụ `2.500.000` | | `{{transaction_date}}` | Thời điểm giao dịch | | `{{transaction_content}}` | Nội dung chuyển khoản | | `{{accumulated}}` | Số dư sau giao dịch nếu ngân hàng gửi kèm | Mẫu gọn cho nhóm bán hàng: ``` ✅ +{{amount}}đ vào {{account_number}} {{transaction_content}} · {{transaction_date}} ``` ## API /api/v1/telegram-configs Mọi request cần `Authorization: Bearer`; POST/PUT/DELETE thêm `X-Client-Secret` (xem [Xác thực](/docs/api/xac-thuc)). | Endpoint | Việc | |---|---| | `GET /api/v1/telegram-configs` | Danh sách cấu hình | | `POST /api/v1/telegram-configs` | Tạo cấu hình | | `PUT /api/v1/telegram-configs/{config_id}` | Sửa (gửi trường cần đổi, có `is_active` để tắt tạm) | | `DELETE /api/v1/telegram-configs/{config_id}` | Xoá | | `POST /api/v1/telegram-configs/test` | Gửi thử vào nhóm | Trường khi tạo: | Trường | Kiểu | Bắt buộc | Ghi chú | |---|---|---|---| | `group_id` | string | có | ID nhóm Telegram | | `friendly_name` | string | có | Tên gợi nhớ | | `message_template` | string | có | Mẫu tin, dùng biến ở bảng trên | | `virtual_account_id` | uuid | không | Chỉ nhận giao dịch của VA này; bỏ trống = mọi tài khoản | | `topic_id` | string | không | Chủ đề trong nhóm | **cURL: tạo cấu hình** ```bash curl -X POST https://api.monapay.vn/api/v1/telegram-configs \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"group_id":"-1001234567890","friendly_name":"Nhom ke toan","message_template":"✅ +{{amount}}đ vào {{account_number}}\n{{transaction_content}} · {{transaction_date}}"}' ``` **cURL: gửi thử giao dịch giả lập** ```bash curl -X POST https://api.monapay.vn/api/v1/telegram-configs/test \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"group_id":"-1001234567890","is_dummy":true}' ``` `is_dummy: false` gửi tin "Kết nối thành công" để kiểm tra bot đã vào nhóm; `true` gửi tin giao dịch giả theo `message_template` (bỏ trống thì dùng mẫu mặc định). **PHP** ```php '-1001234567890', 'friendly_name' => 'Nhom ke toan', 'message_template' => "✅ +{{amount}}đ vào {{account_number}}\n{{transaction_content}} · {{transaction_date}}", ]; $ch = curl_init('https://api.monapay.vn/api/v1/telegram-configs'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('MONA_TOKEN'), 'X-Client-Secret: ' . getenv('MONA_SECRET')], CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE), ]); $res = json_decode(curl_exec($ch), true); curl_close($ch); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Tạo cấu hình Telegram thất bại'); ``` **Node** ```js const r = await fetch('https://api.monapay.vn/api/v1/telegram-configs', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET }, body: JSON.stringify({ group_id: '-1001234567890', friendly_name: 'Nhom ke toan', message_template: '✅ +{{amount}}đ vào {{account_number}}\n{{transaction_content}} · {{transaction_date}}', }), }); const { success, message } = await r.json(); if (!success) throw new Error(message); ``` ## Gửi lại một giao dịch Tin không tới (nhóm đổi ID, bot bị kick rồi thêm lại) thì gửi lại từ dashboard mục Giao dịch, hoặc gọi `POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry` với `{"target_type":"TELEGRAM"}` (xem [Giao dịch](/docs/api/giao-dich)). ## Lỗi thường gặp | Tình huống | Nguyên nhân | Cách xử lý | |---|---|---| | Gửi thử báo lỗi, không có tin | Bot chưa được thêm vào nhóm, hoặc `group_id` sai (thiếu dấu trừ, thiếu `100` đầu) | Thêm bot vào nhóm, copy lại ID từ web.telegram.org | | Tin rơi vào chủ đề "General" thay vì chủ đề mong muốn | Thiếu `topic_id` | Thêm `topic_id` vào cấu hình | | Có webhook nhưng không có tin Telegram | Cấu hình gắn VA khác, hoặc `is_active` đang tắt | Kiểm tra lại VA và trạng thái cấu hình | | Tin hiện thẻ `` thô | Mẫu dùng thẻ Telegram không hỗ trợ | Chỉ dùng ``, ``, `` | --- # Địa chỉ IP của MONA Pay > IP máy chủ MONA Pay gửi webhook là 103.168.55.14 (kiểm 28/08/2026). Cách whitelist trên nginx, ufw, Cloudflare và vì sao vẫn phải verify chữ ký HMAC. > URL: https://monapay.vn/docs/dia-chi-ip · Markdown: https://monapay.vn/docs/dia-chi-ip.md Webhook của MONA Pay đi ra từ địa chỉ IP `103.168.55.14` (máy chủ `api.monapay.vn`, alias `ipn.mona.host`), kiểm ngày 28/08/2026. Nếu firewall của anh chị chỉ cho một số IP gọi vào endpoint webhook, thêm địa chỉ này vào danh sách cho phép. Có thay đổi, tụi em cập nhật tại chính trang này và báo trước qua email tài khoản; nên đọc lại trang này hoặc bản [dia-chi-ip.md](/docs/dia-chi-ip.md) mỗi khi log webhook hiện nhãn `CONNECTION`. ## Danh sách IP | Mục đích | IP | Ghi chú | |---|---|---| | Gửi webhook tới server anh chị | `103.168.55.14` | IPv4, cả webhook thật lẫn gửi thử | | API `api.monapay.vn` (anh chị gọi vào) | `103.168.55.14` | Không cần whitelist chiều ra trừ khi server anh chị chặn outbound | ## Whitelist vẫn phải kèm verify chữ ký Chặn theo IP giúp giảm rác nhưng không thay được xác thực: ai đứng sau proxy hoặc giả header `X-Forwarded-For` vẫn có thể lọt nếu server anh chị tin header đó. Luôn bật `HMAC_SHA256` và kiểm `X-Mona-Signature` + `X-Mona-Timestamp` theo [Bảo mật webhook](/docs/webhooks/bao-mat). Whitelist IP là lớp thứ hai, không phải lớp duy nhất. ## Cấu hình mẫu **nginx**: chỉ cho IP MONA Pay vào đường dẫn webhook ```nginx location = /webhook/monapay { allow 103.168.55.14; deny all; proxy_pass http://127.0.0.1:3000; } ``` **ufw** (server chỉ mở webhook cho MONA Pay, nhưng cổng 443 thường phục vụ cả website nên cách này ít dùng; ưu tiên chặn ở nginx như trên): ```bash sudo ufw allow from 103.168.55.14 to any port 443 proto tcp ``` **Cloudflare WAF**: nếu website đi qua Cloudflare, tạo rule "URI Path equals /webhook/monapay AND IP Source Address is not in {103.168.55.14} → Block". Nhớ tắt các tính năng thử thách (JS challenge, Bot Fight Mode) cho đường dẫn webhook, vì MONA Pay là máy gọi, không vượt được thử thách trình duyệt; log sẽ hiện `HTTP_4XX` hoặc `HTTP_5XX` nếu quên. **PHP**: kiểm IP ở tầng ứng dụng (khi không sửa được nginx) ```php { const ip = req.ip.replace('::ffff:', ''); // app.set('trust proxy', ...) nếu sau Cloudflare/nginx if (!ALLOWED.has(ip)) return res.status(403).send('IP khong duoc phep'); next(); }); ``` ## Kiểm tra nhanh Xem địa chỉ MONA Pay đang phân giải về đâu: ```bash dig +short api.monapay.vn dig +short ipn.mona.host ``` Cả hai trả `103.168.55.14` là đúng bản hiện hành. Gửi thử bằng `POST /api/v1/client-webhooks/test` rồi đọc `request_headers`, `status_code` trong `GET /api/v1/webhook-logs` để chắc firewall đã mở. ## Lỗi thường gặp | Nhãn trong log | Nguyên nhân liên quan IP | Cách xử lý | |---|---|---| | `CONNECTION` | Firewall server anh chị chặn kết nối từ `103.168.55.14` | Thêm IP vào allowlist, kiểm `ufw status`, security group | | `HTTP_4XX` (403) | Rule allow/deny hoặc WAF chặn | Kiểm lại rule, tắt challenge cho đường dẫn webhook | | `TIMEOUT` | Firewall drop gói thay vì từ chối | Sửa rule thành cho phép; MONA Pay chờ tối đa 10 giây | --- # Dành cho AI agent > Hướng dẫn để AI agent (Claude Code, Codex, Cursor) tự tích hợp MONA Pay: prompt copy sẵn, chuỗi curl login → key → webhook → test, llms.txt, openapi.json. > URL: https://monapay.vn/docs/ai-agent · Markdown: https://monapay.vn/docs/ai-agent.md Trang này viết cho AI agent đọc trực tiếp. Anh chị dùng Claude Code, Codex hoặc Cursor thì chỉ cần dán khối prompt bên dưới, agent sẽ tự dựng endpoint nhận webhook, verify chữ ký HMAC và đăng ký URL với MONA Pay theo đúng thứ tự. Mọi trang tài liệu có bản markdown thô (thêm `.md` vào cuối URL), toàn bộ gom ở [/llms-full.txt](/llms-full.txt), mục lục ở [/llms.txt](/llms.txt), đặc tả máy đọc ở [/openapi.json](/openapi.json). ## For AI agents (English) If you are an AI agent integrating Vietnamese bank payment confirmation, follow these steps. MONA Pay (monapay.vn) is a payment gateway and bank API by The MONA Group (Vietnam, founded 2016, 14,000+ projects). Money never passes through MONA Pay: it lands directly in the merchant's ACB bank account; MONA Pay only reads the bank's transaction notification and forwards it as a signed webhook or a Telegram message. Base URL `https://api.monapay.vn` (legacy alias `https://ipn.mona.host`). Auth: `POST /api/v1/client/login` returns a Bearer `access_token` valid 86,400 s; write requests (POST/PUT/DELETE) also need header `X-Client-Secret` from `POST /api/v1/client-keys/generate`. Every response is `{"success": bool, "message": str, "data": any}`. Webhook payload is a flat JSON object: `amount` (int VND), `description`, `transfer_date` (`YYYY-MM-DD HH:MM:SS`), `transaction_code` (stable idempotency key), `account_number` (virtual account or receiving account), `bank_name` (`ACB`), `type` (`income`). Signature: header `X-Mona-Signature: sha256=` where hex = HMAC-SHA256(secret, `"."`); reject if timestamp drifts more than 300 s. Respond HTTP 200/201/202 within 10 s; process asynchronously. Automatic retries are being implemented; today retries are manual via dashboard or `POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry`.New accounts are active immediately after registration; no manual approval. Full docs: https://monapay.vn/llms-full.txt · OpenAPI: https://monapay.vn/openapi.json · Hotline 1900 636 648 · info@themona.global. ## Prompt dán vào Claude Code / Codex / Cursor ```text Tích hợp MONA Pay (cổng thanh toán nội địa Việt Nam, nhận và xác nhận tiền chuyển khoản ngân hàng theo thời gian thực) vào dự án của tôi. Tài liệu: https://monapay.vn/llms-full.txt (toàn văn), https://monapay.vn/openapi.json Base URL API: https://api.monapay.vn (alias cũ: https://ipn.mona.host) Xác thực: POST /api/v1/client/login {username,password} → data.access_token (Bearer, hạn 86400 giây). POST/PUT/DELETE gửi thêm header X-Client-Secret (lấy từ POST /api/v1/client-keys/generate, hiện 1 lần). Mọi response: {"success": bool, "message": str, "data": any}. Việc cần làm: 1. Tạo endpoint HTTPS POST /webhook/monapay trong dự án để nhận thông báo tiền vào. MONA Pay POST JSON: {"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"MONA0000010234","bank_name":"ACB","type":"income"} 2. Verify chữ ký: header X-Mona-Signature = "sha256=" + hex(HMAC-SHA256(secret, X-Mona-Timestamp + "." + raw_body)). Từ chối nếu |now - X-Mona-Timestamp| > 300 giây. So sánh chữ ký bằng hàm timing-safe. Đọc raw body, không parse trước khi ký. 3. Chống trùng bằng transaction_code (UNIQUE). Bỏ qua transaction_code = "DUMMY123" (payload gửi thử). 4. Trả HTTP 200 ngay trong 10 giây, xử lý đơn hàng bất đồng bộ. Khớp đơn theo account_number (số VA) hoặc mã đơn trong description, và so amount với số tiền đơn. 5. Đăng ký URL webhook: POST /api/v1/client-webhooks {name, webhook_url, auth_type:"HMAC_SHA256", secret_key} (Bearer + X-Client-Secret), rồi gửi thử POST /api/v1/client-webhooks/test {webhook_url, auth_type, secret_key, is_dummy:true}. Secret HMAC và client_secret đọc từ biến môi trường MONA_WEBHOOK_SECRET, MONA_SECRET; không ghi cứng. ``` ## Chuỗi curl từ đầu đến cuối Thay `shopabc`, mật khẩu và URL bằng của anh chị. Tài khoản đăng ký xong dùng ngay, không cần ai duyệt. ```bash BASE=https://api.monapay.vn # 1. Đăng nhập lấy Bearer token (hạn 24 giờ) TOKEN=$(curl -s -X POST $BASE/api/v1/client/login \ -H 'Content-Type: application/json' \ -d '{"username":"shopabc","password":"MatKhauManh#2026"}' | jq -r .data.access_token) # 2. Sinh client_secret (chỉ hiện 1 lần, ghi vào .env ngay) SECRET=$(curl -s -X POST $BASE/api/v1/client-keys/generate \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"name":"agent-integration"}' | jq -r .data.client_secret) # 3. Secret HMAC do anh chị tự sinh, dùng chung giữa MONA Pay (ký) và server anh chị (verify) WEBHOOK_SECRET=$(openssl rand -hex 32) # 4. Đăng ký URL webhook với HMAC-SHA256 curl -s -X POST $BASE/api/v1/client-webhooks \ -H "Authorization: Bearer $TOKEN" -H "X-Client-Secret: $SECRET" -H 'Content-Type: application/json' \ -d "{\"name\":\"Web ban hang\",\"webhook_url\":\"https://shop.vn/webhook/monapay\",\"auth_type\":\"HMAC_SHA256\",\"secret_key\":\"$WEBHOOK_SECRET\",\"payload_format\":\"application/json\"}" # 5. Gửi thử payload giả lập (transaction_code = DUMMY123) curl -s -X POST $BASE/api/v1/client-webhooks/test \ -H "Authorization: Bearer $TOKEN" -H "X-Client-Secret: $SECRET" -H 'Content-Type: application/json' \ -d "{\"webhook_url\":\"https://shop.vn/webhook/monapay\",\"auth_type\":\"HMAC_SHA256\",\"secret_key\":\"$WEBHOOK_SECRET\",\"is_dummy\":true}" # 6. Đọc log lần gửi thử: status_code, duration_ms, error_label curl -s "$BASE/api/v1/webhook-logs?limit=5" -H "Authorization: Bearer $TOKEN" | jq '.data.items[0]' ``` Giả lập MONA Pay bắn vào máy local (không cần tài khoản) để thử code verify: ```bash SECRET='secret_hmac_test' URL='http://localhost:3000/webhook/monapay' TS=$(date +%s) BODY='{"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"MONA0000010234","bank_name":"ACB","type":"income"}' SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST "$URL" -H 'Content-Type: application/json' \ -H "X-Mona-Timestamp: $TS" -H "X-Mona-Signature: sha256=$SIG" --data "$BODY" ``` ## Endpoint nhận webhook tối thiểu **PHP** ```php 300) { http_response_code(400); exit; } if (!hash_equals('sha256=' . hash_hmac('sha256', "$ts.$raw", $secret), $sig)) { http_response_code(401); exit; } http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); $d = json_decode($raw, true); if ($d['transaction_code'] === 'DUMMY123') exit; // INSERT IGNORE theo UNIQUE(transaction_code), rồi khớp đơn theo account_number / description, so amount ``` **Node (Express)** ```js app.post('/webhook/monapay', express.raw({ type: 'application/json' }), (req, res) => { const ts = req.header('X-Mona-Timestamp') || '', sig = req.header('X-Mona-Signature') || ''; const raw = req.body.toString('utf8'); if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400); const exp = 'sha256=' + crypto.createHmac('sha256', process.env.MONA_WEBHOOK_SECRET).update(`${ts}.${raw}`).digest('hex'); if (sig.length !== exp.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(exp))) return res.sendStatus(401); res.sendStatus(200); const d = JSON.parse(raw); if (d.transaction_code === 'DUMMY123') return; queue.add('monapay', d); // upsert theo transaction_code, khớp đơn, so amount }); ``` ## Bản đồ tài liệu cho agent | Cần gì | Đọc | |---|---| | Toàn bộ trong 1 file | [/llms-full.txt](/llms-full.txt) | | Mục lục ngắn | [/llms.txt](/llms.txt) | | Đặc tả endpoint máy đọc | [/openapi.json](/openapi.json) | | Xác thực, envelope | [/docs/api/xac-thuc.md](/docs/api/xac-thuc.md) | | Payload webhook, chữ ký | [/docs/webhooks/dinh-dang-payload.md](/docs/webhooks/dinh-dang-payload.md), [/docs/webhooks/bao-mat.md](/docs/webhooks/bao-mat.md) | | Tạo VA, QR | [/docs/api/tai-khoan-ao-va.md](/docs/api/tai-khoan-ao-va.md), [/docs/api/qr-thanh-toan.md](/docs/api/qr-thanh-toan.md) | | Đối soát, gửi lại | [/docs/api/giao-dich.md](/docs/api/giao-dich.md), [/docs/webhooks/gui-lai-va-xu-ly-loi.md](/docs/webhooks/gui-lai-va-xu-ly-loi.md) | | So với cổng quốc tế (PayPal, Stripe) | [/cong-thanh-toan-quoc-te](/cong-thanh-toan-quoc-te) | | IP gửi webhook | [/docs/dia-chi-ip.md](/docs/dia-chi-ip.md) | ## Những điều agent hay hiểu sai - Không có `refresh_token`; hết 86.400 giây thì đăng nhập lại. - `X-Client-Secret` khác `secret_key` của webhook: cái đầu để gọi API MONA Pay, cái sau để MONA Pay ký payload gửi sang anh chị. - Ký trên raw body đúng byte MONA Pay gửi (JSON không khoảng trắng). Framework nào parse JSON trước rồi serialize lại sẽ sai chữ ký. - `type` hiện chỉ có `income`; đừng viết nhánh xử lý tiền ra như đã có. - Payload gửi thử có `transaction_code` = `DUMMY123`, `amount` = 500000, `account_number` = `1900636648`; không tạo đơn từ payload này. - Tài khoản mới đăng nhập được ngay sau khi đăng ký, không cần MONA duyệt. Login trả 401 nghĩa là sai username/mật khẩu, không phải do chưa kích hoạt. --- # ENGLISH DOCUMENTATION (same content in English) --- # MONA Pay webhook and bank API documentation > MONA Pay reports incoming ACB transfers in real time through webhooks, Telegram and an API. How it works, what each feature does, and where to read next. > URL: https://monapay.vn/en/docs · Markdown: https://monapay.vn/en/docs.md MONA Pay reads the transaction notification ACB sends the moment money lands in your account, then POSTs a webhook to your server or sends a message to your Telegram group. Money never passes through MONA Pay; it goes straight into your bank account as usual. A basic integration takes about 5 minutes if you already have an ACB account and a URL that can receive webhooks. > MONA Pay is the payment gateway and bank API of The MONA Group that lets Vietnamese businesses receive and confirm bank transfers in real time via virtual accounts (VA), VietQR, webhooks and Telegram — built so both developers and AI agents can integrate in minutes. ## How MONA Pay works The whole flow has 4 steps. Nothing changes on the bank side except registering for transaction notifications (done once, with an OTP from ACB). ```text [1] Customer transfers [2] ACB notifies [3] MONA Pay records [4] Your system into an ACB account ───▶ MONA Pay ───▶ and sends the event ───▶ confirms the order (VA or VietQR scan) (real time) webhook / Telegram (nobody watches the app) ``` 1. **The customer transfers.** They pay into the virtual account (VA) attached to the order, or scan a VietQR code that already carries the amount and reference. 2. **ACB notifies MONA Pay.** As soon as the money arrives, ACB pushes the transaction to MONA Pay. This is an official connection you register once from the dashboard with an OTP sent to your phone. 3. **MONA Pay records and forwards.** The transaction appears on the dashboard, then MONA Pay POSTs a JSON payload to your webhook URL (signed with HMAC-SHA256 if enabled) and sends a Telegram message if configured. 4. **Your system handles it.** Your store, back-office software or even a Google Sheet receives the webhook and flips the order to "paid". Nobody opens a banking app to match transfers by hand. Your server only has to answer HTTP 200, 201 or 202 within 10 seconds for MONA Pay to count the delivery as successful. Every attempt is logged so you can inspect it later. ## Features and where they are documented | Feature | When to use it | Documentation | |---|---|---| | Virtual accounts (VA) | Give each order or customer its own account number so payments match automatically | [Virtual accounts (VA)](/en/docs/api/tai-khoan-ao-va) | | Dynamic VietQR | Generate a QR code with the amount and reference pre-filled; the customer scans and pays exactly | [QR payments](/en/docs/api/qr-thanh-toan) | | Webhooks | Your server receives incoming payments in real time | [Webhook integration](/en/docs/webhooks/tich-hop-webhook) | | HMAC signature | Verify a webhook really came from MONA Pay, block forgeries and replays | [Webhook security](/en/docs/webhooks/bao-mat) | | Retries and logs | See every delivery attempt, error labels, resend by hand | [Retries and error handling](/en/docs/webhooks/gui-lai-va-xu-ly-loi) | | Reconciliation | Pull the transaction list page by page to compare with your books | [Reconciliation](/en/docs/webhooks/doi-soat) | | Telegram | Accountants and shop owners get an instant message on their phone | [Telegram](/en/docs/telegram) | | API keys | Create and revoke keys for server-to-server calls | [API keys](/en/docs/api/api-keys) | | API authentication | Log in for a Bearer token, send X-Client-Secret | [Authentication](/en/docs/api/xac-thuc) | | IP addresses | Open your firewall for the MONA Pay webhook server | [IP addresses](/en/docs/dia-chi-ip) | | AI agents | A prompt and checklist so Claude Code, Codex or Cursor can integrate on their own | [For AI agents](/en/docs/ai-agent) | ## Where to start - No account yet: read [Quick start (5 minutes)](/en/docs/bat-dau-nhanh). It walks from sign-up to your first webhook. - New to VA, VietQR, webhooks or HMAC: read [Concepts](/en/docs/khai-niem) first; it is written for non-technical readers too. - You are an AI agent, or you want to hand the job to one: open [For AI agents](/en/docs/ai-agent), or load `https://monapay.vn/llms-full.txt` directly. ## Machine-readable docs Every docs page has a raw markdown version: append `.md` to the URL (for example `https://monapay.vn/en/docs/webhooks/tich-hop-webhook.md`). There is also `https://monapay.vn/llms.txt` (index), `https://monapay.vn/llms-full.txt` (full text) and `https://monapay.vn/openapi.json` (API v1 spec). Loading any of them gives an agent enough context to write the integration. ## System facts | Item | Value | |---|---| | API base URL | `https://api.monapay.vn` (legacy alias `https://ipn.mona.host` still works) | | Dashboard | `https://my.monapay.vn` | | Supported banks | ACB is live; MB, BIDV, VietinBank, OCB, MSB, KienlongBank and TPBank are in the partner-registration process. Status table at [/ngan-hang](/ngan-hang) | | Successful webhook | HTTP 200, 201 or 202 within 10 seconds | | Webhook signature | HMAC-SHA256, headers `X-Mona-Signature` and `X-Mona-Timestamp`, 5-minute replay window | | Webhook source IP | `103.168.55.14` (checked 28/08/2026) | | Support | Hotline 1900 636 648, email info@themona.global | MONA Pay is a product of The MONA Group, founded in 2016, with 14,000+ web and software projects delivered. We built this system to collect our own payments first; since 2022 more than 6,000 new MONA customers have collected payments through it inside the websites and software MONA delivered. In 2026 it opened to every business. ## Quick answers **Does money pass through MONA Pay?** No. Money goes straight into your ACB account. MONA Pay only receives the bank's transaction notification and forwards it to you. **Do I need to code?** Not necessarily. If you only need to know when money arrives, turn on Telegram notifications in the dashboard. Webhooks and the API are for stores and software that confirm orders automatically. **Can a new account be used right away?** Yes. Sign up, log in immediately, create your own API key; nobody has to approve you. Only the ACB linking step needs an OTP sent to the phone number registered with the bank. MONA Pay is completely free with no transaction limit (see [pricing](/bang-gia)). **Are banks other than ACB supported?** ACB is live; MB, BIDV, VietinBank, OCB, MSB, KienlongBank and TPBank are in the partner-registration process, with status updated at [supported banks](/ngan-hang). Webhooks, Telegram and the API are shared across banks and the payload carries `bank_name`, so adding a bank does not change your integration. --- # Quick start with MONA Pay in 5 minutes > From sign-up to your first webhook. 6 steps with cURL, PHP and Node samples you can paste and run. > URL: https://monapay.vn/en/docs/bat-dau-nhanh · Markdown: https://monapay.vn/en/docs/bat-dau-nhanh.md To receive real-time notifications for money arriving in an ACB account you need 6 steps: sign up (usable immediately, no approval), log in for a token, create an API key, link your ACB account with an OTP, register a webhook URL, then send a small transfer to check. If you already have an ACB account and a server that can receive webhooks, the hands-on part takes about 5 minutes. ## Before you start - An ACB bank account in your name or your company's name, plus the phone number registered with ACB (to receive the OTP). - An HTTPS URL on your server to receive webhooks, for example `https://your-domain.com/webhook/monapay`. No server yet? Telegram notifications still work, see [Telegram](/en/docs/telegram). - A way to call the API: cURL, Postman, or let an AI agent do it with the [ready-made prompt](/en/docs/ai-agent). ## Step 1. Sign up Go to `https://my.monapay.vn/auth`, open the Sign up tab and fill in a username, password, name and email. You can also call the API: ```bash curl -X POST https://api.monapay.vn/api/v1/client/register-client \ -H 'Content-Type: application/json' \ -d '{"username":"my-shop","password":"a-strong-password","name":"My Shop"}' ``` The account is usable immediately: log in at step 2, nobody has to approve it. MONA Pay is completely free with no transaction limit (see [pricing](/bang-gia)). ## Step 2. Log in for a token ```bash curl -X POST https://api.monapay.vn/api/v1/client/login \ -H 'Content-Type: application/json' \ -d '{"username":"my-shop","password":"a-strong-password"}' ``` Every API returns the same envelope: ```json { "success": true, "message": "Login successful", "data": { "access_token": "eyJhbGciOi...", "expires_in": 86400 } } ``` From here every call carries `Authorization: Bearer `. Details in [Authentication](/en/docs/api/xac-thuc). ## Step 3. Create an API key POST, PUT and DELETE requests need an extra `X-Client-Secret` header. Create a key in the dashboard under API Keys, or: ```bash curl -X POST https://api.monapay.vn/api/v1/client-keys/generate \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"production-server"}' ``` `client_secret` is shown exactly once in the response. Store it in an environment variable right away; if you lose it, create a new key and revoke the old one. See [API keys](/en/docs/api/api-keys). ## Step 4. Link your ACB account (4 steps, with OTP) In the dashboard, open Banks & VA and click Add account: 1. Enter the ACB account number, the phone number registered with ACB, and the customer type (personal or business). 2. ACB sends an OTP to the phone. Enter it to verify and create your first virtual account (VA). 3. Register for transaction notifications. ACB sends a second OTP; enter it too. 4. Done. From now on, money arriving in the VA or the account reaches MONA Pay as a notification. To do this through the API instead of the dashboard, see [Virtual accounts (VA)](/en/docs/api/tai-khoan-ao-va); the flow is the same 4 calls. ## Step 5. Register your webhook URL In the dashboard open Webhooks, click Add webhook, enter the URL, pick HMAC_SHA256 as the auth type and set a secret. Or call the API: ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks \ -H "Authorization: Bearer $TOKEN" \ -H "X-Client-Secret: $CLIENT_SECRET" \ -H 'Content-Type: application/json' \ -d '{ "name": "Online store", "webhook_url": "https://your-domain.com/webhook/monapay", "auth_type": "HMAC_SHA256", "secret_key": "your_hmac_secret", "payload_format": "application/json" }' ``` Without `virtual_account_id` the webhook receives every transaction on every account. Pass the id of one VA to receive only that VA's transactions. On your side the endpoint does 3 things: verify the signature, answer HTTP 200 immediately, then process the order. Paste-ready samples: ```php " // 1. Block replays: reject if the timestamp is more than 5 minutes off if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit('timestamp expired'); } // 2. Verify the signature: HMAC-SHA256(secret, ".") $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret); if (!hash_equals($expected, $signature)) { // hash_equals prevents timing attacks http_response_code(401); exit('bad signature'); } // 3. Answer 200 right away, push heavy work to a queue/cron http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); $data = json_decode($raw, true); // $data['amount'], $data['description'], $data['transaction_code'], ... // TODO: record the transaction and update your order here ``` ```js // webhook-monapay.js: receive MONA Pay webhooks with Express const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.MONA_WEBHOOK_SECRET; // the HMAC secret you set when enabling HMAC // Read the raw body or the signature will not match (do not use express.json() on this route) app.post('/webhook/monapay', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.header('X-Mona-Timestamp') || ''; const signature = req.header('X-Mona-Signature') || ''; // "sha256=" const rawBody = req.body.toString('utf8'); // 1. Block replays: reject if the timestamp is more than 5 minutes off if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.status(400).send('timestamp expired'); } // 2. Verify the signature: HMAC-SHA256(secret, ".") const expected = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(timestamp + '.' + rawBody) .digest('hex'); const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!valid) return res.status(401).send('bad signature'); // 3. Answer 200 right away, process the order asynchronously res.status(200).send('OK'); const data = JSON.parse(rawBody); // data.amount, data.description, data.transaction_code, ... // TODO: record the transaction and update your order here }); app.listen(3000, () => console.log('MONA Pay webhook listening on port 3000')); ``` ## Step 6. Send a test, then a real transfer First click Send test in the dashboard (Webhooks) or call `POST /api/v1/client-webhooks/test`. MONA Pay fires a sample payload at your URL; the result shows up immediately in Delivery history with the HTTP code and response time. To test locally without waiting for MONA Pay, replay exactly what MONA Pay sends with cURL: ```bash # Simulate MONA Pay firing a webhook at your endpoint for a local test SECRET='your_hmac_secret' # the HMAC secret you set in the dashboard URL='https://your-domain.com/webhook/monapay' TS=$(date +%s) BODY='{"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"1234567890","bank_name":"ACB","type":"income"}' # Signature = HMAC-SHA256(secret, "."); printf avoids a trailing newline SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST "$URL" \ -H 'Content-Type: application/json' \ -H "X-Mona-Timestamp: $TS" \ -H "X-Mona-Signature: sha256=$SIG" \ --data "$BODY" ``` Finally transfer a small amount, say 10,000 VND, into the VA you just created from any banking app. Within seconds the transaction appears on the dashboard and the webhook reaches your server. ## After it works - Read [Payload format](/en/docs/webhooks/dinh-dang-payload) to learn every field and use `transaction_code` as your deduplication key. - Read [Retries and error handling](/en/docs/webhooks/gui-lai-va-xu-ly-loi) to understand logs, error labels and resending. - Whitelist IP `103.168.55.14` if your server blocks unknown connections, see [IP addresses](/en/docs/dia-chi-ip). - Want a QR code with the amount pre-filled for each order: [QR payments](/en/docs/api/qr-thanh-toan). ## Common problems **Login says wrong credentials although the password is right.** Check the username (case-sensitive) and the password. New accounts can log in immediately; there is no activation queue. Still stuck: call 1900 636 648. **POST is rejected even with a valid Bearer token.** The `X-Client-Secret` header is missing. Send it on every POST, PUT and DELETE (GET does not need it); server-side enforcement is being rolled out, so sending it now means nothing to change later. **Test delivery shows TIMEOUT.** Your server did not answer within 10 seconds. Return HTTP 200 first and process the order afterwards, as in the samples above. **Signature mismatch.** Usually the framework parsed the JSON and re-serialised it before signing. Sign the raw body exactly as MONA Pay sent it, byte for byte; see [Webhook security](/en/docs/webhooks/bao-mat). **Transferred money but no transaction shows up.** Check that step 4 completed both OTPs. Without the second OTP (notification registration) ACB does not push transactions to MONA Pay. --- # What is a webhook, VietQR and a virtual account (plain-language guide) > The 7 terms you meet in MONA Pay explained for non-technical readers, with a short developer note in each section. > URL: https://monapay.vn/en/docs/khai-niem · Markdown: https://monapay.vn/en/docs/khai-niem.md Using MONA Pay you will run into 7 terms: virtual account (VA), VietQR, webhook, Telegram, HMAC, API key and reconciliation. This page explains each one the way a shop owner or accountant would read it, with a few lines for developers in every section. After reading it you know enough to configure the dashboard without asking a developer. ## Virtual account (VA) **For users.** A virtual account is a secondary account number issued by ACB; money sent to it still lands in your main account. The useful part is that you can create many different virtual numbers, each attached to one order or one customer. Whichever number the customer pays into tells the system which order it belongs to, without the customer having to type the right transfer note. For example, order 1052 gets VA `MONA1052...`; when the customer pays into it, order 1052 flips to paid on its own. **For developers.** VAs are created under the prefix you registered with ACB, managed in the dashboard under Banks & VA or through `POST /api/v1/acb/virtual-account/registration`. Webhooks can be configured per VA, so you can route tuition, hosting fees and one-off orders to different endpoints. Details in [Virtual accounts (VA)](/en/docs/api/tai-khoan-ao-va). ## VietQR **For users.** VietQR is the standard bank-transfer QR code in Vietnam; every banking app can scan it. MONA Pay generates "dynamic" codes: the account number, amount and reference are already inside the code. When the customer scans it, the transfer screen is pre-filled and they only confirm, so nothing can be mistyped. An order of 350,000 VND produces a QR that says exactly 350,000 VND. **For developers.** Call `POST /api/v1/acb/qr-payment/generate` with `orderId`, `amount` (integer VND, up to 1,000,000,000), `description` (up to 255 characters) and the account details. When the customer scans and pays, ACB notifies MONA Pay and the webhook fires exactly like a VA transaction. Cancel a code with `DELETE /api/v1/acb/qr-payment/{qr_code_id}/cancellation`. See [QR payments](/en/docs/api/qr-thanh-toan). ## Webhook **For users.** A webhook is how MONA Pay "calls" your software every time money arrives. You give MONA Pay a web address (URL); for each transaction MONA Pay sends a package of information there: amount, reference, time, transaction code. Your software then acts on it, for example updating the order status, emailing the customer, or unlocking a course. **For developers.** MONA Pay POSTs JSON to your URL, `Content-Type: application/json` by default. Your endpoint returns HTTP 200, 201 or 202 within 10 seconds to count as delivered. The payload has 7 fields; the deduplication key is `transaction_code`. See [Webhook integration](/en/docs/webhooks/tich-hop-webhook) and [Payload format](/en/docs/webhooks/dinh-dang-payload). ## Telegram **For users.** If you only need to know that money arrived and no software has to react, Telegram notifications are enough. Add the MONA Pay bot to your company's Telegram group; every incoming transfer posts one message: bank, account number, amount, time, reference. The accountant sees it wherever they are without logging into a banking app. **For developers.** Configure it in the dashboard under Telegram: enter the `group_id` (and `topic_id` if the group uses topics), choose one VA or all accounts, edit the message template, and use the test button. The API lives at `/api/v1/telegram-configs`. See [Telegram](/en/docs/telegram). ## HMAC (webhook signature) **For users.** A webhook URL is public; in theory anyone who knows it could send a fake "money arrived" package. HMAC is the anti-forgery signature: you and MONA Pay share a secret, and every package MONA Pay sends carries a signature computed from that secret. Your software recomputes the signature; if it matches, trust the package, otherwise drop it. An attacker without the secret cannot forge it. **For developers.** Header `X-Mona-Signature: sha256=` where hex = HMAC-SHA256(secret, `"."`). The timestamp is unix seconds; reject anything more than 5 minutes off to block replays. Sign the raw body byte for byte. See [Webhook security](/en/docs/webhooks/bao-mat). ## API key and Bearer token **For users.** An API key is the key your software uses to call into MONA Pay (create VAs, create QR codes, list transactions). Create keys in the dashboard under API Keys, name each one so you know where it is used, and revoke a leaked key without touching the others. **For developers.** Two layers: log in with `POST /api/v1/client/login` for an `access_token` sent as a Bearer on every request; POST, PUT and DELETE requests add the `X-Client-Secret` header, which is the `client_secret` generated by `POST /api/v1/client-keys/generate`. The `client_secret` is shown only once. Every response uses the envelope `{"success": true, "message": "...", "data": {...}}`. See [Authentication](/en/docs/api/xac-thuc) and [API keys](/en/docs/api/api-keys). ## Reconciliation **For users.** Reconciliation means comparing your books with the bank's transaction list to make sure nothing was missed. Webhooks cover real time, but your server can be under maintenance or offline right when money arrives. Periodic reconciliation (hourly or end of day) is the safety net for those moments. **For developers.** Pull the list with `GET /api/v1/acb/virtual-account/transactions?virtual_account_number=...&page=1&limit=100` (at most 100 per page), compare `transaction_code` with your transactions table and insert what is missing. See [Reconciliation](/en/docs/webhooks/doi-soat). ## Putting it together A typical online store works like this: each order gets a dynamic VietQR code (or a VA), the customer scans and pays. ACB notifies MONA Pay, MONA Pay fires an HMAC-signed webhook at the store, the store verifies the signature and marks the order paid. The accountants' Telegram group gets the message at the same time. At the end of the day a cron job calls the reconciliation API to make sure nothing slipped. Nobody in that chain opens a banking app. ## Quick answers **Is money ever held by MONA Pay?** No. A VA is a secondary number on your own ACB account, so money is in your bank account the moment it arrives. MONA Pay only receives the notification. **Do webhooks work without a VA?** Yes. Once the ACB account is linked and notification registration is done (2 OTPs), every transaction on the account is reported. VAs only make order matching automatic. **VA or VietQR?** Use both. Dynamic VietQR is the most convenient way for customers to pay, and the VA is how the system tells which order the money belongs to. MONA Pay's dynamic QR already carries the amount and reference, so matching is reliable. **Is HMAC mandatory?** Not mandatory, but we recommend enabling it from day one. It costs about 10 lines of code on the receiving side and makes incoming-payment notifications impossible to forge. --- # Payment webhook integration: receive incoming ACB transfers in 5 minutes > How to register a webhook URL, write the endpoint that receives incoming ACB transfers, the 200/201/202-within-10-seconds success rule, and deduplication by transaction_code. > URL: https://monapay.vn/en/docs/webhooks/tich-hop-webhook · Markdown: https://monapay.vn/en/docs/webhooks/tich-hop-webhook.md A MONA Pay webhook is an HTTP POST sent to your URL every time money arrives in a linked ACB account. To integrate, register the URL in the dashboard or through the API, write an endpoint that receives JSON, verify the signature, answer HTTP 200 within 10 seconds, then process the order. It is about 30 lines of code; the PHP and Node samples below are paste-ready. ## Life of a webhook 1. Money arrives in the ACB account (through a VA, a VietQR code or a plain transfer). 2. ACB notifies MONA Pay; MONA Pay stores the transaction on the dashboard. 3. MONA Pay finds matching webhook configurations: "all accounts" configurations always fire; a configuration bound to one VA fires only when the transaction belongs to that VA. 4. For each configuration, MONA Pay POSTs the payload to the URL, signed if HMAC is enabled, and waits at most 10 seconds. 5. Your server answers HTTP 200, 201 or 202: logged as success. Any other code, or more than 10 seconds: logged as failed with an error label; you can resend from the dashboard. ## Registering the webhook URL ### In the dashboard Open Webhooks, click Add webhook and fill in: | Field | Meaning | |---|---| | Name | Something memorable, e.g. "Online store", "Tuition software" | | URL | The HTTPS address on your server that receives the webhook | | Auth type | `HMAC_SHA256` (recommended), `API_KEY` or `NONE` | | Secret | The secret used to sign (HMAC) or sent in a header (API key) | | Payload format | `application/json` (default), `application/x-www-form-urlencoded`, `multipart/form-data` | | Accounts | All accounts, or one specific VA | ### Through the API ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks \ -H "Authorization: Bearer $TOKEN" \ -H "X-Client-Secret: $CLIENT_SECRET" \ -H 'Content-Type: application/json' \ -d '{ "name": "Online store", "webhook_url": "https://your-domain.com/webhook/monapay", "auth_type": "HMAC_SHA256", "secret_key": "your_hmac_secret", "payload_format": "application/json", "virtual_account_id": null }' ``` Related calls: `GET /api/v1/client-webhooks` lists configurations, `PUT /api/v1/client-webhooks/{config_id}` updates, `DELETE /api/v1/client-webhooks/{config_id}` removes, `POST /api/v1/client-webhooks/test` sends a sample payload. Parameters are documented in [Webhook configuration API](/en/docs/api/webhook-configs). ## The payload MONA Pay sends ```json { "amount": 2500000, "description": "noi dung chuyen khoan", "transfer_date": "10:30:00 28/08/2026", "transaction_code": "FT26240001234", "account_number": "1234567890", "bank_name": "ACB", "type": "income" } ``` `transaction_code` stays the same across every resend; use it as your deduplication key. `account_number` is the VA number or the receiving account number. Each field is explained in [Payload format](/en/docs/webhooks/dinh-dang-payload). ## Writing the receiving endpoint A good endpoint does exactly 3 things in order: verify the signature, answer 200 immediately, process afterwards. Heavy work (updating orders, sending emails, calling other APIs) goes to a queue or runs after the response, so you never hit the 10-second limit. ```bash # Simulate MONA Pay firing a webhook at your endpoint for a local test SECRET='your_hmac_secret' # the HMAC secret you set in the dashboard URL='https://your-domain.com/webhook/monapay' TS=$(date +%s) BODY='{"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"1234567890","bank_name":"ACB","type":"income"}' # Signature = HMAC-SHA256(secret, "."); printf avoids a trailing newline SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST "$URL" \ -H 'Content-Type: application/json' \ -H "X-Mona-Timestamp: $TS" \ -H "X-Mona-Signature: sha256=$SIG" \ --data "$BODY" ``` ```php " // 1. Block replays: reject if the timestamp is more than 5 minutes off if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit('timestamp expired'); } // 2. Verify the signature: HMAC-SHA256(secret, ".") $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret); if (!hash_equals($expected, $signature)) { // hash_equals prevents timing attacks http_response_code(401); exit('bad signature'); } // 3. Answer 200 right away, push heavy work to a queue/cron http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); $data = json_decode($raw, true); // 4. Deduplicate: transaction_code is the unique key $pdo = new PDO(getenv('DB_DSN'), getenv('DB_USER'), getenv('DB_PASS')); $stmt = $pdo->prepare( 'INSERT IGNORE INTO transactions (transaction_code, amount, description, transfer_date, account_number) VALUES (?, ?, ?, ?, ?)' ); $stmt->execute([$data['transaction_code'], $data['amount'], $data['description'], $data['transfer_date'], $data['account_number']]); if ($stmt->rowCount() === 0) exit; // already processed, this is a resend // TODO: match the order by account_number (VA) or description, update the order status ``` ```js // webhook-monapay.js: receive MONA Pay webhooks with Express const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.MONA_WEBHOOK_SECRET; // the HMAC secret you set when enabling HMAC // Read the raw body or the signature will not match (do not use express.json() on this route) app.post('/webhook/monapay', express.raw({ type: 'application/json' }), async (req, res) => { const timestamp = req.header('X-Mona-Timestamp') || ''; const signature = req.header('X-Mona-Signature') || ''; // "sha256=" const rawBody = req.body.toString('utf8'); // 1. Block replays: reject if the timestamp is more than 5 minutes off if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.status(400).send('timestamp expired'); } // 2. Verify the signature: HMAC-SHA256(secret, ".") const expected = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(timestamp + '.' + rawBody) .digest('hex'); const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!valid) return res.status(401).send('bad signature'); // 3. Answer 200 right away, process the order asynchronously res.status(200).send('OK'); const data = JSON.parse(rawBody); // 4. Deduplicate: transaction_code is the unique key (e.g. a table with UNIQUE(transaction_code)) // const inserted = await db.insertIgnore('transactions', { transaction_code: data.transaction_code, ... }); // if (!inserted) return; // resend, already processed // TODO: match the order by data.account_number (VA) or data.description, update the order status }); app.listen(3000, () => console.log('MONA Pay webhook listening on port 3000')); ``` ## Deduplicating transactions The same transaction can reach your endpoint more than once: a manual resend from the dashboard, two webhook configurations pointing at the same URL, or your server erroring and the delivery being retried. The standard fix is a `transaction_code` column with a UNIQUE constraint in your transactions table, inserted with `INSERT IGNORE` (MySQL) or `ON CONFLICT DO NOTHING` (PostgreSQL). Anything that fails to insert was already processed; skip it. Do not rely on amount + time: two customers paying the same amount in the same minute is normal. ## Pre-production checklist - The endpoint runs over HTTPS with a valid certificate. Self-signed or expired certificates produce the `SSL` error label. - The firewall allows IP `103.168.55.14` (see [IP addresses](/en/docs/dia-chi-ip)). - No redirect on the webhook URL (for example `http` to `https`, or adding a trailing `/`). Redirects produce the `HTTP_3XX` label and count as failed. - Click Send test in the dashboard; Delivery history should show 200 with a response time under 1 second. - Transfer a small amount into a VA to run the real flow. ## Common problems **The webhook arrives but the signature does not match.** The framework (Laravel, Express, Next.js) parsed the JSON and used the re-serialised body for signing. Use the untouched raw body. Per-framework instructions are in [Webhook security](/en/docs/webhooks/bao-mat). **TIMEOUT label although the endpoint works.** The endpoint finishes processing the order before answering and exceeds 10 seconds under load. Answer 200 first, process afterwards. **HTTP_4XX with 404 or 405.** Wrong path, or the route only accepts GET. Webhooks are always POST. **The same transaction arrives twice.** Normal. Deduplicate with `transaction_code` as described above. **Nothing arrives although the money is in.** The webhook configuration is bound to one VA but the customer paid into the main account or another VA. Add an "all accounts" configuration so nothing is missed, or check Delivery history in the dashboard to see whether MONA Pay sent anything. --- # MONA Pay webhook payload format > The 7 fields in the JSON MONA Pay sends when money arrives, the 3 Content-Type options, accompanying headers, the test payload and the compatible format in progress. > URL: https://monapay.vn/en/docs/webhooks/dinh-dang-payload · Markdown: https://monapay.vn/en/docs/webhooks/dinh-dang-payload.md Every MONA Pay webhook is a 7-field JSON payload: amount, reference, time, transaction code, receiving account number, bank name and transaction type. You can choose 1 of 3 encodings (JSON, form-urlencoded, multipart). MONA Pay is also rolling out an optional payload format compatible with popular Vietnamese gateways; see the last section. ## The `monapay` payload format (default) ```json { "amount": 2500000, "description": "noi dung chuyen khoan", "transfer_date": "10:30:00 28/08/2026", "transaction_code": "FT26240001234", "account_number": "1234567890", "bank_name": "ACB", "type": "income" } ``` | Field | Type | Meaning | Notes | |---|---|---|---| | `amount` | integer | Transaction amount in VND | No decimals. 2,500,000 VND is sent as `2500000` | | `description` | string | The transfer note the customer typed, or the note embedded in the QR code | Used to match orders when not using VAs | | `transfer_date` | string | Transaction time, format `YYYY-MM-DD HH:MM:SS`, Vietnam time | Taken from the ACB notification | | `transaction_code` | string | The bank's transaction code | Identical across every resend. Use it as the deduplication key | | `account_number` | string | The VA number or the receiving account number | Compare this field when matching by VA | | `bank_name` | string | Bank name | Currently always `ACB` | | `type` | string | Transaction type | Currently only `income` (money in) | Three things worth knowing: - **`transaction_code` is the unique key.** Resend 10 times and all 10 payloads carry the same `transaction_code`. Put a UNIQUE constraint on that column in your transactions table. - **`account_number` tells you which VA received the money.** If each order has its own VA, comparing this field is enough to know which order was paid; you do not need to parse `description`. - **Fields get added, never renamed.** When MONA Pay adds banks or transaction types, we add new values (a `bank_name` other than `ACB`, a `type` other than `income`) rather than renaming these 7 fields. Your code should ignore unknown fields instead of failing. ## The three Content-Types Choose in the dashboard or through the `payload_format` field when creating a configuration: | `payload_format` | What MONA Pay sends | Use when | |---|---|---| | `application/json` | The body is the JSON string above with no extra whitespace | Default. Every modern framework handles it | | `application/x-www-form-urlencoded` | Body like `amount=2500000&description=...`, as an HTML form | Legacy systems that only read `$_POST` | | `multipart/form-data` | Each field is one part of a multipart form | Systems that only accept multipart | With HMAC, the signed string is the raw body exactly as sent: compact JSON for the JSON type, the urlencoded string for the form type. Details in [Webhook security](/en/docs/webhooks/bao-mat). ## Accompanying headers | Header | Present when | Value | |---|---|---| | `Content-Type` | Always | Matches the chosen `payload_format` | | `X-Mona-Timestamp` | Auth type `HMAC_SHA256` | Unix seconds at send time | | `X-Mona-Signature` | Auth type `HMAC_SHA256` | `sha256=` | | Custom header name (default `X-Webhook-Secret`) | Auth type `API_KEY` | The secret you set | ## Compatible format (in progress) MONA Pay is adding an optional payload format compatible with popular Vietnamese gateways so switching providers does not require code changes; it will be announced on this page when it ships. Meanwhile the 7 fields above are enough for a 10-line adapter at the top of your handler: rename fields to match your existing structure, keep `transaction_code` as the deduplication key, then call your existing processing function. ## The test payload When you click Send test in the dashboard or call `POST /api/v1/client-webhooks/test`, MONA Pay sends a payload with exactly these 7 fields and sample data. Your endpoint should tell test payloads from real transactions by checking that `transaction_code` exists in the transaction list (through the [reconciliation API](/en/docs/webhooks/doi-soat)) before changing important order states, or simply send tests to a staging environment only. ## Quick answers **Can the amount ever be a decimal?** No. VND has no fractional unit; `amount` is always an integer. **Timezone of `transfer_date`?** Vietnam time (UTC+7), exactly as reported by ACB. **Are outgoing transactions sent?** Not yet. `type` is always `income`. When more types are added, field names stay the same. **Can I receive webhooks with Google Apps Script or n8n?** Yes, as long as the URL accepts POST and answers 200 within 10 seconds. For the HMAC signature you need the raw body to recompute it; see [Webhook security](/en/docs/webhooks/bao-mat). --- # Webhook security: HMAC-SHA256 signatures and replay protection > The 3 MONA Pay webhook auth types (HMAC_SHA256, API_KEY, NONE), how to compute and verify X-Mona-Signature, the 5-minute replay window, with cURL, PHP and Node samples. > URL: https://monapay.vn/en/docs/webhooks/bao-mat · Markdown: https://monapay.vn/en/docs/webhooks/bao-mat.md MONA Pay webhooks support 3 auth types: `HMAC_SHA256` (each payload signed with a shared secret, recommended), `API_KEY` (the secret sent in a header) and `NONE` (no authentication). With HMAC, every payload carries `X-Mona-Timestamp` and `X-Mona-Signature`; you recompute the signature from the raw body, accept on match, and reject payloads whose timestamp is more than 5 minutes off. ## Why authenticate A webhook URL is public. Anyone who guesses it can send a fake JSON payload saying "50,000,000 VND arrived". If the endpoint trusts it blindly, an order gets marked paid without a single dong. Authentication makes the endpoint trust only payloads MONA Pay sent. Of the 3 types, HMAC is the strongest because the secret never travels over the wire, only a signature derived from it. ## Type 1: `HMAC_SHA256` (recommended) ### How MONA Pay signs 1. `timestamp` = unix seconds at send time. 2. `raw_body` = the body string exactly as it will be sent: compact JSON (type `application/json`) or the urlencoded string (form type). 3. `signature = HMAC-SHA256(secret, timestamp + "." + raw_body)`, output as lowercase hex. 4. Send 2 headers: ```text X-Mona-Timestamp: 1756355400 X-Mona-Signature: sha256=3f2a9c...e71b ``` The secret is the string you set when creating the webhook configuration (the Secret field in the dashboard or `secret_key` via the API). Use at least 32 random characters and a different secret per configuration. ### How you verify 1. Read the untouched raw body (before parsing). 2. Check `|now - timestamp| <= 300` seconds. More than 5 minutes off: answer 400 and drop it. This is replay protection: an attacker who captures a valid payload cannot reuse it after 5 minutes. 3. Recompute `sha256=` + HMAC-SHA256(secret, `timestamp + "." + raw_body`). 4. Compare with `X-Mona-Signature` using a constant-time function (`hash_equals`, `crypto.timingSafeEqual`). Match: process. Otherwise answer 401. ```bash # Simulate MONA Pay firing a webhook at your endpoint for a local test SECRET='your_hmac_secret' # the HMAC secret you set in the dashboard URL='https://your-domain.com/webhook/monapay' TS=$(date +%s) BODY='{"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"1234567890","bank_name":"ACB","type":"income"}' # Signature = HMAC-SHA256(secret, "."); printf avoids a trailing newline SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST "$URL" \ -H 'Content-Type: application/json' \ -H "X-Mona-Timestamp: $TS" \ -H "X-Mona-Signature: sha256=$SIG" \ --data "$BODY" ``` ```php " // 1. Block replays: reject if the timestamp is more than 5 minutes off if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit('timestamp expired'); } // 2. Verify the signature: HMAC-SHA256(secret, ".") $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret); if (!hash_equals($expected, $signature)) { // hash_equals prevents timing attacks http_response_code(401); exit('bad signature'); } // 3. Answer 200 right away, push heavy work to a queue/cron http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); $data = json_decode($raw, true); // $data['amount'], $data['description'], $data['transaction_code'], ... // TODO: record the transaction and update your order here ``` ```js // webhook-monapay.js: receive MONA Pay webhooks with Express const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.MONA_WEBHOOK_SECRET; // the HMAC secret you set when enabling HMAC // Read the raw body or the signature will not match (do not use express.json() on this route) app.post('/webhook/monapay', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.header('X-Mona-Timestamp') || ''; const signature = req.header('X-Mona-Signature') || ''; // "sha256=" const rawBody = req.body.toString('utf8'); // 1. Block replays: reject if the timestamp is more than 5 minutes off if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.status(400).send('timestamp expired'); } // 2. Verify the signature: HMAC-SHA256(secret, ".") const expected = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(timestamp + '.' + rawBody) .digest('hex'); const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!valid) return res.status(401).send('bad signature'); // 3. Answer 200 right away, process the order asynchronously res.status(200).send('OK'); const data = JSON.parse(rawBody); // data.amount, data.description, data.transaction_code, ... // TODO: record the transaction and update your order here }); app.listen(3000, () => console.log('MONA Pay webhook listening on port 3000')); ``` ### Getting the raw body in each framework The signature is computed over every byte of the body. Any framework that parses JSON and re-encodes it breaks verification, because key order, whitespace or unicode escaping may differ. How to get the raw body: | Framework | Raw body | |---|---| | Plain PHP, WordPress | `file_get_contents('php://input')` | | Laravel | `$request->getContent()` | | Express | `express.raw({ type: 'application/json' })` on the webhook route, then `req.body.toString('utf8')` | | Next.js (App Router) | `await request.text()` instead of `request.json()` | | Django | `request.body` (bytes) | | FastAPI | `await request.body()` | | Google Apps Script | `e.postData.contents` | In WordPress, register the route with `register_rest_route` using `methods => 'POST'` and read `$request->get_body()`. ## Type 2: `API_KEY` MONA Pay sends the secret verbatim in one header. You choose the header name when creating the configuration (`api_key_name`), default `X-Webhook-Secret`. The endpoint compares the header with the stored secret and accepts on match. ```php What counts as a successful webhook (200/201/202 within 10 seconds), the 9 error labels in the log and how to fix them, manual resend from the dashboard, success-rate and P95 statistics. > URL: https://monapay.vn/en/docs/webhooks/gui-lai-va-xu-ly-loi · Markdown: https://monapay.vn/en/docs/webhooks/gui-lai-va-xu-ly-loi.md MONA Pay counts a webhook delivery as successful when your server answers HTTP 200, 201 or 202 within 10 seconds. Every delivery is logged with the HTTP code, response time and an error label. Failed deliveries can be resent by hand from the dashboard; scheduled automatic retries (up to 7) are being rolled out. ## What counts as success | Condition | Value | |---|---| | Accepted HTTP codes | 200, 201, 202 | | Maximum wait | 10 seconds from the moment MONA Pay starts sending | | Response body | Optional, MONA Pay does not read it | | Redirects | Not followed. 301/302 count as failed | So your endpoint only needs to return `200 OK` with an empty body. Do not return 204 (not in the list) and do not put a redirect in front of the webhook URL. ## Per-delivery log Every delivery MONA Pay makes, including tests and resends, is one row in the dashboard under Webhooks, tab Delivery history. The same data is available at `GET /api/v1/webhook-logs`. | Log field | Meaning | |---|---| | Sent at | When MONA Pay started the request | | HTTP code | The code your server returned, empty if no connection | | `duration_ms` | Time from sending to receiving the response, in milliseconds | | `error_label` | Result classification, see the table below | | Payload | The payload that was sent, for your reference | | Response | The first few hundred characters of your server's response body, handy for debugging | ## The 9 error labels and how to fix them | `error_label` | Meaning | Common cause | Fix | |---|---|---|---| | `OK` | Success | Server answered 200/201/202 within 10 seconds | Nothing to do | | `HTTP_3XX` | Server returned a redirect | URL missing `https`, missing or extra trailing `/`, www redirect | Register the final URL, no redirects | | `HTTP_4XX` | Server rejected | 401/403 wrong secret or an application firewall; 404 wrong path; 405 route does not accept POST; 422 endpoint expects fields that are absent | Check secret, path and method. For WAFs (Cloudflare, ModSecurity) allow IP `103.168.55.14` | | `HTTP_5XX` | Server error | Code threw an exception, out of memory, database unreachable | Check your server logs around the `duration_ms` timestamp | | `TIMEOUT` | No response within 10 seconds | Endpoint processes the order before answering, or the server is overloaded | Answer 200 first, process afterwards | | `SSL` | TLS handshake failed | Expired or self-signed certificate, wrong hostname, missing intermediate chain | Check with `openssl s_client -connect your-domain:443`, install the full chain | | `DNS` | Hostname does not resolve | Typo in the domain, expired domain, DNS not propagated | Check `dig your-domain` from an outside machine | | `CONNECTION` | Could not open a connection | Firewall blocks port 443, server down, wrong port | Open port 443 for IP `103.168.55.14`, check the server is running | | `ERROR` | Other error | Anything that does not fit the labels above | Read the response in the log; call 1900 636 648 if unclear | ## Manual resend In Delivery history every failed row has a Resend button. Clicking it makes MONA Pay send the exact same payload to the same URL immediately and adds a new log row. `transaction_code` does not change, so your endpoint can tell it is an old payload. You can also resend through the API: `POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry` with body `{"target_type": "WEBHOOK"}` (or `"TELEGRAM"`, plus `target_id` for the configuration to resend to), with `Authorization: Bearer` + `X-Client-Secret` like every write call. See [Webhook configuration API](/en/docs/api/webhook-configs). Two situations where resending helps: - **After fixing a server bug.** For example the endpoint returned 500 because a table was missing; create the table, then resend each failed row. - **Testing after a code change.** Resend an old transaction to see whether the new code handles it correctly, instead of transferring real money. ## Automatic retries (in progress) Webhook configurations have a `max_retries` field, default 7, reserved for scheduled retries with increasing delays when your server errors. This mechanism is being rolled out and is not yet live in production; today every resend is triggered by you. When it ships, the exact schedule will be published on this page. Meanwhile run [periodic reconciliation](/en/docs/webhooks/doi-soat) so nothing is missed while your server has trouble. ## Success rate and P95 statistics The Webhooks section of the dashboard has a statistics tab fed by `GET /api/v1/webhook-logs/stats`: total deliveries, success rate, average and P95 response time (95% of deliveries are faster than this), and the distribution of error labels. Some reference points: - Success rate below 99% in a day: something is wrong on your server; look at the error label distribution. - P95 above 3,000 ms: the endpoint does heavy work before answering and will eventually hit `TIMEOUT` under load. - A sudden rise in `HTTP_4XX` after a deploy: usually the secret or the path changed without updating the configuration. ```bash curl "https://api.monapay.vn/api/v1/webhook-logs/stats" \ -H "Authorization: Bearer $TOKEN" ``` ## Handling on your side Three rules so the endpoint never fails under load: 1. **Answer first, work later.** As soon as the signature checks out, return 200. Push order updates, emails and third-party API calls to a queue (Redis, database, cron). The PHP sample uses `fastcgi_finish_request()`, the Node sample calls `res.send()` before continuing; see [Webhook integration](/en/docs/webhooks/tich-hop-webhook). 2. **Deduplicate with `transaction_code`.** Manual resends, two configurations on the same URL, and future automatic retries all produce duplicates. A UNIQUE constraint on `transaction_code` is enough. 3. **Keep your own log.** Store the raw body and headers of every payload for at least 30 days. When you need to compare with MONA Pay's log, both sides look at the same `transaction_code`. ## Common problems **Log says `OK` but the order status did not change.** The endpoint returned 200 and the processing afterwards failed. Check your application log; MONA Pay only sees the HTTP code. **Resend produces the same error label.** The server-side bug is not fixed yet. Reproduce with the cURL simulation on the [Webhook security](/en/docs/webhooks/bao-mat) page from another machine. **`TIMEOUT` only at peak hours.** The server is overloaded and processes synchronously. Switch to answering 200 first. **Every webhook shows `CONNECTION` from a certain moment.** Usually a firewall or server change. Re-open port 443 for IP `103.168.55.14`. --- # Reconciling transactions with the MONA Pay API > Pull the transaction list page by page (up to 100 per page) through the API, compare with your books by transaction_code, with a suggested hourly or daily cron schedule. > URL: https://monapay.vn/en/docs/webhooks/doi-soat · Markdown: https://monapay.vn/en/docs/webhooks/doi-soat.md Reconciliation compares the transactions MONA Pay recorded with the transactions table in your own system, to fill in anything the webhook did not deliver (server maintenance, network outage, a code bug). You call `GET /api/v1/acb/virtual-account/transactions` page by page, at most 100 transactions per page, and compare by `transaction_code`. Run it hourly, or at least daily. ## Why reconcile when you already have webhooks Webhooks are the real-time channel, but they depend on your server being alive the moment money arrives. A 2-minute deploy, an expired SSL certificate, a locked database, and a payment is missed. MONA Pay currently resends by hand (automatic retries are in progress, see [Retries and error handling](/en/docs/webhooks/gui-lai-va-xu-ly-loi)), so periodic reconciliation is the safety net you want from day one. ## The transaction list API ```text GET https://api.monapay.vn/api/v1/acb/virtual-account/transactions ``` | Query parameter | Required | Meaning | |---|---|---| | `virtual_account_number` | yes | The VA whose transactions you want | | `page` | no | Page number, starting at 1 | | `limit` | no | Transactions per page, at most 100 | It is a GET, so only `Authorization: Bearer ` is needed, no `X-Client-Secret`. ```bash curl "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=1234567890&page=1&limit=100" \ -H "Authorization: Bearer $TOKEN" ``` The response uses the common envelope `{"success": true, "message": "...", "data": {...}}`, where `data` is a pagination block (verified in the backend code on 28/08/2026): `data.data` is the array of transactions, alongside `current_page`, `per_page`, `total` and `last_page`. Each transaction in `data.data` carries the same information as the webhook payload: transaction code, amount, time, reference, receiving account number. Stop looping when `current_page >= last_page`. Field details are in `https://monapay.vn/openapi.json` (section `/api/v1/acb/virtual-account/transactions`). The API does not yet have a `since_id` parameter or date-range filter. The working approach is to pull page by page from page 1 (newest first) and stop when you hit a `transaction_code` already in your table and the page contains no new codes. ## Reconciliation algorithm 1. For each VA in use, call page 1 with `limit=100`. 2. For each transaction, check whether `transaction_code` already exists in your table. 3. Not there: insert it and run exactly the same processing as when a webhook arrives (update the order, send email...). Use the same function so the order is updated identically whichever way the transaction came in. 4. If the whole page already exists, stop. If there were new codes, call the next page. 5. Record the run time and the number of transactions added, so you know whether webhooks are being missed. ```bash # Quick shell reconciliation: list transaction_code on page 1 for an eyeball check curl -s "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=1234567890&page=1&limit=100" \ -H "Authorization: Bearer $TOKEN" | python3 -c ' import sys, json d = json.load(sys.stdin)["data"] items = d if isinstance(d, list) else d.get("data") or [] # data.data = array of transactions for t in items: print(t.get("transaction_code"), t.get("amount"), t.get("transaction_date") or t.get("transfer_date")) ' ``` ```php prepare('SELECT 1 FROM transactions WHERE transaction_code = ?'); for ($page = 1; $page <= 50; $page++) { $url = "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=$va&page=$page&limit=100"; $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]); $res = json_decode(curl_exec($ch), true); curl_close($ch); $items = $res['data']['data'] ?? []; // data.data = array of transactions, with current_page/last_page if (!$items) break; $added = 0; foreach ($items as $t) { $check->execute([$t['transaction_code']]); if ($check->fetch()) continue; // already there, the webhook arrived processTransaction($t); // same function as the webhook handler, UNIQUE prevents duplicates $added++; } if ($added === 0) break; // whole page already known, stop } ``` ```js // reconcile.js: run hourly from cron (node reconcile.js) const TOKEN = process.env.MONA_ACCESS_TOKEN; const VA = '1234567890'; async function reconcile() { for (let page = 1; page <= 50; page++) { const url = `https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=${VA}&page=${page}&limit=100`; const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } }); const body = await res.json(); let items = body.data?.data ?? []; // data.data = array of transactions, with current_page/last_page if (items.length === 0) break; let added = 0; for (const t of items) { const exists = await db.exists('transactions', { transaction_code: t.transaction_code }); if (exists) continue; await processTransaction(t); // same function as the webhook handler added++; } if (added === 0) break; } } reconcile().catch((e) => { console.error(e); process.exit(1); }); ``` Note that the `access_token` expires (`expires_in` at login). Cron scripts should log in again on 401; see [Authentication](/en/docs/api/xac-thuc). ## Suggested schedule | System type | Frequency | Why | |---|---|---| | Instant-delivery stores, ticket sales, courses unlocked immediately | Every 15 minutes | Customers are waiting right after paying | | Management software, tuition, service fees | Hourly | Fast enough, low resource use | | End-of-day accounting | 23:30 daily | Closing the books, printing reports | Combine both: a quick page-1 reconciliation every hour, and a full pass through every page at the end of the day. ## Reconciling from the dashboard No code needed: open Transactions in the dashboard, filter by VA and date range, export CSV and compare with your books in Excel. The `transaction_code` column is the key; use VLOOKUP or Power Query. This suits accountants closing weekly or monthly. ## Common problems **The API returns 401.** The token expired. Log in again for a new token. **Page 1 is empty although there are transactions.** Wrong `virtual_account_number`, or the money went to the main account rather than that VA. Check the VA number in the dashboard under Banks & VA. **Reconciliation inserts duplicates.** The table has no UNIQUE constraint on `transaction_code`, or the webhook handler and the reconciliation job write to two different tables. Use one function and one table. **The script runs too long.** It pulls every page every time. Stop as soon as a page has no new codes, as in the algorithm above. --- # MONA Pay API authentication: Bearer token and X-Client-Secret > Log in for a Bearer token (valid 24 hours), add X-Client-Secret on POST/PUT/DELETE. The success/message/data envelope, 401/422 errors, and cURL, PHP, Node samples. > URL: https://monapay.vn/en/docs/api/xac-thuc · Markdown: https://monapay.vn/en/docs/api/xac-thuc.md Every MONA Pay API call needs the header `Authorization: Bearer `. The token comes from `POST /api/v1/client/login` using the username and password of your my.monapay.vn account and is valid for 86,400 seconds (24 hours). Write calls (POST, PUT, DELETE) add the header `X-Client-Secret` with a secret generated under [API keys](/en/docs/api/api-keys). Every response is wrapped in the same envelope `{"success": true, "message": "...", "data": ...}`. ## Base URL | Environment | URL | |---|---| | Production | `https://api.monapay.vn` | | Legacy alias (still running, for pre-2026 integrations) | `https://ipn.mona.host` | Accounts are usable immediately after sign-up: log in, create an API key, no approval step. A separate sandbox (simulated data, no real bank) is in progress. ## Common response envelope Every endpoint returns the same envelope, including on errors: ```json { "success": true, "message": "Success", "data": { } } ``` | Field | Type | Meaning | |---|---|---| | `success` | boolean | `true` when processed, `false` on a business error | | `message` | string | Short message for logging | | `data` | object / array / null | Returned data, `null` if none | Validation errors (missing field, wrong type) return HTTP 422 in the FastAPI style with a `detail[]` list of the offending fields. ## Two layers of authentication | Layer | Header | Used for | Where to get it | |---|---|---|---| | Bearer token | `Authorization: Bearer ` | Every request (except sign-up and login) | `POST /api/v1/client/login` | | Client secret | `X-Client-Secret: ` | POST, PUT, DELETE | `POST /api/v1/client-keys/generate` | Keep the token and the secret in environment variables, never hard-coded, never committed to git. ## POST /api/v1/client/register-client Creates a new account. No authentication. The account is usable immediately; log in right away, no approval. The sign-up form at [my.monapay.vn/auth](https://my.monapay.vn/auth) (Sign up tab) calls this same endpoint. | Field | Type | Required | Notes | |---|---|---|---| | `username` | string (1-255) | yes | Login name | | `password` | string (1-255) | yes | Password | | `name` | string (1-255) | yes | Business name / display name | ```bash curl -X POST https://api.monapay.vn/api/v1/client/register-client \ -H 'Content-Type: application/json' \ -d '{"username":"shopabc","password":"StrongPassword#2026","name":"Shop ABC"}' ``` Response 200/201: ```json { "success": true, "message": "Client registered successfully", "data": {} } ``` ## POST /api/v1/client/login Exchanges username + password for an `access_token`. No authentication. | Field | Type | Required | |---|---|---| | `username` | string | yes | | `password` | string | yes | Response 200: ```json { "success": true, "message": "Login successful", "data": { "access_token": "Zx9...64-url-safe-characters", "expires_in": 86400, "token_type": "Bearer" } } ``` `expires_in` is in seconds. When it expires, log in again; there is no refresh token. If the account has 2FA enabled in the dashboard, web login asks for an OTP as well; for direct API calls use an account without 2FA or contact us. **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/client/login \ -H 'Content-Type: application/json' \ -d '{"username":"shopabc","password":"StrongPassword#2026"}' ``` **PHP** ```php [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => json_encode(['username' => getenv('MONA_USER'), 'password' => getenv('MONA_PASS')]), ], ])), true); if (empty($res['success'])) { throw new RuntimeException('MONA Pay login failed: ' . ($res['message'] ?? 'unknown')); } $accessToken = $res['data']['access_token']; // keep it, valid for 24 hours ``` **Node** ```js const res = await fetch('https://api.monapay.vn/api/v1/client/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: process.env.MONA_USER, password: process.env.MONA_PASS }), }); const json = await res.json(); if (!json.success) throw new Error('MONA Pay login failed: ' + json.message); const accessToken = json.data.access_token; // keep it, valid for 24 hours ``` ## GET /api/v1/client/me Information about the logged-in account. Needs Bearer. ```bash curl https://api.monapay.vn/api/v1/client/me \ -H "Authorization: Bearer $MONA_TOKEN" ``` Response: `data` contains the client record (id, username, name, active status, creation time). ## PUT /api/v1/client/change-password Changes your own password. Needs Bearer + `X-Client-Secret`. | Field | Type | Required | |---|---|---| | `old_password` | string | yes | | `new_password` | string | yes | ```bash curl -X PUT https://api.monapay.vn/api/v1/client/change-password \ -H "Authorization: Bearer $MONA_TOKEN" \ -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"old_password":"StrongPassword#2026","new_password":"NewPassword#2026"}' ```
**Enforcement status (checked 28/08/2026):** the production server does not yet reject write calls that lack `X-Client-Secret`; the enforcing update is written and waiting to be deployed. Send the header now so nothing changes for you when it is switched on.
After changing the password the old token keeps working until it expires; to cut it off immediately, log in again and revoke the key under API keys. ## Common errors | HTTP | Cause | Fix | |---|---|---| | 401 | Wrong username/password or expired token | Check the credentials (new accounts can log in immediately, there is no approval queue); log in again if the token expired | | 401 `Authorization scheme must be Bearer` | Malformed header | Use `Authorization: Bearer ` with a space after Bearer | | 422 | Missing required field or wrong type | Read `detail[]` in the body and fix the field names | | 500 | MONA Pay-side error | Retry after a few seconds; if it repeats, report it to us with the `message` | ## Next steps 1. [Create an API key](/en/docs/api/api-keys) to get `X-Client-Secret`. 2. [Link an ACB account and create virtual accounts](/en/docs/api/tai-khoan-ao-va). 3. [Configure webhooks](/en/docs/api/webhook-configs) to receive incoming payments. --- # API keys: create, list and revoke X-Client-Secret > Generate a client_secret (shown once), list and revoke keys. The secret is sent in the X-Client-Secret header on every POST/PUT/DELETE. > URL: https://monapay.vn/en/docs/api/api-keys · Markdown: https://monapay.vn/en/docs/api/api-keys.md A MONA Pay API key is the `client_secret` string generated by `POST /api/v1/client-keys/generate`. The system shows the secret exactly once at creation and then stores only a hash, so it cannot be viewed again. Send the secret in the `X-Client-Secret` header on every POST, PUT and DELETE request (together with the Bearer token). If you lose it, create a new key and revoke the old one; an account can have many keys. ## When you need a key | Action | Needs Bearer | Needs X-Client-Secret | |---|---|---| | Sign up, log in | no | no | | Reading data (GET): transactions, VAs, webhook logs | yes | no | | Writing data (POST, PUT, DELETE): create VA, create QR, configure webhooks/Telegram, change password | yes | yes | Clean practice: one key per integrated system (store, accounting software, bot), named after that system. If one system leaks its key, revoke that key only; the others are unaffected. ## POST /api/v1/client-keys/generate Needs Bearer. | Field | Type | Required | Notes | |---|---|---|---| | `name` | string | no | A memorable name, default `Default Key` | Response 200: ```json { "success": true, "message": "Key generated successfully", "data": { "id": "0190a1b2-...", "client_id": "0190a0f0-...", "client_secret": "mps_...secret-shown-only-once", "name": "Online store", "is_active": true, "created_at": "2026-08-28T10:30:00" } } ``` Store `client_secret` in an environment variable (`MONA_SECRET`) right away. Once the screen is closed it cannot be retrieved. **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/client-keys/generate \ -H "Authorization: Bearer $MONA_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"Online store"}' ``` **PHP** ```php ['method' => 'POST', 'header' => $headers, 'content' => json_encode($body), 'ignore_errors' => true], ])); return json_decode($raw, true) ?? ['success' => false, 'message' => 'Could not read response']; } $res = monaPost('/api/v1/client-keys/generate', ['name' => 'Online store'], getenv('MONA_TOKEN')); $clientSecret = $res['data']['client_secret'] ?? null; // write to .env, shown only once ``` **Node** ```js const res = await fetch('https://api.monapay.vn/api/v1/client-keys/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}` }, body: JSON.stringify({ name: 'Online store' }), }); const { success, data, message } = await res.json(); if (!success) throw new Error(message); console.log('Write to .env:', data.client_secret); // shown only once ``` ## GET /api/v1/client-keys/list Lists the account's keys without secrets. Needs Bearer. ```bash curl https://api.monapay.vn/api/v1/client-keys/list \ -H "Authorization: Bearer $MONA_TOKEN" ``` ```json { "success": true, "message": "Keys retrieved", "data": [ { "id": "0190a1b2-...", "client_id": "0190a0f0-...", "name": "Online store", "is_active": true, "created_at": "2026-08-28T10:30:00" } ] } ``` ## DELETE /api/v1/client-keys/destroy/{key_id} Revokes a key. Needs Bearer. Requests using this key's secret are rejected immediately after revocation. ```bash curl -X DELETE https://api.monapay.vn/api/v1/client-keys/destroy/0190a1b2-... \ -H "Authorization: Bearer $MONA_TOKEN" ``` Response: `{"success": true, "message": "Key destroyed", "data": null}`. A key that does not exist or does not belong to the account returns 404. ## Using the secret in a write request ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks \ -H "Authorization: Bearer $MONA_TOKEN" \ -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"name":"Online store","webhook_url":"https://shop.example.com/webhook/monapay","auth_type":"HMAC_SHA256","secret_key":"your-hmac-secret"}' ``` Note the two different secrets: `client_secret` (X-Client-Secret) is for you to call the MONA Pay API; `secret_key` in a webhook configuration is for MONA Pay to sign payloads sent to your server (see [Webhook security](/en/docs/webhooks/bao-mat)). They should be different strings.
**Enforcement status (checked 28/08/2026):** the production server does not yet reject write calls that lack `X-Client-Secret`; the enforcing update is written and waiting to be deployed. Send the header now so nothing changes for you when it is switched on.
## Common errors | HTTP | Cause | Fix | |---|---|---| | 401 | Missing or wrong Bearer token | Log in again for a new token | | 404 | `key_id` does not exist or does not belong to the account | Call `/client-keys/list` for the right id | | 422 | `key_id` is not a valid UUID | Copy the id exactly from the list | Keys can also be created and revoked in the dashboard under API Keys, which includes a "copy prompt for AI agent" block to paste into Claude Code, Codex or Cursor. --- # ACB virtual accounts (VA): creating a VA through the API > Register an ACB VA in 4 API steps: submit the request, verify the OTP, register for notifications, verify the second OTP. Plus querying, cancelling, and cURL, PHP, Node samples. > URL: https://monapay.vn/en/docs/api/tai-khoan-ao-va · Markdown: https://monapay.vn/en/docs/api/tai-khoan-ao-va.md A virtual account (VA) is a secondary account number ACB issues under your real account, using the prefix you registered. Money sent to a VA still sits in your real ACB account, but each VA can be attached to one order or one customer, so MONA Pay matches payments automatically without parsing the transfer note. Creating a VA through the API takes 4 steps: submit the registration, enter the OTP ACB sends to your phone, register for transaction notifications, enter the second OTP. Requirements: an ACB account in your name and the phone number currently registered with ACB. If you would rather not call the API, the my.monapay.vn dashboard has the same 4-step wizard under Banks & VA. ## The 4-step flow | Step | Endpoint | What happens | |---|---|---| | 1 | `POST /api/v1/acb/virtual-account/registration` | Send the account number, phone number and VA prefix. ACB receives the request and sends an OTP to the phone | | 2 | `POST /api/v1/acb/{acb_request_id}/virtual-account/verification` | Enter the OTP. ACB creates the VA and returns its number | | 3 | `POST /api/v1/acb/{virtual_account_id}/notification/registration` | Register for real-time transaction notifications. ACB sends a second OTP | | 4 | `POST /api/v1/acb/{acb_request_id}/notification/verification` | Enter the second OTP. From here on, incoming money is reported to MONA Pay | Skip steps 3 and 4 and the VA exists but ACB never reports transactions for it; webhooks and Telegram stay silent. This is the mistake we see most often when customers set things up themselves. Every request below needs `Authorization: Bearer` and `X-Client-Secret` (see [Authentication](/en/docs/api/xac-thuc)). ## Step 1: POST /api/v1/acb/virtual-account/registration | Field | Type | Required | Notes | |---|---|---|---| | `bank_account_id` | uuid | no | Use when the ACB account was linked before (from `GET /api/v1/client/bank-accounts`). When present, omit `account_number` and `phone_number` | | `customer_type` | string | no | ACB customer type code, e.g. `PERS` for personal | | `account_number` | integer | no | The real ACB current account number | | `phone_number` | string | no | The phone number registered with ACB, receives the OTP | | `virtual_account_info.virtual_account_prefix_code` | string | yes | The VA prefix registered with ACB | | `virtual_account_info.virtual_account_content` | string | no | Identifier attached to the VA (order code, customer code) | | `virtual_account_info.virtual_account_explain` | string | no | Description at registration | | `virtual_account_info.beneficiary_name_rule` | integer | no | How the beneficiary name is displayed, per ACB convention | | `user_agreement` | boolean | no | Customer accepts the service terms; send `true` | Sample body (first time, no bank_account_id yet): ```json { "customer_type": "PERS", "account_number": 123456789, "phone_number": "0901234567", "virtual_account_info": { "virtual_account_prefix_code": "MONA", "virtual_account_content": "DH10234", "virtual_account_explain": "Don hang 10234" }, "user_agreement": true } ``` Response 200: `data` is the bank account just recorded, with `acb_request` containing the `id` needed in step 2. ```json { "success": true, "message": "Success", "data": { "id": "0190b0c1-...", "account_number": "123456789", "acb_request": { "id": "0190b0c2-...", "status": "PENDING", "created_at": "2026-08-28T10:31:00" } } } ``` **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/acb/virtual-account/registration \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"customer_type":"PERS","account_number":123456789,"phone_number":"0901234567","virtual_account_info":{"virtual_account_prefix_code":"MONA","virtual_account_content":"DH10234"},"user_agreement":true}' ``` ## Step 2: POST /api/v1/acb/{acb_request_id}/virtual-account/verification | Field | Type | Required | |---|---|---| | `code` | string | yes, the OTP ACB sent to the phone | ```bash curl -X POST https://api.monapay.vn/api/v1/acb/0190b0c2-.../virtual-account/verification \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' -d '{"code":"123456"}' ``` Response 200: `data` is the VA just created. ```json { "success": true, "message": "Success", "data": { "id": "0190b0c3-...", "bank_account_id": "0190b0c1-...", "prefix_code": "MONA", "content": "DH10234", "explain": "Don hang 10234", "beneficiary_name_rule": null, "virtual_account_number": "MONA0000010234", "created_at": "2026-08-28T10:32:00", "updated_at": null } } ``` `virtual_account_number` is the number you give the customer to pay into (or embed in a QR). `id` is used in step 3. ## Step 3: POST /api/v1/acb/{virtual_account_id}/notification/registration | Field | Type | Required | Notes | |---|---|---|---| | `receive_noti_realtime` | boolean | yes | `true` = report each transaction immediately (required for webhooks). `false` = ACB batches an end-of-day report | | `username` | string (≤50) | no | Login information when ACB requires it; leave empty otherwise | ```bash curl -X POST https://api.monapay.vn/api/v1/acb/0190b0c3-.../notification/registration \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' -d '{"receive_noti_realtime":true}' ``` Response 200/201: `data` contains `acb_request.id` for step 4. ## Step 4: POST /api/v1/acb/{acb_request_id}/notification/verification Body `{"code": ""}`, same as step 2. On success you are done: from the next transaction on, ACB reports to MONA Pay and MONA Pay fires the [webhook](/en/docs/webhooks/tich-hop-webhook) or [Telegram](/en/docs/telegram) message according to your configuration. ## PHP example: steps 1 and 2 end to end ```php true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]); $res = json_decode(curl_exec($ch), true) ?? []; curl_close($ch); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Unknown error'); return $res['data']; } // Step 1: submit the request, ACB sends an OTP to the phone $reg = call("$base/api/v1/acb/virtual-account/registration", $headers, [ 'customer_type' => 'PERS', 'account_number' => 123456789, 'phone_number' => '0901234567', 'virtual_account_info' => ['virtual_account_prefix_code' => 'MONA', 'virtual_account_content' => 'DH10234'], 'user_agreement' => true, ]); $requestId = $reg['acb_request']['id']; // Step 2: enter the OTP (read from your form) $va = call("$base/api/v1/acb/$requestId/virtual-account/verification", $headers, ['code' => $_POST['otp']]); echo 'VA number: ' . $va['virtual_account_number']; ``` ## Node example: steps 3 and 4 ```js const base = 'https://api.monapay.vn'; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET, }; async function call(path, body) { const r = await fetch(base + path, { method: 'POST', headers, body: JSON.stringify(body) }); const j = await r.json(); if (!j.success) throw new Error(j.message); return j.data; } // Step 3: register for real-time notifications, ACB sends the second OTP const noti = await call(`/api/v1/acb/${vaId}/notification/registration`, { receive_noti_realtime: true }); // Step 4: verify the second OTP await call(`/api/v1/acb/${noti.acb_request.id}/notification/verification`, { code: secondOtp }); ``` ## Querying and managing | Endpoint | Purpose | |---|---| | `GET /api/v1/client/bank-accounts?page=1&limit=10` | Linked ACB accounts (to get `bank_account_id`) | | `GET /api/v1/acb/{bank_account_id}/virtual-account/retrieve?virtual_account_number=&page=1&limit=10` | VAs of one account, filterable by VA number, `limit` up to 100 | | `GET /api/v1/acb/{virtual_account_id}/notification/details` | View a VA's notification registration | | `POST /api/v1/acb/{acb_notification_id}/notification/modification` | Change the notification mode (body as in step 3), needs OTP re-verification through step 4 | | `POST /api/v1/acb/{acb_notification_id}/notification/delete` | Cancel notifications | | `POST /api/v1/acb/{virtual_account_id}/virtual-account/delete` | Cancel the VA, no body. ACB may require an OTP through the verification endpoint | ## Common problems | Symptom | Cause | Fix | |---|---|---| | 400 after step 1 | The account is not an ACB account, the phone number does not match ACB's records, or the VA prefix has not been issued by ACB | Check with ACB; the VA prefix must be registered with ACB first | | 400 wrong `code` in step 2/4 | OTP mistyped or expired | Repeat the previous step so ACB sends a new OTP | | VA created but no transactions show up | Steps 3 and 4 not done | Call `notification/registration` and verify the second OTP | | 401 | Token expired or `X-Client-Secret` missing | Log in again, check headers | | 422 | Missing `virtual_account_info.virtual_account_prefix_code` | This field is required | --- # ACB bank QR API (dynamic VietQR) per order > Create an ACB VietQR code carrying the order code and amount via POST /acb/qr-payment/generate, get qr_data_url to display; cancel unpaid QR codes. cURL, PHP, Node samples. > URL: https://monapay.vn/en/docs/api/qr-thanh-toan · Markdown: https://monapay.vn/en/docs/api/qr-thanh-toan.md A MONA Pay payment QR is a dynamic VietQR code generated by ACB: the account number (or VA), amount and order code are pre-filled, so the customer scans it with any banking app and pays the exact amount with the exact reference, nothing typed by hand. Call `POST /api/v1/acb/qr-payment/generate` with `orderId`, `amount` (VND, integer, up to 1,000,000,000) and the VA prefix; MONA Pay returns `qr_data_url` for you to render on the checkout page. When the customer pays, ACB reports it and MONA Pay fires the [webhook](/en/docs/webhooks/tich-hop-webhook) exactly like money arriving in a VA. A QR created by mistake can be cancelled through the cancellation endpoint before the customer scans it. If you would rather not call the API, the dashboard has a Create QR section: pick a VA, enter the amount, download the image to print or send to the customer. ## POST /api/v1/acb/qr-payment/generate Needs Bearer + `X-Client-Secret`. Requirement: a linked ACB account with a VA (see [Virtual accounts](/en/docs/api/tai-khoan-ao-va)). | Field | Type | Required | Notes | |---|---|---|---| | `ownerNumber` | string | yes | The receiving ACB account number | | `ownerType` | `PER` / `ORG` | yes | `PER` personal, `ORG` business | | `merchantId` | string | yes | Merchant code issued by ACB when the QR service was opened. The correct value is shown in the dashboard under Create QR | | `terminalId` | string | yes | Terminal code issued by ACB, shown next to `merchantId` | | `orderId` | string | yes | Your order code, unique per payment | | `virtualAccountPrefix` | string (1-10) | yes | VA prefix | | `beneficiaryName` | string (1-100) | yes | The beneficiary name shown in the customer's banking app | | `amount` | integer | yes | VND amount, 0 to 1,000,000,000. `0` = the customer enters the amount | | `description` | string (≤255) | no | Transfer note pre-filled for the customer | | `traceNumber` | string | no | Your own tracking code, recommended for lookups | | `userId` | string | no | Customer id in your system | | `voucherCode`, `loyaltyCode` | string | no | Discount / loyalty codes if you want them stored | | `additionalInfo` | array `[{key, value}]` | no | Extra key-value data | Sample body: ```json { "ownerNumber": "123456789", "ownerType": "ORG", "merchantId": "MC00012345", "terminalId": "TM0001", "orderId": "DH10234", "virtualAccountPrefix": "MONA", "beneficiaryName": "CONG TY ABC", "amount": 2500000, "description": "Thanh toan DH10234", "traceNumber": "DH10234-20260828" } ``` Response 200: `data` is the QR record; the important fields are `qr_data_url` and `virtual_account_number`. ```json { "success": true, "message": "Success", "data": { "id": "0190c0d1-...", "virtual_account_id": "0190b0c3-...", "owner_number": "123456789", "owner_type": "ORG", "merchant_id": "MC00012345", "terminal_id": "TM0001", "user_id": null, "order_id": "DH10234", "virtual_account_prefix": "MONA", "beneficiary_name": "CONG TY ABC", "amount": 2500000, "voucher_code": null, "loyalty_code": null, "description": "Thanh toan DH10234", "additional_info": null, "virtual_account_number": "MONA0000010234", "trace_number": "DH10234-20260828", "qr_data_url": "00020101021238...6304ABCD", "created_at": "2026-08-28T10:40:00", "updated_at": null } } ``` `qr_data_url` is the standard VietQR (EMVCo) data string. Feed it to any QR rendering library (for example `qrcode` for Node, `endroid/qr-code` for PHP) to get an image. Keep `id` in case you need to cancel. **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/acb/qr-payment/generate \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"ownerNumber":"123456789","ownerType":"ORG","merchantId":"MC00012345","terminalId":"TM0001","orderId":"DH10234","virtualAccountPrefix":"MONA","beneficiaryName":"CONG TY ABC","amount":2500000,"description":"Thanh toan DH10234"}' ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('MONA_TOKEN'), 'X-Client-Secret: ' . getenv('MONA_SECRET')], CURLOPT_POSTFIELDS => json_encode([ 'ownerNumber' => '123456789', 'ownerType' => 'ORG', 'merchantId' => getenv('ACB_MERCHANT_ID'), 'terminalId' => getenv('ACB_TERMINAL_ID'), 'orderId' => $order->code, 'virtualAccountPrefix' => 'MONA', 'beneficiaryName' => 'CONG TY ABC', 'amount' => (int) $order->total, 'description' => 'Thanh toan ' . $order->code, ]), ]); $res = json_decode(curl_exec($ch), true); curl_close($ch); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'QR generation failed'); $qrString = $res['data']['qr_data_url']; // feed to a QR rendering library $qrId = $res['data']['id']; // keep to cancel if the order is cancelled ``` **Node** ```js import QRCode from 'qrcode'; // npm i qrcode const r = await fetch('https://api.monapay.vn/api/v1/acb/qr-payment/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET }, body: JSON.stringify({ ownerNumber: '123456789', ownerType: 'ORG', merchantId: process.env.ACB_MERCHANT_ID, terminalId: process.env.ACB_TERMINAL_ID, orderId: order.code, virtualAccountPrefix: 'MONA', beneficiaryName: 'CONG TY ABC', amount: order.total, description: `Thanh toan ${order.code}`, }), }); const { success, data, message } = await r.json(); if (!success) throw new Error(message); const pngDataUrl = await QRCode.toDataURL(data.qr_data_url); // ``` ## DELETE /api/v1/acb/qr-payment/{qr_code_id}/cancellation Cancels a QR that was created but not yet paid (order cancelled, amount changed). Needs Bearer + `X-Client-Secret`. The body must repeat the details used at creation: | Field | Type | Required | |---|---|---| | `ownerNumber` | string | yes | | `ownerType` | `PER` / `ORG` | yes | | `orderId` | string | yes | | `amount` | integer | yes | | `traceNumber` | string | no | ```bash curl -X DELETE https://api.monapay.vn/api/v1/acb/qr-payment/0190c0d1-.../cancellation \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"ownerNumber":"123456789","ownerType":"ORG","orderId":"DH10234","amount":2500000}' ``` Response: `{"success": true, "message": "Success", "data": null}`. ## What happens after the customer scans ACB records the incoming money, notifies MONA Pay, and MONA Pay fires the webhook to your configured URL with the same payload as a VA transaction: ```json {"amount":2500000,"description":"Thanh toan DH10234","transfer_date":"10:45:12 28/08/2026","transaction_code":"FT26240001234","account_number":"MONA0000010234","bank_name":"ACB","type":"income"} ``` Match the order by `account_number` (the VA attached to the QR) or by the `orderId` inside `description`. Compare `amount` with the order total before marking it paid. ## Common problems | Symptom | Cause | Fix | |---|---|---| | 400 on creation | The account has no VA registered with this prefix, or `merchantId`/`terminalId` is wrong | Check the dashboard under Create QR; create one code on the web first | | 422 | Missing required field, `amount` above 1,000,000,000, `virtualAccountPrefix` longer than 10 characters | Fix according to `detail[]` | | Customer can scan but no webhook arrives | The VA is not registered for notifications (steps 3, 4 of VA creation) | See [Virtual accounts](/en/docs/api/tai-khoan-ao-va) | | QR expired in the banking app | Every dynamic QR has a validity period set by ACB | Cancel the old code and create a new one with the same `orderId` | --- # Transactions API: look up incoming money by virtual account > Query transactions by VA number (paginated, up to 100 per page), the record structure, reconciliation by transaction_code, and the endpoint that resends notifications for one transaction. > URL: https://monapay.vn/en/docs/api/giao-dich · Markdown: https://monapay.vn/en/docs/api/giao-dich.md Every transaction ACB reports is stored by MONA Pay, even when the webhook to your server failed. Query with `GET /api/v1/acb/virtual-account/transactions?virtual_account_number=&page=1&limit=100` (Bearer token, `limit` up to 100). Each record has `transaction_code`, the bank's stable reference, which you use as the deduplication key during reconciliation. For a transaction that never reached your server, call the retry endpoint and MONA Pay resends the webhook or Telegram message for exactly that transaction. ## GET /api/v1/acb/virtual-account/transactions Needs Bearer. The system looks the VA up by number, checks it belongs to your account, then returns its transactions. | Query parameter | Type | Required | Notes | |---|---|---|---| | `virtual_account_number` | string | yes | VA number, e.g. `MONA0000010234` | | `page` | integer ≥1 | no | Default 1 | | `limit` | integer 1-100 | no | Default 10 | ```bash curl "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=MONA0000010234&page=1&limit=100" \ -H "Authorization: Bearer $MONA_TOKEN" ``` Response 200: ```json { "success": true, "message": "Success", "data": { "data": [ { "id": "0190d0e1-...", "acb_callback_request_id": "0190d0e0-...", "transaction_status": "SUCCESS", "transaction_channel": "IBFT", "transaction_date": "2026-08-28T10:45:12", "effective_date": "2026-08-28T10:45:12", "debit_or_credit": "credit", "amount": 2500000, "transaction_content": "Thanh toan DH10234", "transaction_code": "FT26240001234", "account_number": "123456789", "va_prefix_cd": "MONA", "va_nbr": "MONA0000010234", "attributes": { "remitter_name": "NGUYEN VAN A", "remitter_account_number": "9876543210", "issuer_bank_name": "Vietcombank", "reference_number": "FT26240001234" } } ], "current_page": 1, "per_page": 100, "total": 1, "last_page": 1, "start": 1, "end": 1, "has_next": false, "has_prev": false } } ``` ### Field meanings | Field | Meaning | |---|---| | `id` | The transaction id inside MONA Pay, used by the retry endpoint | | `transaction_status` | Status reported by ACB (`SUCCESS` means the money is in) | | `transaction_channel` | ACB channel (fast transfer, QR...) | | `transaction_date` | Transaction time, Vietnam time | | `effective_date` | Booking date | | `debit_or_credit` | `credit` = money in, `debit` = money out. Webhooks currently fire for money in only | | `amount` | VND amount, integer | | `transaction_content` | The transfer note the customer typed (or the QR pre-filled) | | `transaction_code` | Bank reference, stable across resends, deduplication key | | `account_number` | The real ACB account that received the money | | `va_prefix_cd`, `va_nbr` | Prefix and number of the VA matched to the transaction, `null` when money went straight to the main account | | `attributes` | Extra data from ACB: remitter name and account, remitting bank, reference number, `custom1`..`custom10` when present | The webhook sends your server a condensed version of this same record (see [Payload format](/en/docs/webhooks/dinh-dang-payload)). ## Reconciling through the API There is no `since_id` parameter yet; reconcile as follows (cron every 15-30 minutes or end of day): 1. For each VA in use, call page 1 with `limit=100`, keep reading while `has_next` is `true`. 2. For each record, look up `transaction_code` in your transactions table. Skip if present; otherwise insert it and process the order as if a webhook had arrived. 3. Stop when you hit a `transaction_code` you already have whose `transaction_date` is older than the previous reconciliation mark (the list is newest first). Details and samples in [Reconciliation](/en/docs/webhooks/doi-soat). **PHP: read every page** ```php ['header' => 'Authorization: Bearer ' . getenv('MONA_TOKEN') . "\r\n", 'ignore_errors' => true]]); return json_decode(file_get_contents($url, false, $ctx), true) ?? []; } $va = 'MONA0000010234'; $page = 1; do { $res = monaGet("https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=$va&page=$page&limit=100"); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Query failed'); foreach ($res['data']['data'] as $tx) { if ($tx['debit_or_credit'] !== 'credit') continue; // INSERT ... ON DUPLICATE KEY (UNIQUE transaction_code) → only new records are written recordTransaction($tx['transaction_code'], $tx['amount'], $tx['transaction_content'], $tx['transaction_date']); } $page++; } while (!empty($res['data']['has_next'])); ``` **Node** ```js async function* monaTransactions(va) { for (let page = 1; ; page++) { const r = await fetch(`https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=${va}&page=${page}&limit=100`, { headers: { Authorization: `Bearer ${process.env.MONA_TOKEN}` }, }); const j = await r.json(); if (!j.success) throw new Error(j.message); yield* j.data.data; if (!j.data.has_next) break; } } for await (const tx of monaTransactions('MONA0000010234')) { if (tx.debit_or_credit !== 'credit') continue; await db.upsertByTransactionCode(tx.transaction_code, tx); // UNIQUE(transaction_code) } ``` ## POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry Resends the notification for one specific transaction (your server was down at the time, or the webhook configuration was wrong and has been fixed). Needs Bearer + `X-Client-Secret`. Scheduled automatic retries are in progress; today it is this call or the Resend button in the dashboard. | Field | Type | Required | Notes | |---|---|---|---| | `target_type` | `WEBHOOK` / `TELEGRAM` | yes | Channel to resend on | | `target_id` | uuid | no | A specific webhook or Telegram configuration id. Empty = resend to every active configuration matching the transaction | ```bash curl -X POST https://api.monapay.vn/api/v1/acb/virtual-account/transactions/0190d0e1-.../retry \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' -d '{"target_type":"WEBHOOK"}' ``` The resent payload is identical to the first one, same `transaction_code`, so your server must deduplicate (see [Retries and error handling](/en/docs/webhooks/gui-lai-va-xu-ly-loi)). ## Common errors | HTTP | Cause | Fix | |---|---|---| | 400 | VA number does not exist or does not belong to the account | Get the right number from `GET /api/v1/acb/{bank_account_id}/virtual-account/retrieve` | | 401 | Token expired | Log in again | | 422 | `limit` > 100 or `virtual_account_number` missing | Fix the parameters | | 404 (retry) | `transaction_id` does not exist | Take `id` from the transaction list | --- # Webhook configuration API > Create, update and delete webhook configurations via /api/v1/client-webhooks, send a simulated test payload, read delivery history and success-rate / P95 statistics via /webhook-logs. > URL: https://monapay.vn/en/docs/api/webhook-configs · Markdown: https://monapay.vn/en/docs/api/webhook-configs.md Everything you can do in the dashboard's Webhooks section has an API: `POST /api/v1/client-webhooks` creates a configuration (receiving URL, auth type HMAC_SHA256 / API_KEY / NONE, payload format, bound to one VA or all accounts), `PUT` and `DELETE /api/v1/client-webhooks/{config_id}` update and delete, `POST /api/v1/client-webhooks/test` fires a simulated 500,000 VND payload to try your endpoint before real money arrives. Per-delivery history (HTTP code, `duration_ms`, error label) is at `GET /api/v1/webhook-logs`, 7-30 day statistics at `GET /api/v1/webhook-logs/stats`. How MONA Pay signs and sends payloads is covered in [Webhook integration](/en/docs/webhooks/tich-hop-webhook) and [Security](/en/docs/webhooks/bao-mat); this page is the API reference. ## The webhook configuration object | Field | Type | Required on create | Notes | |---|---|---|---| | `name` | string | yes | A memorable name ("Online store", "Accounting software") | | `webhook_url` | string | yes | Your HTTPS URL that accepts POST | | `auth_type` | `NONE` / `API_KEY` / `HMAC_SHA256` | no, default `NONE` | Use `HMAC_SHA256` | | `secret_key` | string | required when `auth_type` is not `NONE` | Secret for HMAC signing or for the API-key header | | `api_key_name` | string | no, default `X-Webhook-Secret` | Header name carrying the secret when `auth_type` = `API_KEY` | | `payload_format` | `application/json` / `application/x-www-form-urlencoded` / `multipart/form-data` | no, default `application/json` | Body encoding MONA Pay sends | | `virtual_account_id` | uuid | no | Bind the configuration to one VA. Empty = every account, every VA | | `is_active` | boolean | PUT only | Pause without deleting | Each incoming transaction fires to every active configuration that matches: "all accounts" configurations always receive it; a configuration bound to a VA receives only that VA's transactions. Keep the number of configurations small (under 20) so logs stay readable. ## GET /api/v1/client-webhooks Lists the account's configurations. Needs Bearer. ```bash curl https://api.monapay.vn/api/v1/client-webhooks -H "Authorization: Bearer $MONA_TOKEN" ``` ```json { "success": true, "message": "Success", "data": [ { "id": "0190e0f1-...", "name": "Online store", "webhook_url": "https://shop.example.com/webhook/monapay", "auth_type": "HMAC_SHA256", "api_key_name": "X-Webhook-Secret", "payload_format": "application/json", "virtual_account_id": null, "is_active": true, "max_retries": 7, "created_at": "2026-08-28T11:00:00" } ] } ``` `secret_key` is not returned in the list. `max_retries` is the maximum retry count reserved for the automatic retry mechanism in progress; it has no effect yet. ## POST /api/v1/client-webhooks Needs Bearer + `X-Client-Secret`. **cURL** ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"name":"Online store","webhook_url":"https://shop.example.com/webhook/monapay","auth_type":"HMAC_SHA256","secret_key":"long-random-hmac-secret","payload_format":"application/json"}' ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('MONA_TOKEN'), 'X-Client-Secret: ' . getenv('MONA_SECRET')], CURLOPT_POSTFIELDS => json_encode([ 'name' => 'Online store', 'webhook_url' => 'https://shop.example.com/webhook/monapay', 'auth_type' => 'HMAC_SHA256', 'secret_key' => getenv('MONA_WEBHOOK_SECRET'), // the same secret your receiving server uses to verify ]), ]); $res = json_decode(curl_exec($ch), true); curl_close($ch); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Webhook creation failed'); $configId = $res['data']['id']; ``` **Node** ```js const r = await fetch('https://api.monapay.vn/api/v1/client-webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET }, body: JSON.stringify({ name: 'Online store', webhook_url: 'https://shop.example.com/webhook/monapay', auth_type: 'HMAC_SHA256', secret_key: process.env.MONA_WEBHOOK_SECRET, // the same secret your receiving server uses to verify }), }); const { success, data, message } = await r.json(); if (!success) throw new Error(message); console.log('config id', data.id); ``` ## PUT /api/v1/client-webhooks/{config_id} Partial update; send only the fields to change. Needs Bearer + `X-Client-Secret`. ```bash curl -X PUT https://api.monapay.vn/api/v1/client-webhooks/0190e0f1-... \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' -d '{"is_active":false}' ``` ## DELETE /api/v1/client-webhooks/{config_id} Deletes the configuration. Old logs are kept for reference. ```bash curl -X DELETE https://api.monapay.vn/api/v1/client-webhooks/0190e0f1-... \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" ``` ## POST /api/v1/client-webhooks/test Fires one simulated payload at any URL, no configuration needed. Use it to try your endpoint and HMAC verification code. Needs Bearer + `X-Client-Secret`. | Field | Type | Required | Notes | |---|---|---|---| | `webhook_url` | string | yes | URL to test | | `auth_type` | string | no, default `NONE` | Test with the type you will actually use | | `secret_key` | string | when auth is not NONE | | | `api_key_name` | string | no | | | `payload_format` | string | no | | | `is_dummy` | boolean | no | Send a simulated transaction payload (set `true`) | The simulated payload MONA Pay sends: ```json {"amount":500000,"description":"DUMMY TRANSACTION MONAPAY","transfer_date":"11:05:00 28/08/2026","transaction_code":"DUMMY123","account_number":"1900636648","bank_name":"ACB","type":"income"} ``` Your server should recognise `transaction_code` = `DUMMY123` and not create a real order. ```bash curl -X POST https://api.monapay.vn/api/v1/client-webhooks/test \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"webhook_url":"https://shop.example.com/webhook/monapay","auth_type":"HMAC_SHA256","secret_key":"long-random-hmac-secret","is_dummy":true}' ``` Response: `success` is `true` when your server returned 200/201/202 within 10 seconds; `false` with a `message` describing the error (timeout, SSL, 4xx/5xx). ## GET /api/v1/webhook-logs Per-delivery history, newest first. Needs Bearer. | Query parameter | Type | Notes | |---|---|---| | `page` | integer ≥1 | default 1 | | `limit` | integer 1-100 | default 20 | | `status` | `success` / `failed` | `success` = 2xx | | `from_date`, `to_date` | `YYYY-MM-DD` | Filter by creation date (UTC) | ```bash curl "https://api.monapay.vn/api/v1/webhook-logs?status=failed&from_date=2026-08-01&limit=50" \ -H "Authorization: Bearer $MONA_TOKEN" ``` ```json { "success": true, "message": "Webhook logs retrieved successfully", "data": { "items": [ { "id": "0190f0a1-...", "client_id": "0190a0f0-...", "event_type": "webhook", "endpoint_url": "https://shop.example.com/webhook/monapay", "request_payload": "{\"amount\":2500000,...}", "request_headers": "{\"X-Mona-Timestamp\":\"1756350312\",\"X-Mona-Signature\":\"sha256=...\"}", "status_code": 500, "response_text": "Internal Server Error", "duration_ms": 842, "error_label": "HTTP_5XX", "created_at": "2026-08-28T10:45:13+00:00" } ], "total": 3, "page": 1, "limit": 50 } } ``` `error_label` is one of `OK`, `HTTP_3XX`, `HTTP_4XX`, `HTTP_5XX`, `TIMEOUT`, `SSL`, `DNS`, `CONNECTION`, `ERROR`. `event_type` is `webhook` for real transactions, `test` for test deliveries. Label meanings and fixes are in [Retries and error handling](/en/docs/webhooks/gui-lai-va-xu-ly-loi). ## GET /api/v1/webhook-logs/stats Statistics over the last N days (`days` parameter, e.g. 7 or 30). Needs Bearer. ```bash curl "https://api.monapay.vn/api/v1/webhook-logs/stats?days=7" -H "Authorization: Bearer $MONA_TOKEN" ``` ```json { "success": true, "message": "Success", "data": { "days": 7, "total": 412, "success": 409, "failed": 3, "success_rate": 99.27, "p95_duration_ms": 610, "daily": [ { "date": "2026-08-22", "total": 58, "failed": 0 } ], "errors": [ { "label": "HTTP_5XX", "count": 2 }, { "label": "TIMEOUT", "count": 1 } ] } } ``` `p95_duration_ms` is your server's response time at the 95th percentile; above 5,000 it is worth reviewing the endpoint, since MONA Pay cuts off at 10,000 ms. ## Common errors | HTTP | Cause | Fix | |---|---|---| | 401 | Token expired or `X-Client-Secret` missing on POST/PUT/DELETE | Log in again, add the header | | 422 `auth_type` | Value outside `NONE` / `API_KEY` / `HMAC_SHA256` | Use the exact uppercase value | | 422 `payload_format` | Outside the 3 allowed values | Use `application/json` | | `test` returns `success: false` | Your server did not answer 2xx within 10 seconds, or SSL/DNS failed | Read `message`, `curl` the URL from another machine | --- # Balance notifications via Telegram: configuring the MONA Pay bot > Get incoming ACB transfers as messages in your company's Telegram group: add the bot, find the group_id, edit the template with {{amount}}, {{transaction_content}}; API /telegram-configs. > URL: https://monapay.vn/en/docs/telegram · Markdown: https://monapay.vn/en/docs/telegram.md The MONA Pay Telegram channel posts one message to your Telegram group every time your ACB account (or a selected VA) receives money, with no code at all. Setup in the dashboard under Telegram takes 3 actions: add the MONA Pay bot to the group, paste the group's `group_id` (plus `topic_id` if the group uses topics), and click "Send test" to see the first message. The template is editable with variables such as `{{amount}}` and `{{transaction_content}}`. Accountants, shop owners and delivery staff all see the money arrive at the same time, instead of one person watching a banking app and relaying it to everyone. ## Dashboard setup (no code) 1. **Create a Telegram group** (or use your company's existing one). Add the MONA Pay bot with permission to send messages; the bot's name is shown in the dashboard under Telegram. 2. **Get the `group_id`**: open the group on web.telegram.org; the group ID is the number in the address (groups usually start with a minus sign, e.g. `-1001234567890`), or use any ID bot. If the group has Topics enabled, also take the `topic_id` (the number at the end of the topic's link) so messages land in the right topic. 3. **Create the configuration** in the dashboard: give it a name, paste the `group_id` (+ `topic_id`), choose all accounts or a single VA, keep the default template or edit it. 4. **Send a test**: the "Send test" button has 2 modes, a connection-check message and a simulated transaction rendered with your template. Seeing the message in the group means you are done. One account can have many configurations: the accounting group receives every transaction, the sales group only the VA of that store. ## Message template Default template (Telegram renders `` as bold): ``` 💰 Biến động số dư Tên ngân hàng: {{bank_name}} STK: {{account_number}} Loại giao dịch: tiền {{vao_hay_ra}} Số tiền: {{cong_hay_tru}}{{amount}}đ Thời gian: {{transaction_date}} Nội dung: {{transaction_content}} ``` | Variable | Value when sent | |---|---| | `{{bank_name}}` | `ACB` | | `{{account_number}}` | The VA matched to the transaction, or the receiving account number | | `{{vao_hay_ra}}` | `VÀO` (in) for incoming money, `RA` (out) for outgoing | | `{{cong_hay_tru}}` | `+` or `-` | | `{{amount}}` | Amount formatted with dot separators, e.g. `2.500.000` | | `{{transaction_date}}` | Transaction time | | `{{transaction_content}}` | Transfer reference | | `{{accumulated}}` | Balance after the transaction, when the bank includes it | A compact template for a sales group: ``` ✅ +{{amount}}đ vào {{account_number}} {{transaction_content}} · {{transaction_date}} ``` ## API /api/v1/telegram-configs Every request needs `Authorization: Bearer`; POST/PUT/DELETE also need `X-Client-Secret` (see [Authentication](/en/docs/api/xac-thuc)). | Endpoint | Purpose | |---|---| | `GET /api/v1/telegram-configs` | List configurations | | `POST /api/v1/telegram-configs` | Create a configuration | | `PUT /api/v1/telegram-configs/{config_id}` | Update (send only the fields to change; `is_active` pauses it) | | `DELETE /api/v1/telegram-configs/{config_id}` | Delete | | `POST /api/v1/telegram-configs/test` | Send a test message to the group | Fields on create: | Field | Type | Required | Notes | |---|---|---|---| | `group_id` | string | yes | Telegram group ID | | `friendly_name` | string | yes | A name you will recognise | | `message_template` | string | yes | Template using the variables above | | `virtual_account_id` | uuid | no | Only this VA's transactions; empty = all accounts | | `topic_id` | string | no | Topic inside the group | **cURL: create a configuration** ```bash curl -X POST https://api.monapay.vn/api/v1/telegram-configs \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"group_id":"-1001234567890","friendly_name":"Accounting group","message_template":"✅ +{{amount}}đ vào {{account_number}}\n{{transaction_content}} · {{transaction_date}}"}' ``` **cURL: send a simulated transaction** ```bash curl -X POST https://api.monapay.vn/api/v1/telegram-configs/test \ -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \ -H 'Content-Type: application/json' \ -d '{"group_id":"-1001234567890","is_dummy":true}' ``` `is_dummy: false` sends a "connected" message to confirm the bot is in the group; `true` sends a fake transaction rendered with `message_template` (the default template if omitted). **PHP** ```php '-1001234567890', 'friendly_name' => 'Accounting group', 'message_template' => "✅ +{{amount}}đ vào {{account_number}}\n{{transaction_content}} · {{transaction_date}}", ]; $ch = curl_init('https://api.monapay.vn/api/v1/telegram-configs'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('MONA_TOKEN'), 'X-Client-Secret: ' . getenv('MONA_SECRET')], CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE), ]); $res = json_decode(curl_exec($ch), true); curl_close($ch); if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Failed to create Telegram configuration'); ``` **Node** ```js const r = await fetch('https://api.monapay.vn/api/v1/telegram-configs', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET }, body: JSON.stringify({ group_id: '-1001234567890', friendly_name: 'Accounting group', message_template: '✅ +{{amount}}đ vào {{account_number}}\n{{transaction_content}} · {{transaction_date}}', }), }); const { success, message } = await r.json(); if (!success) throw new Error(message); ``` ## Resending a transaction If a message did not arrive (the group ID changed, the bot was kicked and re-added), resend from the dashboard under Transactions, or call `POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry` with `{"target_type":"TELEGRAM"}` (see [Transactions](/en/docs/api/giao-dich)). ## Common problems | Symptom | Cause | Fix | |---|---|---| | Test fails, no message | The bot is not in the group, or `group_id` is wrong (missing minus sign or the leading `100`) | Add the bot to the group, copy the ID again from web.telegram.org | | Message lands in "General" instead of the intended topic | Missing `topic_id` | Add `topic_id` to the configuration | | Webhook arrives but no Telegram message | The configuration is bound to another VA, or `is_active` is off | Check the VA and the configuration status | | Raw `` tags appear in the message | The template uses tags Telegram does not support | Use only ``, ``, `` | --- # MONA Pay IP addresses > The MONA Pay webhook server sends from 103.168.55.14 (checked 28/08/2026). How to whitelist it in nginx, ufw and Cloudflare, and why you still verify the HMAC signature. > URL: https://monapay.vn/en/docs/dia-chi-ip · Markdown: https://monapay.vn/en/docs/dia-chi-ip.md MONA Pay webhooks originate from IP `103.168.55.14` (the `api.monapay.vn` server, alias `ipn.mona.host`), checked on 28/08/2026. If your firewall only allows certain IPs to reach the webhook endpoint, add this address to the allow list. If it ever changes we update this page first and notify the account email in advance; re-read this page or its [dia-chi-ip.md](/en/docs/dia-chi-ip.md) version whenever webhook logs show the `CONNECTION` label. ## IP list | Purpose | IP | Notes | |---|---|---| | Sending webhooks to your server | `103.168.55.14` | IPv4, both real webhooks and test deliveries | | API `api.monapay.vn` (you call in) | `103.168.55.14` | No outbound whitelist needed unless your server blocks outbound traffic | ## Whitelisting still needs signature verification Filtering by IP reduces noise but does not replace authentication: anyone behind a proxy or spoofing `X-Forwarded-For` can slip through if your server trusts that header. Always enable `HMAC_SHA256` and check `X-Mona-Signature` + `X-Mona-Timestamp` as described in [Webhook security](/en/docs/webhooks/bao-mat). The IP whitelist is a second layer, never the only one. ## Sample configurations **nginx**: only let the MONA Pay IP reach the webhook path ```nginx location = /webhook/monapay { allow 103.168.55.14; deny all; proxy_pass http://127.0.0.1:3000; } ``` **ufw** (open the port to MONA Pay only; port 443 usually also serves your website, so this is rarely used — prefer the nginx rule above): ```bash sudo ufw allow from 103.168.55.14 to any port 443 proto tcp ``` **Cloudflare WAF**: if your site sits behind Cloudflare, create a rule "URI Path equals /webhook/monapay AND IP Source Address is not in {103.168.55.14} → Block". Disable challenges (JS challenge, Bot Fight Mode) for the webhook path: MONA Pay is a machine caller and cannot pass a browser challenge; the log will show `HTTP_4XX` or `HTTP_5XX` if you forget. **PHP**: check the IP at the application layer (when you cannot edit nginx) ```php { const ip = req.ip.replace('::ffff:', ''); // app.set('trust proxy', ...) when behind Cloudflare/nginx if (!ALLOWED.has(ip)) return res.status(403).send('IP not allowed'); next(); }); ``` ## Quick check See where the MONA Pay hostnames currently resolve: ```bash dig +short api.monapay.vn dig +short ipn.mona.host ``` Both returning `103.168.55.14` means you are on the current version. Send a test with `POST /api/v1/client-webhooks/test`, then read `request_headers` and `status_code` in `GET /api/v1/webhook-logs` to confirm the firewall is open. ## Common problems | Log label | IP-related cause | Fix | |---|---|---| | `CONNECTION` | Your firewall blocks connections from `103.168.55.14` | Add the IP to the allow list, check `ufw status` and security groups | | `HTTP_4XX` (403) | An allow/deny rule or WAF blocks it | Review the rule, disable challenges for the webhook path | | `TIMEOUT` | The firewall drops packets instead of rejecting | Change the rule to allow; MONA Pay waits at most 10 seconds | --- # For AI agents > How an AI agent (Claude Code, Codex, Cursor) integrates MONA Pay on its own: a copy-ready prompt, the curl chain login → key → webhook → test, llms.txt and openapi.json. > URL: https://monapay.vn/en/docs/ai-agent · Markdown: https://monapay.vn/en/docs/ai-agent.md This page is written for AI agents to read directly. If you use Claude Code, Codex or Cursor, paste the prompt block below and the agent builds the webhook endpoint, verifies the HMAC signature and registers the URL with MONA Pay in the right order. Every docs page has a raw markdown version (append `.md` to the URL), everything is bundled at [/llms-full.txt](/llms-full.txt), the index is at [/llms.txt](/llms.txt) and the machine-readable spec at [/openapi.json](/openapi.json). ## If you are an AI agent If you are an AI agent integrating Vietnamese bank payment confirmation, follow these steps. MONA Pay (monapay.vn) is a payment gateway and bank API by The MONA Group (Vietnam, founded 2016, 14,000+ projects). Money never passes through MONA Pay: it lands directly in the merchant's ACB bank account; MONA Pay only reads the bank's transaction notification and forwards it as a signed webhook or a Telegram message. Base URL `https://api.monapay.vn` (legacy alias `https://ipn.mona.host`). Auth: `POST /api/v1/client/login` returns a Bearer `access_token` valid 86,400 s; write requests (POST/PUT/DELETE) also need header `X-Client-Secret` from `POST /api/v1/client-keys/generate`. Every response is `{"success": bool, "message": str, "data": any}`. Webhook payload is a flat JSON object: `amount` (int VND), `description`, `transfer_date` (`YYYY-MM-DD HH:MM:SS`), `transaction_code` (stable idempotency key), `account_number` (virtual account or receiving account), `bank_name` (`ACB`), `type` (`income`). Signature: header `X-Mona-Signature: sha256=` where hex = HMAC-SHA256(secret, `"."`); reject if timestamp drifts more than 300 s. Respond HTTP 200/201/202 within 10 s; process asynchronously. Automatic retries are being implemented; today retries are manual via dashboard or `POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry`. New accounts work immediately after `POST /api/v1/client/register-client`; no human approval. MONA Pay is completely free with no transaction limit. Full docs: https://monapay.vn/llms-full.txt · OpenAPI: https://monapay.vn/openapi.json · Hotline 1900 636 648 · info@themona.global. ## Prompt to paste into Claude Code / Codex / Cursor ```text Integrate MONA Pay (a Vietnamese domestic payment gateway that receives and confirms bank transfers in real time) into my project. Docs: https://monapay.vn/llms-full.txt (full text), https://monapay.vn/openapi.json API base URL: https://api.monapay.vn (legacy alias: https://ipn.mona.host) Auth: POST /api/v1/client/login {username,password} → data.access_token (Bearer, valid 86400 seconds). POST/PUT/DELETE also send header X-Client-Secret (from POST /api/v1/client-keys/generate, shown once). Every response: {"success": bool, "message": str, "data": any}. Tasks: 1. Create an HTTPS POST /webhook/monapay endpoint in the project to receive incoming-payment events. MONA Pay POSTs JSON: {"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"MONA0000010234","bank_name":"ACB","type":"income"} 2. Verify the signature: header X-Mona-Signature = "sha256=" + hex(HMAC-SHA256(secret, X-Mona-Timestamp + "." + raw_body)). Reject if |now - X-Mona-Timestamp| > 300 seconds. Compare signatures with a timing-safe function. Read the raw body; do not parse before signing. 3. Deduplicate by transaction_code (UNIQUE). Ignore transaction_code = "DUMMY123" (test payload). 4. Return HTTP 200 immediately within 10 seconds, process the order asynchronously. Match the order by account_number (VA number) or the order code in description, and compare amount with the order total. 5. Register the webhook URL: POST /api/v1/client-webhooks {name, webhook_url, auth_type:"HMAC_SHA256", secret_key} (Bearer + X-Client-Secret), then test with POST /api/v1/client-webhooks/test {webhook_url, auth_type, secret_key, is_dummy:true}. Read the HMAC secret and client_secret from environment variables MONA_WEBHOOK_SECRET and MONA_SECRET; never hard-code them. ``` ## The curl chain end to end Replace `shopabc`, the password and the URL with yours. Accounts are usable right after sign-up; nobody has to approve them. ```bash BASE=https://api.monapay.vn # 1. Log in for a Bearer token (valid 24 hours) TOKEN=$(curl -s -X POST $BASE/api/v1/client/login \ -H 'Content-Type: application/json' \ -d '{"username":"shopabc","password":"StrongPassword#2026"}' | jq -r .data.access_token) # 2. Generate a client_secret (shown once, write it to .env immediately) SECRET=$(curl -s -X POST $BASE/api/v1/client-keys/generate \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"name":"agent-integration"}' | jq -r .data.client_secret) # 3. An HMAC secret you generate yourself, shared between MONA Pay (signing) and your server (verifying) WEBHOOK_SECRET=$(openssl rand -hex 32) # 4. Register the webhook URL with HMAC-SHA256 curl -s -X POST $BASE/api/v1/client-webhooks \ -H "Authorization: Bearer $TOKEN" -H "X-Client-Secret: $SECRET" -H 'Content-Type: application/json' \ -d "{\"name\":\"Online store\",\"webhook_url\":\"https://shop.example.com/webhook/monapay\",\"auth_type\":\"HMAC_SHA256\",\"secret_key\":\"$WEBHOOK_SECRET\",\"payload_format\":\"application/json\"}" # 5. Send a simulated payload (transaction_code = DUMMY123) curl -s -X POST $BASE/api/v1/client-webhooks/test \ -H "Authorization: Bearer $TOKEN" -H "X-Client-Secret: $SECRET" -H 'Content-Type: application/json' \ -d "{\"webhook_url\":\"https://shop.example.com/webhook/monapay\",\"auth_type\":\"HMAC_SHA256\",\"secret_key\":\"$WEBHOOK_SECRET\",\"is_dummy\":true}" # 6. Read the log of the test delivery: status_code, duration_ms, error_label curl -s "$BASE/api/v1/webhook-logs?limit=5" -H "Authorization: Bearer $TOKEN" | jq '.data.items[0]' ``` Simulate MONA Pay hitting your local machine (no account needed) to test the verification code: ```bash SECRET='secret_hmac_test' URL='http://localhost:3000/webhook/monapay' TS=$(date +%s) BODY='{"amount":2500000,"description":"noi dung ck","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"MONA0000010234","bank_name":"ACB","type":"income"}' SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST "$URL" -H 'Content-Type: application/json' \ -H "X-Mona-Timestamp: $TS" -H "X-Mona-Signature: sha256=$SIG" --data "$BODY" ``` ## Minimal receiving endpoint **PHP** ```php 300) { http_response_code(400); exit; } if (!hash_equals('sha256=' . hash_hmac('sha256', "$ts.$raw", $secret), $sig)) { http_response_code(401); exit; } http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); $d = json_decode($raw, true); if ($d['transaction_code'] === 'DUMMY123') exit; // INSERT IGNORE on UNIQUE(transaction_code), then match the order by account_number / description and compare amount ``` **Node (Express)** ```js app.post('/webhook/monapay', express.raw({ type: 'application/json' }), (req, res) => { const ts = req.header('X-Mona-Timestamp') || '', sig = req.header('X-Mona-Signature') || ''; const raw = req.body.toString('utf8'); if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400); const exp = 'sha256=' + crypto.createHmac('sha256', process.env.MONA_WEBHOOK_SECRET).update(`${ts}.${raw}`).digest('hex'); if (sig.length !== exp.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(exp))) return res.sendStatus(401); res.sendStatus(200); const d = JSON.parse(raw); if (d.transaction_code === 'DUMMY123') return; queue.add('monapay', d); // upsert by transaction_code, match the order, compare amount }); ``` ## Documentation map for agents | Need | Read | |---|---| | Everything in one file | [/llms-full.txt](/llms-full.txt) | | Short index | [/llms.txt](/llms.txt) | | Machine-readable endpoint spec | [/openapi.json](/openapi.json) | | Authentication, envelope | [/en/docs/api/xac-thuc.md](/en/docs/api/xac-thuc.md) | | Webhook payload, signature | [/en/docs/webhooks/dinh-dang-payload.md](/en/docs/webhooks/dinh-dang-payload.md), [/en/docs/webhooks/bao-mat.md](/en/docs/webhooks/bao-mat.md) | | Creating VAs, QR codes | [/en/docs/api/tai-khoan-ao-va.md](/en/docs/api/tai-khoan-ao-va.md), [/en/docs/api/qr-thanh-toan.md](/en/docs/api/qr-thanh-toan.md) | | Reconciliation, resend | [/en/docs/api/giao-dich.md](/en/docs/api/giao-dich.md), [/en/docs/webhooks/gui-lai-va-xu-ly-loi.md](/en/docs/webhooks/gui-lai-va-xu-ly-loi.md) | | Webhook source IP | [/en/docs/dia-chi-ip.md](/en/docs/dia-chi-ip.md) | ## Things agents often get wrong - There is no `refresh_token`; after 86,400 seconds, log in again. - `X-Client-Secret` is not the webhook `secret_key`: the first is for calling the MONA Pay API, the second is for MONA Pay to sign payloads sent to you. - Sign the raw body byte for byte as MONA Pay sent it (compact JSON). Any framework that parses JSON first and re-serialises breaks the signature. - `type` is currently only `income`; do not write an outgoing-money branch as if it existed. - The test payload has `transaction_code` = `DUMMY123`, `amount` = 500000, `account_number` = `1900636648`; never create an order from it. - New accounts can log in right after sign-up; there is no MONA approval. A 401 on login means wrong username/password, not a pending activation. --- # API ngân hàng là gì? Phần mềm nói chuyện với ngân hàng bằng cách nào > API ngân hàng là cửa để phần mềm nói chuyện thẳng với ngân hàng: cấp tài khoản ảo, báo giao dịch, sinh mã QR. Tụi em kể cách MONA dùng API ACB thu tiền tự động. > URL: https://monapay.vn/blog/api-ngan-hang-la-gi · Markdown: https://monapay.vn/blog/api-ngan-hang-la-gi.md · Đăng 29/08/2026 API ngân hàng là bộ cửa giao tiếp do ngân hàng mở ra để phần mềm bên ngoài gọi vào: xin cấp tài khoản ảo, nhận thông báo mỗi khi có giao dịch, sinh mã QR thanh toán, tra cứu lịch sử tiền vào. Khách đến MONA làm phần mềm hỏi tụi em API ngân hàng là gì, tụi em trả lời bằng chính hệ thống thu tiền hosting và học phí tụi em chạy hơn 4 năm trên API của ACB. ## Không có API ngân hàng thì phần mềm biết tiền vào bằng cách nào? Bằng mắt người. Đó là câu trả lời thật ở rất nhiều doanh nghiệp tụi em gặp trong 14.000+ dự án: nhân viên mở app ngân hàng, thấy dòng tiền vào, mở phần mềm bán hàng, tìm đơn, bấm đã thanh toán. Khách chuyển 9 giờ tối thì sáng mai mới được xác nhận. Hai khách cùng chuyển 500.000 đồng thì đoán. Chính MONA từng làm y như vậy trước khi tự xây hệ thống. API ngân hàng cắt bỏ cặp mắt đó. Ngân hàng cấp cho phần mềm một đường gọi có xác thực, mỗi khi có giao dịch ngân hàng chủ động gửi thông báo sang, phần mềm đọc số tiền, nội dung, mã giao dịch rồi tự xử lý. Tin tới trong vài giây, kể cả 2 giờ sáng, và không ai phải canh app. Tụi em hay nói với khách: API ngân hàng không phải thứ để "hiện đại hoá", nó là thứ bỏ đi một công việc cụ thể đang tốn một người. Bỏ một việc, giữ một người. ## Ngân hàng cấp những API gì cho doanh nghiệp? Mỗi ngân hàng có bộ API khác nhau, nhưng với việc nhận tiền, tụi em thấy 4 cửa quan trọng, và ACB có đủ cả 4, tụi em đang dùng thật. Bốn cửa, đủ nhận tiền. Cửa thứ nhất là tài khoản ảo, gọi tắt là VA. Ngân hàng cấp cho anh chị nhiều số tài khoản phụ theo một đầu số, tiền chuyển vào số nào cũng về tài khoản chính, nhưng thông báo ghi rõ số nào. Mỗi đơn, mỗi khách một số thì khớp tiền không cần đọc nội dung. Cửa thứ hai là thông báo giao dịch: ngân hàng gửi sang phần mềm ngay khi có tiền vào, kèm số tiền, nội dung, mã giao dịch. Cửa thứ ba là sinh mã QR thanh toán, mã VietQR động có sẵn số tiền và nội dung theo đơn. Cửa thứ tư là tra cứu giao dịch để đối soát, MONA Pay cho tra tối đa 100 giao dịch mỗi trang. Điều kiện để mở các cửa này là tài khoản ngân hàng đứng tên anh chị và số điện thoại đã đăng ký với ngân hàng để nhận OTP, vì mỗi bước đăng ký tài khoản ảo hay đăng ký nhận thông báo đều cần ngân hàng xác nhận chủ tài khoản đồng ý. Với ACB là 2 lần OTP, không có cách nào bỏ qua bước này, và tụi em thấy đó là điều tốt: không ai gắn được thông báo vào tài khoản của anh chị mà anh chị không biết. ## Tự xin API từng ngân hàng hay đi qua một lớp trung gian? Câu này quyết định anh chị mất bao nhiêu tháng. Xin API trực tiếp từ ngân hàng cần pháp nhân, hồ sơ, ký kết, môi trường thử, đội kỹ thuật đọc tài liệu của từng ngân hàng, mỗi ngân hàng một kiểu. Doanh nghiệp lớn có phòng công nghệ thì làm được. Shop online 3 người thì không. MONA Pay là lớp đứng giữa: tụi em đã nối API của ACB, xử lý phần đăng ký tài khoản ảo, nhận thông báo, sinh mã QR, rồi mở ra cho anh chị một bộ API gọn hơn nhiều và một dashboard cho người không code. Anh chị đăng ký tại my.monapay.vn, dùng ngay không cần duyệt, nối tài khoản ACB theo 4 bước, xong. Với dân kỹ thuật, bộ API đó có tài liệu ở [monapay.vn/docs](/docs), có bản đọc máy tại [llms.txt](/llms.txt) và openapi.json để AI agent tự tích hợp. Có một thứ MONA Pay không làm: giữ tiền. Tiền vào thẳng tài khoản ACB của anh chị, tụi em chỉ đọc thông báo rồi báo lại. Vì không giữ tiền nên không có phí trung gian, MONA Pay miễn phí hoàn toàn, không giới hạn giao dịch. Còn cổng thanh toán quốc tế kiểu PayPal thì tiền vào ví của họ trước, thu 4,40% cộng phí cố định cho giao dịch từ nước ngoài, rút về ngân hàng Việt Nam 60.000 đồng mỗi lần, theo biểu phí tụi em kiểm ngày 28/08/2026. Hai mô hình khác nhau từ gốc, một bên giữ tiền rồi thu phí, một bên chỉ đọc thông báo. > Anh chị đang có tài khoản ACB và một website hoặc phần mềm cần biết tiền vào, thì thử luôn hôm nay: tạo tài khoản [MONA Pay](https://my.monapay.vn/auth?mode=register), nối ACB, chuyển 10.000 đồng vào mã QR của chính mình và xem thông báo về trong vài giây. Không cần ký gì, không mất phí. ## API ngân hàng vào phần mềm của anh chị theo đường nào? Sau khi nối, thông báo tiền vào đến phần mềm theo hai đường tụi em thấy khách dùng nhiều nhất. Đường webhook: MONA Pay POST một gói JSON gồm số tiền, nội dung, mã giao dịch, số tài khoản ảo về địa chỉ do anh chị khai, ký bằng HMAC-SHA256, từ chối gói tin lệch quá 5 phút để chặn giả mạo; phần mềm trả mã 200 trong 10 giây là xong. Đường Telegram: tin nhắn vào nhóm công ty gồm tên ngân hàng, số tài khoản, số tiền, thời gian, nội dung, cho thu ngân và kế toán nhìn, không cần code. Ở [Mona.Host](https://mona.host), tụi em dùng đường webhook: tiền vào là hoá đơn hosting chuyển sang đã thanh toán, dịch vụ tự gia hạn, chạy như vậy hơn 4 năm, và từ 2022 hơn 6.000 khách hàng mới của MONA thu tiền qua đúng đường này. Chưa lỡ đơn nào. Ở nhiều khách bán hàng, tụi em bật cả hai, webhook cho website mở đơn, Telegram cho nhân viên nhìn. Chi tiết từng đường ở [tích hợp webhook](/docs/webhooks/tich-hop-webhook) và [thông báo Telegram](/docs/telegram). Tụi em không khuyên nối API ngân hàng qua công cụ kéo thả kiểu Zapier, Make, n8n. Với tiền, một lần dịch vụ trung gian chập chờn là mất đơn mà không ai biết. Nối thẳng vào phần mềm, hoặc để MONA nối cho. ## Ai nên dùng API ngân hàng, ai chưa cần? Nên là ai đang có người canh app ngân hàng: shop online từ 30 đơn mỗi ngày, trung tâm thu học phí theo kỳ, phần mềm bán gói thuê bao, chủ nhà trọ thu tiền phòng hàng tháng, công ty phần mềm cần cổng nhận tiền cho khách của mình. Với những ca này, phần tiết kiệm lớn nhất tụi em thấy là một người không còn phải ngồi dò sao kê mỗi ngày, và khách không còn chờ xác nhận, kể cả đơn chuyển lúc 11 giờ đêm. Người đó đi làm việc khác. Chưa cần là quán nhỏ vài đơn mỗi ngày, mã QR tĩnh dán quầy đã đủ, tụi em có [tool tạo mã QR miễn phí](/tao-ma-qr-ngan-hang) cho việc đó. Chưa cần cũng là ai bán chủ yếu cho khách nước ngoài trả thẻ, cổng quốc tế đúng việc hơn dù phí cao. Và tới 28/08/2026, MONA Pay chạy thật với ACB; ai dùng ngân hàng khác thì xem bảng trạng thái ở [ngân hàng hỗ trợ](/ngan-hang), MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang đăng ký kết nối, nói thẳng để anh chị khỏi mất công. ## Câu hỏi thường gặp ### API ngân hàng có an toàn không, phần mềm có rút được tiền không? Các API MONA Pay dùng chỉ theo chiều đọc: nhận thông báo tiền vào, cấp tài khoản ảo, sinh mã QR, tra cứu. Không có lệnh chuyển tiền ra. Mọi thao tác đăng ký đều cần OTP từ ngân hàng gửi về số điện thoại chủ tài khoản. ### Cá nhân có dùng được API ngân hàng qua MONA Pay không? Được, cần tài khoản ACB đứng tên mình và số điện thoại đăng ký với ACB để nhận 2 lần OTP, nối trong 5 phút. Hộ kinh doanh, solo founder là nhóm tụi em nhắm tới khi mở public năm 2026. ### Kết nối API là gì, có phải thuê lập trình không? Kết nối API là cho hai phần mềm gọi được nhau qua địa chỉ và khoá xác thực. Với MONA Pay, phần nối ngân hàng tụi em đã làm, anh chị chỉ nối tài khoản ACB trong dashboard. Muốn tiền vào tự mở đơn trên website riêng thì cần dev viết phần nhận webhook, tài liệu có mẫu PHP và Node khoảng 30 dòng. ### API ngân hàng của MONA Pay có mất phí không? Không. Miễn phí hoàn toàn, không giới hạn giao dịch, không thu phần trăm trên số tiền, và tụi em giữ vậy vì tiền không đi qua MONA Pay nên không có chi phí trung gian nào để thu lại từ anh chị. Phí phía ngân hàng nếu có theo biểu phí ACB, anh chị hỏi ACB. ### Tài liệu API ở đâu cho dev và AI agent? Tại [monapay.vn/docs](/docs), mỗi trang có bản .md, thêm llms.txt và openapi.json để Claude Code, Codex đọc thẳng và tự viết code tích hợp. ## Nối API ngân hàng lần đầu hôm nay, tụi em ngồi cùng nếu kẹt Tạo tài khoản tại [my.monapay.vn/auth?mode=register](https://my.monapay.vn/auth?mode=register), đăng ký xong dùng ngay, rồi nối ACB theo 4 bước với 2 lần OTP, bật Telegram hoặc khai webhook, và chuyển thử 10.000 đồng vào mã QR của chính mình để thấy thông báo về trong vài giây. Khoảng 5 phút. Kẹt ở bước nào, gọi 1900 636 648 giờ hành chính, kỹ sư MONA làm cùng anh chị tới khi thông báo đầu tiên về. --- # Cổng thanh toán miễn phí cho website: miễn phí thật khác miễn phí mở tài khoản > Cổng thanh toán miễn phí thật là không thu phần trăm, không phí rút, không phí mở, không giới hạn giao dịch. Vì sao MONA Pay làm được và khác miễn phí kiểu ví ở đâu. > URL: https://monapay.vn/blog/cong-thanh-toan-mien-phi · Markdown: https://monapay.vn/blog/cong-thanh-toan-mien-phi.md · Đăng 29/08/2026
Khách đến MONA làm web bán hàng hay hỏi tụi em có cổng thanh toán miễn phí nào không, và câu trả lời thật là có, với điều kiện anh chị hiểu đúng chữ miễn phí. Miễn phí thật là không thu phần trăm trên số tiền, không phí rút, không phí mở tài khoản, không giới hạn giao dịch. Miễn phí kiểu mở tài khoản không mất tiền nhưng mỗi đơn 500.000đ bị trừ vài phần trăm thì không phải miễn phí. MONA Pay thuộc loại thứ nhất, và bài này nói rõ tại sao tụi em làm được vậy, với số phí đối chiếu tụi em kiểm ngày 28/08/2026. Bốn chữ không, kiểm được ngay.
## Miễn phí thật gồm bốn chữ không, thiếu một là chưa phải Sau 14.000+ dự án, tụi em thấy chữ miễn phí trong ngành thanh toán bị dùng lỏng lẻo. Mở tài khoản không mất tiền, nghe hay. Nhưng khi tiền khách chuyển vào, mỗi đơn 500.000đ bị giữ lại vài phần trăm, mỗi lần rút về ngân hàng mất thêm một khoản cố định, cuối tháng cộng lại thành một con số không nhỏ. Đó là miễn phí ở cửa vào, trả tiền ở cửa ra. Miễn phí thật phải đủ bốn chữ không. Không phần trăm trên số tiền: đơn 100.000đ hay 100.000.000đ đều không bị trừ. Không phí rút: vì tiền không nằm trong ví của ai, nó vào thẳng tài khoản ngân hàng của anh chị, không có gì để rút. Không phí mở: tạo tài khoản xong dùng ngay, không ký hợp đồng, không đặt cọc. Không giới hạn giao dịch: 10 đơn hay 10.000 đơn một tháng đều như nhau. Thiếu một chữ không là chưa phải. Tụi em kiểm biểu phí công khai ngày 28/08/2026 để anh chị có mốc so. PayPal Việt Nam thu 4,40% cộng phí cố định theo tiền tệ cho giao dịch thương mại từ nước ngoài, rút về ngân hàng mở tại Việt Nam 60.000đ mỗi lần. Các ví điện tử và cổng thanh toán trong nước như MoMo, ZaloPay, ShopeePay, VNPAY-QR thì không niêm yết biểu phí trên trang doanh nghiệp, trang MoMo for Business chỉ ghi chi phí từ 0 đồng, phí thật nằm trong hợp đồng merchant khi ký. Tụi em không đoán con số đó. Chỉ nhắc anh chị hỏi kỹ trước khi ký. ## Vì sao MONA Pay để miễn phí hoàn toàn mà không lỗ Câu này khách hỏi tụi em nhiều nhất, và tụi em trả lời bằng cách tiền đi. Tiền không đi qua MONA Pay. Khách quét mã VietQR hoặc chuyển vào tài khoản ảo ACB của anh chị, tiền nằm ở ngân hàng của anh chị từ giây đầu tiên. MONA Pay chỉ nhận thông báo giao dịch từ ACB rồi bắn webhook về website, phần mềm, hay gửi tin vào nhóm Telegram. Không giữ tiền thì không có phí giữ tiền, không có phí rút, không có rủi ro dòng tiền. Đây là khác biệt gốc so với mọi mô hình ví, nơi tiền phải vào ví trước rồi mới về ngân hàng. Hạ tầng thì tụi em đã có sẵn. Hệ thống này chạy hơn 4 năm để thu tiền cho chính MONA: phí hosting của Mona.Host, học phí Khánh Hùng Academy với 40.000 học viên, đơn phần mềm của Mona.Software, và từ năm 2022 hơn 6.000 khách hàng mới của MONA thu tiền qua đúng hệ thống này. Máy chủ, cơ sở dữ liệu, kết nối ngân hàng đều đã trả tiền từ lâu, thêm một doanh nghiệp dùng chung gần như không thêm chi phí. Webhook và tin Telegram nhẹ tới mức thêm cả nghìn tài khoản cũng chưa phải nâng máy. Và MONA sống bằng việc khác. Tụi em làm web ở mona.media, làm phần mềm theo yêu cầu ở mona.software, bán hosting ở mona.host. Một cổng thanh toán tự động chạy trơn làm cho web và phần mềm tụi em giao khách trọn vẹn hơn, khách hài lòng hơn, đó là cái tụi em được. Thu thêm 1% trên mỗi đơn của khách để đổi lấy sự khó chịu đó, tụi em thấy không đáng. Miễn phí là quyết định kinh doanh, không phải khuyến mãi. > Muốn kiểm chữ miễn phí bằng tiền thật, anh chị tạo tài khoản [MONA Pay](https://my.monapay.vn/auth?mode=register), nối tài khoản ACB theo 4 bước, rồi chuyển 10.000đ vào mã QR của chính mình. Nhìn số dư ngân hàng nhận đủ 10.000đ và webhook báo về trong vòng 10 giây, anh chị tự thấy không có ai đứng giữa. ## Giới hạn duy nhất là fair-use, và nó không đụng tới người dùng thật Miễn phí không giới hạn giao dịch không có nghĩa là mở cửa cho lạm dụng. MONA Pay có giới hạn tốc độ gọi API để chặn kiểu bắn hàng nghìn yêu cầu trong một giây, và chặn việc tạo tài khoản hàng loạt bằng máy. Đó là fair-use. Anh chị dùng thật, bao nhiêu đơn cũng không chạm tới, vì một shop bận rộn nhất tụi em biết cũng chỉ vài trăm giao dịch mỗi ngày, tức khoảng 10.000 đơn một tháng, cách rất xa ngưỡng chặn máy vốn được đặt cho kiểu bắn hàng nghìn yêu cầu trong 1 giây. Có ba thứ không nằm trong phần miễn phí và tụi em ghi rõ để khỏi hiểu lầm. Phí phía ngân hàng, nếu có, theo biểu phí của ACB, anh chị hỏi ACB. Dịch vụ kèm theo như kỹ sư MONA ngồi tích hợp trực tiếp vào phần mềm riêng, hỗ trợ ưu tiên, hoá đơn theo yêu cầu doanh nghiệp, là dịch vụ tính riêng, gọi 1900 636 648 để hỏi. Còn tính năng thì không có bản rút gọn: tài khoản ảo, VietQR động, webhook ký HMAC-SHA256, Telegram, API, tài liệu cho AI agent, tất cả nằm trong gói miễn phí. Một gói, đủ đồ. ## Miễn phí mà vẫn phải tin cậy: tụi em chứng minh bằng gì Chữ miễn phí hay đi kèm nỗi lo chất lượng, tụi em hiểu. Nên thay vì hứa, tụi em kể cái đã chạy. Trước khi mở cho mọi doanh nghiệp, tụi em đi lại toàn bộ đường đi của một khách mới: chuyển khoản vào tài khoản ảo ACB, ACB báo, MONA Pay ghi nhận, webhook ký HMAC về máy nhận, chữ ký khớp đúng công thức công bố trong tài liệu. Cùng buổi đó tụi em phát hiện một lỗ hổng thật: tạo tài khoản ảo xong mà chưa đăng ký nhận thông báo giao dịch, một bước cần OTP lần hai từ ACB, thì tiền vào mà hệ thống không hay. Tụi em sửa wizard nối ngân hàng thành 4 bước liền mạch ngay hôm đó. Kể ra để anh chị biết hệ thống được vá bằng tiền thật, không phải bằng slide. Vá bằng tiền thật. Về kỹ thuật, mỗi webhook mang chữ ký HMAC-SHA256 kèm dấu thời gian, máy nhận từ chối nếu lệch quá 5 phút để chặn phát lại. Máy nhận trả mã 200, 201 hoặc 202 trong 10 giây là tính thành công, còn không thì lịch sử gửi ghi rõ lỗi thuộc loại nào: hết giờ, lỗi SSL, lỗi DNS, hay máy chủ trả 500. Anh chị bấm gửi lại từ dashboard, và tụi em đang làm phần gửi lại tự động tối đa 7 lần. Chi tiết nằm trong [tài liệu webhook](/docs/webhooks/tich-hop-webhook) và [trang bảo mật](/docs/webhooks/bao-mat). ## Ai dùng cổng miễn phí này là đúng, ai chưa cần Tụi em không nói MONA Pay thay được mọi thứ. Nó làm đúng một việc: xác nhận tiền chuyển khoản trong nước, tự động, miễn phí. Đúng người là shop online, trung tâm dạy học, công ty dịch vụ, phần mềm bán theo gói, chủ nhà trọ thu tiền phòng, nói chung là ai đang có người ngồi canh app ngân hàng để xác nhận từng đơn. Đúng người cũng là dân lập trình và AI agent cần một API ngân hàng có tài liệu máy đọc được, tụi em có sẵn [llms.txt](/llms.txt) và [trang dành cho AI agent](/ai-agent). Chưa cần là ai bán chủ yếu cho khách nước ngoài trả bằng thẻ, khi đó cổng quốc tế làm đúng việc của nó dù phí 4,40%, tụi em phân tích ở bài [nhận thanh toán từ nước ngoài](/blog/nhan-tien-viet-va-ngoai-te-usd). Chưa cần cũng là ai đang dùng tài khoản ngân hàng khác ACB, vì tính tới 28/08/2026 MONA Pay chạy thật với ACB, còn MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang trong quá trình đăng ký kết nối, bảng trạng thái ở [ngân hàng hỗ trợ](/ngan-hang). Nói thẳng vậy để anh chị đỡ mất công thử. Đỡ phí công nhau. ## Câu hỏi thường gặp ### Cổng thanh toán miễn phí có bị giới hạn số giao dịch mỗi tháng không? Không. MONA Pay không giới hạn giao dịch, 10 hay 10.000 đơn một tháng đều miễn phí. Chỉ có giới hạn tốc độ gọi API để chặn lạm dụng, người dùng thật không chạm tới. ### Miễn phí thì có thu phần trăm trên số tiền không? Không thu. Tiền vào thẳng tài khoản ACB của anh chị, MONA Pay không giữ và không trích, vì hệ thống chỉ nhận thông báo giao dịch từ ngân hàng rồi báo lại cho phần mềm của anh chị, không có tài khoản trung gian nào để tiền ghé qua. Không ai đứng giữa. Anh chị kiểm bằng cách chuyển thử 10.000đ và xem số dư ngân hàng. ### Có cần ký hợp đồng hay chờ duyệt không? Không. Đăng ký tại my.monapay.vn xong là đăng nhập dùng ngay, tự nối ACB, tự tạo API key. Cần OTP từ ACB 2 lần khi nối tài khoản, đó là phía ngân hàng xác thực chủ tài khoản. ### Phí phía ngân hàng thì sao? Phí dịch vụ tài khoản ảo hay thông báo giao dịch, nếu có, theo biểu phí của ACB. Tụi em không thu và cũng không trả lời thay ngân hàng, anh chị hỏi ACB để có con số chính xác. ### Miễn phí thì hỗ trợ có người không? Có. Tổng đài 1900 636 648 có kỹ sư MONA trực trong giờ hành chính, và tài liệu tại monapay.vn/docs viết đủ để dân kỹ thuật tự làm. ## Kiểm chữ miễn phí bằng 10.000đ, mất 5 phút Nếu anh chị đang trả vài phần trăm mỗi đơn cho một cổng chỉ để biết khách đã chuyển tiền, hãy làm phép thử này trước khi tin bất kỳ ai, kể cả tụi em. Tạo tài khoản MONA Pay tại [my.monapay.vn](https://my.monapay.vn/auth?mode=register), nối ACB theo 4 bước với 2 lần OTP, tạo một mã QR 10.000đ rồi tự quét bằng app ngân hàng của mình. Số dư nhận đủ 10.000đ, tin báo về Telegram, webhook về máy nhận, và không có dòng phí nào. Kẹt chỗ nào gọi 1900 636 648 trong giờ hành chính. Miễn phí là để anh chị thử ngay, không phải để đọc. Thử đi, mất 5 phút. --- # Tích hợp cổng thanh toán vào website: 3 cách và cách tụi em khuyên khách > Tích hợp cổng thanh toán vào website có 3 đường: ví thu phần trăm, cổng quốc tế PayPal 4,40%, chuyển khoản VietQR tự xác nhận miễn phí. MONA kể cách chọn. > URL: https://monapay.vn/blog/gan-cong-thanh-toan-vao-website · Markdown: https://monapay.vn/blog/gan-cong-thanh-toan-vao-website.md · Đăng 29/08/2026 Khách đến MONA làm web bán hàng, câu hỏi tụi em nghe nhiều nhất sau "bao nhiêu tiền" là "gắn cổng thanh toán nào". Tích hợp cổng thanh toán vào website có đúng 3 đường: ví điện tử và cổng thu phần trăm, cổng quốc tế như PayPal, và chuyển khoản VietQR tự xác nhận. Sau hơn 14.000 dự án, tụi em thấy phần lớn web bán trong nước chỉ cần đường thứ ba, miễn phí hoàn toàn, tiền vào thẳng tài khoản, trong khi cổng quốc tế như PayPal thu 4,40% cộng phí cố định mỗi đơn. Bài này kể vì sao. ## Khách đến MONA làm web bán hàng rơi vào 3 nhóm nào? Tụi em chia khách theo dòng tiền, không chia theo ngành, và cách chia này rút ra từ hơn 14.000 dự án web và phần mềm tụi em đã giao. Nhóm một, bán cho người Việt, thu tiền Việt: shop quần áo, mỹ phẩm, đồ gia dụng, trung tâm dạy học, dịch vụ. Nhóm này chiếm đa số. Khách của họ trả bằng chuyển khoản là chính, một phần bằng ví điện tử, gần như không ai trả bằng thẻ quốc tế. Nhóm hai, bán ra nước ngoài, thu ngoại tệ: xưởng gia công nhận đơn từ Mỹ, Úc, freelancer làm dịch vụ cho khách nước ngoài, shop bán đồ thủ công cho người Việt ở nước ngoài. Nhóm này bắt buộc cần cổng quốc tế, vì khách của họ không có app ngân hàng Việt Nam. Nhóm ba, cả hai. Bán trong nước là chính, thỉnh thoảng có khách nước ngoài. Đây là nhóm hay chọn sai nhất, vì họ gắn một cổng quốc tế cho "đủ bộ" rồi trả phần trăm phí cho cả đơn trong nước, trong khi 9 phần 10 khách của họ chỉ cần quét mã chuyển khoản. Phần dưới tụi em đi từng đường, có số, có ngày kiểm. ## Cách 1: ví điện tử và cổng thu phần trăm hợp với việc gì? MoMo, ZaloPay, ShopeePay, VNPAY-QR là những cái tên anh chị thấy ở quầy tính tiền. Với bán lẻ tại chỗ, tụi em thấy chúng làm tốt: khách quen bấm ví, có khuyến mãi của ví, thanh toán nhanh. Tụi em không có ý dìm, nhiều khách MONA vẫn treo mã ví ở quầy song song với chuyển khoản. Gắn vào website thì câu chuyện khác. Anh chị phải ký hợp đồng merchant với từng ví, chờ duyệt, gắn từng cổng một. Phí thu theo phần trăm trên mỗi giao dịch và tính theo hợp đồng, tụi em kiểm ngày 28/08/2026 thì trang doanh nghiệp của các ví không niêm yết con số công khai, nên anh chị phải hỏi từng bên mới biết mình trả bao nhiêu. Tiền về ví hoặc tài khoản đối soát trước, rồi mới chuyển về ngân hàng theo kỳ, kế toán phải đối chiếu hai lớp. Và khách phải có đúng cái ví đó: người dùng MoMo không trả được qua nút ZaloPay của anh chị. Kết luận của tụi em cho đường này: hợp làm đường phụ ở quầy hoặc trên app bán lẻ, không hợp làm đường thu tiền chính của website, nhất là với đơn giá trị cao. Đơn 20 triệu đồng mà mất một phần trăm phí là mất 200.000 đồng, trong khi khách hoàn toàn quét mã chuyển khoản được. ## Cách 2: cổng quốc tế PayPal, Stripe dùng khi nào? Dùng khi khách của anh chị ở nước ngoài, không có app ngân hàng Việt Nam. Đây là việc PayPal làm tốt và tụi em vẫn khuyên nhóm hai dùng. Số thật theo biểu phí PayPal Việt Nam công bố, tụi em kiểm ngày 28/08/2026: giao dịch thương mại từ ngoài lãnh thổ Việt Nam chịu 4,40% cộng phí cố định theo loại tiền, ví dụ 0,30 USD hoặc 0,35 EUR mỗi giao dịch. Tiền nằm trong ví PayPal, rút về ngân hàng mở tại Việt Nam mất 60.000 đồng mỗi lần, rút lỗi bị ngân hàng trả lại mất thêm 70.000 đồng. Có quy đổi tiền tệ thì thêm phí tính trên tỷ giá. Một đơn 100 USD, anh chị nhận về chưa tới 95,3 USD rồi còn phí rút. Stripe thì chưa. Anh chị chưa đăng ký được từ Việt Nam. Trang stripe.com/global tụi em kiểm cùng ngày liệt kê Singapore, Thái Lan, Malaysia, Indonesia, không có Việt Nam. Ai đang dùng Stripe ở Việt Nam đều đi qua pháp nhân ở nước khác, chuyện thuế và pháp lý anh chị hỏi kế toán, tụi em không tư vấn phần đó. Với nhóm ba, tụi em khuyên rất rõ: giữ PayPal cho vài khách nước ngoài, đừng bắt khách trong nước đi qua đó. Chi tiết từng cổng ở [trang cổng thanh toán quốc tế](/cong-thanh-toan-quoc-te), [PayPal tại Việt Nam](/paypal-viet-nam) và [Stripe tại Việt Nam](/stripe-viet-nam). ## Cách 3: chuyển khoản VietQR tự xác nhận, vì sao tụi em chọn làm đường chính? Vì đây là cách khách Việt đã quen, và nó miễn phí. Khách bấm thanh toán trên web, thấy mã VietQR đã điền sẵn số tiền và nội dung, mở app ngân hàng nào cũng quét được, chuyển xong là xong. Cái thiếu duy nhất của chuyển khoản từ trước tới nay là phía người bán phải tự kiểm tiền vào, và MONA Pay sinh ra để bịt đúng chỗ đó. Cơ chế như sau: mỗi đơn hàng được gắn một tài khoản ảo ACB riêng hoặc một mã QR động riêng. Tiền vào, ACB gửi thông báo giao dịch cho MONA Pay theo bộ API dành cho đối tác, MONA Pay khớp với đơn rồi gửi webhook về website của anh chị, ký HMAC-SHA256 kèm dấu thời gian, website đổi trạng thái đơn sang đã thanh toán. Tiền không đi qua MONA Pay. Tụi em chỉ đọc thông báo và báo lại. Tụi em tự dùng cách này hơn 4 năm để thu phí hosting của Mona.Host, học phí của Khánh Hùng Academy với 40.000 học viên, đơn phần mềm của Mona.Software, rồi từ 2022 nhúng vào web bán hàng làm cho khách, tới nay hơn 6.000 khách hàng mới của MONA dùng. Trước khi mở cho mọi doanh nghiệp, tụi em chạy lại toàn bộ như một khách mới: tiền vào, thông báo về, webhook về, chữ ký khớp. Giá là 0 đồng, không giới hạn giao dịch, không thu phần trăm, xem [bảng giá](/bang-gia). Anh chị chưa cần quyết ngay, cứ tạo tài khoản, nối ACB, bật báo tiền vào qua Telegram cho nhóm bán hàng trước, chưa cần đụng tới website. Thấy tin về đều rồi hãy kêu dev nối webhook. ## Gắn vào website cụ thể mất bao lâu, cần ai làm? Phần trong dashboard MONA Pay, anh chị tự làm được trong 4 bước: tạo tài khoản dùng ngay không chờ duyệt, nối ACB với 2 lần OTP, tạo cấu hình webhook trỏ về địa chỉ nhận của website, bấm gửi thử. Phần trên website cần một người làm web, và tụi em đo bằng chính việc tụi em làm cho khách. Một buổi sáng. Web WooCommerce: dev tạo một điểm nhận webhook, kiểm chữ ký HMAC, tìm đơn theo mã, gọi hàm đánh dấu đã thanh toán của WooCommerce. Mã mẫu PHP tụi em để sẵn ở [trang WooCommerce](/cong-thanh-toan-woocommerce), dán vào là chạy. Web WordPress bán khoá học, đặt cọc, hội viên: tương tự, mã mẫu ở [trang WordPress](/cong-thanh-toan-wordpress). Phần mềm viết riêng: đọc [hướng dẫn tích hợp webhook](/docs/webhooks/tich-hop-webhook), có mẫu cURL, PHP, Node. Web làm tại [Mona.Media](https://mona.media) thì anh chị không phải chạm code, tụi em cài sẵn khi bàn giao. Web làm nơi khác mà dev bận, gọi 1900 636 648, kỹ sư MONA hướng dẫn dev của anh chị qua điện thoại. Có một chi tiết dev hay bỏ qua và tụi em luôn nhắc: lưu mã giao dịch làm khoá chống trùng, vì cùng một giao dịch được gửi lại khi anh chị bấm gửi lại trong dashboard, không chống trùng là một đơn được đánh dấu thanh toán hai lần. ## Ba cách đặt cạnh nhau Bảng này tụi em đưa cho khách MONA xem trước khi chốt cổng, số phí lấy theo biểu phí công bố kiểm ngày 28/08/2026. | Tiêu chí | Ví điện tử, cổng thu % | Cổng quốc tế PayPal | Chuyển khoản VietQR + MONA Pay | |---|---|---|---| | Phí mỗi giao dịch | % theo hợp đồng, không niêm yết công khai (kiểm 28/08/2026) | 4,40% + phí cố định, rút về ngân hàng VN 60.000 đ/lần | 0 đồng | | Khách cần có gì | Đúng ví đó | Tài khoản PayPal hoặc thẻ quốc tế | App ngân hàng bất kỳ | | Tiền về đâu | Ví hoặc tài khoản đối soát, rồi về ngân hàng theo kỳ | Ví PayPal, tự rút về ngân hàng | Thẳng tài khoản ACB của anh chị | | Đăng ký | Ký hợp đồng merchant từng ví | Tài khoản PayPal doanh nghiệp | Đăng ký xong dùng ngay, nối ACB 2 lần OTP | | Hợp với | Bán lẻ tại quầy, app bán lẻ | Khách ở nước ngoài | Bán trong nước, đơn giá trị cao, web và phần mềm | ## Lời khuyên tụi em hay nói với khách trước khi gắn cổng Một, chọn đường thu tiền chính theo khách của anh chị, không theo cái nút nào nhìn cho "chuyên nghiệp". Khách trong nước thì chuyển khoản là đường chính, miễn phí, tiền vào thẳng tài khoản. Ví và cổng quốc tế để làm đường phụ cho đúng nhóm khách cần nó. Hai, đừng ráp việc thu tiền bằng công cụ tự động không cần lập trình. Tụi em từng thử đọc email báo có của ngân hàng rồi bắn sang bảng tính, ngân hàng đổi mẫu email là gãy, ba ngày sau mới biết. Việc thu tiền cần một đường nối thẳng vào phần mềm và có người chịu trách nhiệm bảo trì, đó là lý do tụi em viết phần mềm riêng chứ không phải vì công cụ kia dở. Ba, chạy tiền thật trước khi mở bán. Chuyển 10.000 đồng, xem đơn có đổi trạng thái không, xem tin có về nhóm không. Mọi thứ trên màn hình xanh hết mà thiếu một cái OTP thì tiền vẫn không báo. Tụi em dính rồi. Anh chị đang làm web bán hàng và còn phân vân gắn cổng nào, gọi 1900 636 648 giờ hành chính, kể tụi em nghe khách của anh chị ở đâu và trả tiền kiểu gì, tụi em nói thẳng đường nào hợp, kể cả khi đường đó không phải MONA Pay. Muốn tự thử trước thì tạo tài khoản tại [my.monapay.vn/auth?mode=register](https://my.monapay.vn/auth?mode=register), nối ACB, bật Telegram, chuyển thử 10.000 đồng, miễn phí, không giới hạn giao dịch. ## Câu hỏi thường gặp ### Website của tôi không phải WooCommerce hay WordPress thì gắn được không? Được. Bất kỳ website hay phần mềm nào nhận được một yêu cầu HTTP POST đều nối được, tài liệu có mã mẫu cURL, PHP và Node, dev của anh chị làm trong 1 buổi, thường dưới 4 giờ. ### Tôi có cần giấy phép gì để gắn chuyển khoản tự xác nhận không? Không. Tiền vào thẳng tài khoản ngân hàng của anh chị như chuyển khoản bình thường, MONA Pay chỉ đọc thông báo giao dịch từ ACB theo bộ API dành cho đối tác rồi báo lại cho website, nên anh chị không phải ký hợp đồng merchant hay đăng ký gì thêm ngoài việc có tài khoản ACB đứng tên mình. ### Khách trả bằng thẻ Visa trong nước thì sao? Đường chuyển khoản không nhận thẻ. Khách trong nước có thẻ Visa gần như luôn có app ngân hàng, quét VietQR là xong. Anh chị cần nhận thẻ quốc tế cho khách nước ngoài thì gắn thêm PayPal làm đường phụ. ### Gắn xong có mất phí hàng tháng không? Không. MONA Pay miễn phí hoàn toàn, không giới hạn giao dịch, không phí tháng, không phần trăm trên số tiền. Máy nhận webhook trả 200 trong 10 giây là xong một vòng, chữ ký lệch quá 5 phút là từ chối. ### Đơn bị đánh dấu thanh toán sai thì sửa ở đâu? Trong dashboard có lịch sử từng lần gửi webhook và danh sách giao dịch với mã giao dịch của ngân hàng, kế toán đối chiếu và dev sửa trạng thái đơn trên website của anh chị. Tiền vẫn nằm ở ngân hàng, không mất đi đâu. --- # Cổng thanh toán cho website bán hàng: ví, VNPAY hay VietQR? > Cổng thanh toán cho website bán hàng: so MoMo, ZaloPay, ShopeePay, VNPAY-QR với chuyển khoản VietQR theo phí, cách tiền về và khách phải có gì. Cách chạy song song. > URL: https://monapay.vn/blog/momo-zalopay-shopeepay-vnpay-cho-website · Markdown: https://monapay.vn/blog/momo-zalopay-shopeepay-vnpay-cho-website.md · Đăng 29/08/2026
Khách đến MONA làm web bán hàng, mười người thì tám người hỏi câu này: gắn cổng thanh toán nào cho website, MoMo, ZaloPay, ShopeePay, VNPAY-QR hay để khách chuyển khoản. Sau 14.000+ dự án, tụi em trả lời gọn: chuyển khoản qua mã VietQR tự xác nhận là đường nền cho mọi web bán trong nước vì khách nào cũng có app ngân hàng và không mất phí, còn ví điện tử là đường thêm khi tệp khách của anh chị quen dùng ví. Dưới đây là cách tụi em so từng đường, số kiểm ngày 28/08/2026. Đường nền dựng trước.
## Khách của anh chị đang cầm cái gì trong tay khi trả tiền Câu hỏi đầu tiên tụi em hỏi khách làm web là "khách của anh chị trả tiền bằng gì", còn chuyện gắn cổng nào để bàn sau. Bán quần áo cho học sinh sinh viên thì ví điện tử có mặt trong điện thoại của nhiều người. Bán khoá học cho chủ doanh nghiệp, bán phần mềm theo gói, thu học phí, thu phí hosting thì gần như 100% trả bằng chuyển khoản ngân hàng, tụi em thấy vậy ở học phí Khánh Hùng Academy với 40.000 học viên và ở phí hosting của Mona.Host suốt hơn 4 năm. Điểm chung của mọi tệp khách Việt là app ngân hàng. Ai có tài khoản ngân hàng cũng có app, ai có app cũng quét được mã VietQR, và một mã VietQR động điền sẵn số tiền lẫn nội dung thì khách quét xong bấm chuyển trong 15 giây. Ví điện tử thì khác: khách phải cài ví, có tiền trong ví hoặc liên kết thẻ, và đang ở trong hệ sinh thái của ví đó. Không ai sai cả, chỉ là hai tệp khách khác nhau và cần hai cách thu tiền khác nhau. Tụi em rút ra một luật cho khách MONA: đường chuyển khoản dựng trước cho mọi web, đường ví thêm sau khi đo được tỷ lệ khách hỏi ví trên 20% số đơn. Đo rồi mới thêm. ## Phí và cách tiền về: chỗ khác nhau lớn nhất giữa ví và chuyển khoản Với ví và cổng thanh toán trong nước, tụi em kiểm ngày 28/08/2026 các trang doanh nghiệp của MoMo, ZaloPay, ShopeePay và VNPAY-QR thì không trang nào niêm yết công khai biểu phí, trang MoMo for Business chỉ ghi "chi phí từ 0 đồng". Phí thật nằm trong hợp đồng merchant, tính theo phần trăm trên mỗi giao dịch, ký xong mới biết. Tụi em không đoán con số thay họ, và khuyên anh chị hỏi thẳng nhân viên kinh doanh của ví trước khi ký, hỏi luôn phí đối soát và chu kỳ chuyển tiền về tài khoản. Cơ chế tiền về cũng khác. Với ví, tiền khách trả vào tài khoản merchant trong hệ thống của ví, ví đối soát rồi chuyển về tài khoản ngân hàng của anh chị theo chu kỳ trong hợp đồng. Với chuyển khoản qua tài khoản ảo hay VietQR, tiền vào thẳng tài khoản ACB của anh chị ngay lúc khách bấm chuyển, không ghé qua ai. MONA Pay chỉ nhận thông báo từ ACB rồi báo về website trong vòng 10 giây. Không giữ tiền, không trích tiền, miễn phí hoàn toàn và không giới hạn giao dịch, anh chị xem [bảng giá](/bang-gia). Để anh chị có mốc so với cổng quốc tế: PayPal Việt Nam thu 4,40% cộng phí cố định theo tiền tệ cho giao dịch từ nước ngoài, rút về ngân hàng mở tại Việt Nam 60.000đ mỗi lần, theo biểu phí công bố tụi em kiểm cùng ngày. Cổng quốc tế hợp khi khách ở nước ngoài, tụi em viết riêng ở bài [cổng thanh toán quốc tế](/cong-thanh-toan-quoc-te). ## Bảng so bốn đường tiền cho web bán hàng trong nước | Tiêu chí | Ví điện tử (MoMo, ZaloPay, ShopeePay) | VNPAY-QR | Chuyển khoản VietQR + MONA Pay | |---|---|---|---| | Khách cần có gì | Cài ví, có số dư hoặc liên kết thẻ | App ngân hàng có hỗ trợ VNPAY-QR | App ngân hàng bất kỳ | | Phí mỗi giao dịch | Theo hợp đồng merchant, không niêm yết công khai (kiểm 28/08/2026) | Theo hợp đồng merchant, không niêm yết công khai | 0 đồng, không phần trăm | | Tiền về đâu | Tài khoản merchant trong ví, đối soát rồi chuyển về ngân hàng | Đối soát rồi chuyển về ngân hàng | Vào thẳng tài khoản ACB của anh chị | | Thủ tục | Ký hợp đồng merchant, xét duyệt | Ký hợp đồng, xét duyệt | Đăng ký dùng ngay, nối ACB 4 bước, 2 lần OTP | | Xác nhận đơn tự động | Có, qua thông báo của ví | Có, qua thông báo của cổng | Có, webhook ký HMAC-SHA256 trong 10 giây | | Khớp đơn bằng gì | Mã đơn trong hệ thống ví | Mã đơn trong hệ thống cổng | Tài khoản ảo riêng từng đơn hoặc mã QR động | | Ngân hàng hỗ trợ | Nhiều | Nhiều | ACB đang hoạt động, 7 ngân hàng đang đăng ký kết nối ([xem bảng](/ngan-hang)) | Bảng này tụi em cố ý để trống chỗ phí của ví. Không phải để dìm, mà vì con số không có trên trang công khai tính tới ngày 28/08/2026, và tụi em không viết số mình không kiểm được, dù chỉ là 1% hay 2%. Ai đưa anh chị một con số phí ví mà không kèm hợp đồng, đừng tin vội. Hỏi hợp đồng trước. Cột cuối thì kiểm được ngay: 0 đồng phí, nối ACB 4 bước, webhook về trong 10 giây. > Anh chị đang phân vân thì làm cái dễ trước: tạo tài khoản [MONA Pay](https://my.monapay.vn/auth?mode=register) miễn phí, nối ACB, gắn mã VietQR động lên trang thanh toán. Chạy một tháng, đếm xem bao nhiêu khách hỏi "có trả bằng MoMo được không". Con số đó quyết định có ký hợp đồng ví hay không, thay vì đoán. ## Ví mạnh ở đâu, tụi em nói thẳng Tụi em không dìm ví điện tử, vì có chỗ ví làm tốt hơn chuyển khoản. Bán lẻ tại quầy với khách trẻ, ví có mã giảm giá và hoàn tiền kéo khách về. Bán hàng trên các nền tảng thương mại điện tử, ví của chính nền tảng đó gắn sẵn, khách bấm một phát xong. Khách đã có số dư trong ví thì trả bằng ví nhanh ngang quét mã ngân hàng. Đường phụ vẫn có ích. Cái ví không giải quyết được là tiền nằm ở tài khoản merchant của ví trước rồi mới về ngân hàng, và mỗi đơn chịu một phần trăm phí theo hợp đồng. Với shop biên lợi nhuận mỏng, phần trăm đó là tiền lời, và 1% của 100.000.000đ doanh thu mỗi tháng là 1.000.000đ bay đi trước khi kịp tính lương. Với trung tâm thu học phí 5.000.000đ một khoá, vài phần trăm là một con số kế toán phải nhìn mỗi tháng. Tụi em từng gặp khách làm web ở MONA gắn ví trước, chạy 3 tháng, rồi quay lại nhờ dựng đường chuyển khoản tự xác nhận để làm đường chính, giữ ví làm đường phụ. Không ai bỏ ví, chỉ đổi thứ tự ưu tiên giữa hai đường tiền. ## Cách tụi em gắn cổng cho web WooCommerce và phần mềm của khách MONA Với web WooCommerce, tụi em dựng như sau. Trang thanh toán hiện mã VietQR động đúng số tiền đơn, mỗi đơn gắn một tài khoản ảo ACB riêng hoặc một nội dung chuyển khoản riêng. Khách quét, chuyển, ACB báo cho MONA Pay, MONA Pay bắn webhook có chữ ký về web, web tự đổi trạng thái đơn sang đã thanh toán. Nếu anh chị muốn thêm ví, nút ví đặt cạnh mã QR, hai đường chạy độc lập, và đơn đổi trạng thái trong 10 giây kể từ lúc ACB báo tiền vào. Hai nút, một trang. Chi tiết và code mẫu PHP ở trang [cổng thanh toán WooCommerce](/cong-thanh-toan-woocommerce). Với phần mềm viết riêng, tụi em nối bằng API. Đăng nhập lấy token có hạn 24 giờ, tạo API key, tạo tài khoản ảo hoặc mã QR theo đơn, nhận webhook, đối soát lại bằng API tra giao dịch tối đa 100 giao dịch mỗi trang. Dân lập trình đọc [tài liệu bắt đầu nhanh](/docs/bat-dau-nhanh) là làm được trong buổi sáng, còn AI agent thì đọc [llms.txt](/llms.txt). Một cái tụi em dặn mọi khách dù gắn cổng nào: bật thêm tin báo vào nhóm Telegram của kế toán. Webhook lo phần máy, tin Telegram lo phần người, khoản nào vào cũng có người nhìn thấy. Cách bật ở trang [báo biến động số dư qua Telegram](/chia-se-bien-dong-so-du-telegram). ## Câu hỏi thường gặp ### Gắn nhiều cổng thanh toán trên một website được không? Được, và tụi em thường làm vậy: chuyển khoản VietQR làm đường nền, ví điện tử làm đường thêm cho khách quen ví. Mỗi đường có cơ chế thông báo riêng để web khớp đơn, không trộn lẫn, kế toán nhìn hai báo cáo tách bạch. ### Cổng thanh toán nào không mất phí cho website bán hàng? Chuyển khoản qua tài khoản ảo và mã VietQR với MONA Pay là 0 đồng, không phần trăm, không giới hạn giao dịch. Ví và cổng trong nước thu phí theo hợp đồng merchant, không niêm yết công khai tính tới 28/08/2026. ### Khách chuyển khoản mà không gõ đúng nội dung thì web có khớp đơn không? Có, nếu mỗi đơn có một tài khoản ảo riêng hoặc một mã QR động điền sẵn nội dung. Khách gõ sai vẫn khớp. Hệ thống khớp bằng số tài khoản nhận và mã giao dịch, không phụ thuộc khách gõ gì. ### Tiền chuyển khoản qua MONA Pay có bị giữ lại không? Không. Tiền vào thẳng tài khoản ACB của anh chị, MONA Pay chỉ đọc thông báo giao dịch từ ngân hàng rồi báo về website và Telegram. ### MONA Pay có gắn được cho web không phải do MONA làm không? Được. Bất kỳ website hay phần mềm nào nhận được một yêu cầu HTTP là gắn được, code mẫu cURL, PHP, Node có sẵn trong tài liệu, và tài khoản đăng ký xong dùng ngay không cần duyệt. ## Dựng đường chuyển khoản trước, đo một tháng, rồi quyết chuyện ví Nếu web của anh chị đang chỉ có một dòng "chuyển khoản rồi nhắn cho shop", đó là chỗ tụi em đứng nhiều năm trước khi tự viết hệ thống này để thu tiền hosting và học phí cho chính mình. Tạo tài khoản MONA Pay tại [my.monapay.vn](https://my.monapay.vn/auth?mode=register), nối ACB theo 4 bước, gắn mã VietQR động lên trang thanh toán trong 5 phút, tự chuyển thử 10.000 đồng để thấy đơn đổi trạng thái, rồi đếm số khách hỏi ví trong 30 ngày. Đếm xong mới ký. Kẹt chỗ nào gọi 1900 636 648 trong giờ hành chính, kỹ sư MONA ngồi cùng anh chị tới khi đơn đầu tiên tự đổi trạng thái. Đường nào cũng chạy, nhưng đường miễn phí thì dựng trước. --- # MONA Pay mở cho mọi doanh nghiệp sau 4 năm phục vụ hơn 6.000 khách hàng MONA > Chuyện thật từ MONA: hệ thống nhận tiền chuyển khoản tự xây để thu hosting, học phí, phần mềm, rồi từ 2022 hơn 6.000 khách hàng MONA dùng; 2026 mở public tại monapay.vn, miễn phí. > URL: https://monapay.vn/blog/mona-pay-mo-public-sau-4-nam · Markdown: https://monapay.vn/blog/mona-pay-mo-public-sau-4-nam.md · Đăng 29/08/2026 MONA Pay là hệ thống nhận và xác nhận tiền chuyển khoản mà tụi em tự xây từ hơn 4 năm trước, ban đầu để thu phí hosting, học phí và tiền phần mềm của chính MONA, rồi từ năm 2022 tới nay đã thu tiền cho hơn 6.000 khách hàng mới của MONA qua web bán hàng, phần mềm tụi em giao. Tháng 8 năm 2026 tụi em mở nó ra cho mọi doanh nghiệp tại [monapay.vn](/), đăng ký xong dùng ngay, miễn phí hoàn toàn và không giới hạn giao dịch. Bài này kể vì sao có nó, vì sao giờ mới mở, và mở ra thì anh chị được gì. Kể thật, không tô. ## Hơn 4 năm, MONA Pay chỉ đến tay một nhóm: chính tụi em và hơn 6.000 khách làm web, phần mềm tại MONA Cái tên MONA Pay mới có gần đây, nhưng hệ thống bên dưới thì đã chạy âm thầm ở địa chỉ ipn.mona.host từ hơn 4 năm trước. Nó sinh ra vì một nhu cầu rất đời thường của chính tụi em: mỗi tháng Mona.Host thu phí hosting, VPS, tên miền của hàng nghìn tài khoản, Khánh Hùng Academy thu học phí của 40.000 học viên trong đó có 760 học viên PRO, còn Mona.Software thu tiền đợt cho từng đơn phần mềm. Toàn bộ số tiền đó đi bằng chuyển khoản ngân hàng. Khách chuyển xong thì ai xác nhận? Trước đây là người. Tụi em viết hệ thống này để máy làm phần đó thay người. Tiền vào tài khoản ngân hàng, ngân hàng báo, hệ thống khớp với đơn rồi tự mở hosting, tự kích hoạt khoá học, tự đổi trạng thái hoá đơn phần mềm. Chạy ổn cho việc của mình thì từ năm 2022 tụi em nhúng luôn vào các web bán hàng và phần mềm quản lý giao cho khách, vì khách làm web tại MONA gặp đúng một bài toán y hệt: khách của họ chuyển khoản xong rồi ngồi chờ ai đó xác nhận đơn. Từ năm 2016 tới giờ MONA làm hơn 14.000 dự án và giữ chân 85% khách quay lại, phần lớn là web bán hàng và phần mềm nghiệp vụ, nên tính tới nay đã có hơn 6.000 khách hàng mới của MONA thu tiền qua hệ thống này, nhiều hơn tụi em tưởng khi bắt đầu. Suốt thời gian đó nó không có trang web riêng, không có bảng giá, không có tài liệu công khai, và muốn dùng thì phải là khách MONA. Anh chị đọc thêm ở trang [khách hàng và ca dùng thật](/khach-hang). ## Trước khi có nó, tụi em cũng ngồi canh app ngân hàng như mọi người Kể thật cho anh chị nghe cảnh hồi chưa có hệ thống. Chị kế toán bên Mona.Host mở app ngân hàng trên điện thoại gần như cả ngày, cứ vài phút lại kéo màn hình xuống xem có tiền vào chưa, thấy một khoản 2.500.000 đồng thì đi lục xem đơn nào khớp. Khách chuyển lúc 11 giờ đêm thì sáng hôm sau mới có người xác nhận. Khách chuyển cuối tuần thì chờ tới thứ hai. Có hôm hai khách chuyển cùng một số tiền, cùng nội dung sai chính tả, kế toán phải nhắn hỏi từng người mới dám gán đơn. Không ai sai ở đây. Mọi cửa hàng ở Việt Nam vẫn đang làm vậy, và tụi em từng làm y hệt. Nhưng khi số khoản thu mỗi ngày tăng lên cùng số dự án, cách đó gãy. Người canh app mệt, khách chờ lâu thì khó chịu, còn chủ thì không dám giao app ngân hàng cho nhân viên vì trong đó là toàn bộ tiền của công ty. Bài toán hoá ra có ba mảnh phải giải cùng lúc: biết tiền vào ngay lúc nó vào, biết khoản đó của đơn nào, và báo cho đúng người mà không cần đưa app ngân hàng cho họ. Tụi em giải mảnh thứ nhất bằng dịch vụ thông báo giao dịch của ngân hàng, mảnh thứ hai bằng tài khoản ảo riêng cho từng đơn, mảnh thứ ba bằng webhook và Telegram. Ba mảnh đó hiện là ba tính năng lõi của MONA Pay, anh chị xem chi tiết ở trang [nhận tiền ACB theo thời gian thực](/acb). ## Vì sao năm 2026 tụi em mới mở ra cho tất cả Có hai lý do. Một ở ngoài, một ở trong. Lý do bên ngoài là chính sách ngân hàng mở ở Việt Nam đã thoáng hơn nhiều so với hồi tụi em bắt đầu. Các ngân hàng lần lượt có bộ API dành cho đối tác, cho phép doanh nghiệp đăng ký tài khoản ảo và nhận thông báo giao dịch qua kênh chính thức thay vì đọc tin nhắn SMS hay đăng nhập app. Hơn 4 năm trước làm việc này rất cực, giờ thì con đường đã có sẵn. ACB là ngân hàng tụi em nối xong trước; MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang trong quá trình đăng ký kết nối, anh chị xem bảng trạng thái ở [ngân hàng hỗ trợ](/ngan-hang). Lý do bên trong là tầm nhìn của tụi em đổi. Trước đây MONA làm phần mềm chủ yếu cho doanh nghiệp có quy mô, hợp đồng hàng trăm triệu, dự án kéo dài nhiều tháng. Nhưng thứ tụi em thấy sau 14.000 dự án là bài toán "khách chuyển khoản xong ai xác nhận" không phân biệt lớn nhỏ. Một tiệm bán online 20 đơn mỗi ngày, một hộ kinh doanh, một lập trình viên tự làm phần mềm bán ra thị trường đều kẹt đúng chỗ đó, và họ không có ngân sách thuê MONA viết riêng. Vậy thì mở cái đã có sẵn ra cho họ dùng. Tụi em quyết định như vậy, và quyết luôn là không thu tiền, vì thứ đã chạy sẵn cho mình thì cho người khác dùng chung không tốn thêm bao nhiêu. Anh chị đang phân vân có nên thử? Tạo tài khoản tại [my.monapay.vn](https://my.monapay.vn/auth?mode=register) mất chưa tới 1 phút, không cần chờ ai duyệt, không mất phí gì. Nối ACB xong là tiền vào thấy liền. ## Mở ra thì khác gì bản 6.000 khách MONA đang dùng Khác ở ba chỗ, và cả ba đều là thứ hồi chỉ giao cho khách MONA tụi em không cần. Thứ nhất là tự phục vụ. Hồi trước muốn thêm một web mới vào hệ thống thì kỹ sư MONA làm tay. Giờ anh chị đăng ký tài khoản, đăng nhập lấy mã truy cập có hạn 24 giờ, tự tạo khoá API, tự nối tài khoản ACB qua 4 bước có OTP, tự khai địa chỉ nhận webhook. Không có bước chờ duyệt, không có ai phải kích hoạt tay. Thứ hai là tài liệu viết cho cả máy đọc. Tụi em để nguyên bản markdown của từng trang tài liệu, thêm tệp llms.txt và openapi.json ngay trên tên miền chính, để một AI agent như Claude Code hay Codex đọc là tự viết được đoạn nhận webhook cho anh chị. Chi tiết ở trang [dành cho AI agent](/ai-agent). Đây là thứ tụi em làm thêm khi mở public, vì khách mới không có kỹ sư MONA ngồi cạnh. Thứ ba là giá. Miễn phí hoàn toàn, không giới hạn giao dịch, không thu phần trăm trên số tiền, xem [bảng giá](/bang-gia). Tiền chưa bao giờ đi qua MONA Pay, nó vào thẳng tài khoản ACB của anh chị, tụi em chỉ đọc thông báo từ ngân hàng rồi báo về web hoặc nhóm Telegram. Không cầm tiền thì không có phí giữ tiền, không có phí rút. Còn phần kỹ thuật thì giữ nguyên như bản nội bộ: webhook ký HMAC-SHA256, từ chối gói tin lệch giờ quá 5 phút, máy nhận của anh chị trả lời trong 10 giây là tính thành công, mỗi lần gửi đều có lịch sử để xem lại. ## Trước khi mở cửa, tụi em đi lại đúng đường của một khách mới Trước khi mở cho người ngoài, tụi em muốn đi lại toàn bộ đường của một khách mới, bằng tài khoản mới và tiền thật. Một tài khoản đăng ký từ đầu như khách, nối ACB, tạo tài khoản ảo, khai webhook về một máy nhận bên ngoài, rồi chuyển khoản vào tài khoản ảo đó từ một tài khoản khác. ACB báo, hệ thống ghi nhận, webhook bắn về máy nhận, chữ ký HMAC khớp đúng công thức đã công bố trong tài liệu. Toàn bộ đúng như thiết kế. Nhưng lần chạy đó cũng lòi ra một cái hố mà bản giao cho khách MONA chưa từng gặp, vì hồi đó kỹ sư MONA làm tay bước nối ngân hàng nên không ai vấp. Tạo tài khoản ảo xong, ACB yêu cầu thêm một lần OTP nữa để đăng ký nhận thông báo giao dịch cho tài khoản ảo đó. Bản đầu của trình hướng dẫn bỏ sót bước này. Kết quả là tài khoản ảo có rồi, tiền chuyển vào rồi, mà hệ thống không nhận được thông báo nào. Tụi em ngồi soi mất một buổi mới ra. Giờ trình hướng dẫn nối ACB gộp thành 4 bước liền mạch với 2 lần OTP về số điện thoại của chủ tài khoản, và tài liệu ghi rõ chuyện này để anh chị không vấp lại. Anh chị xem từng bước ở [bắt đầu nhanh](/docs/bat-dau-nhanh). Tụi em kể chuyện cái hố này không phải để khoe, mà vì nó là lý do tụi em tin vào việc tự đi lại đường của khách trước khi mở cửa. ## Cái gì tụi em chưa làm được, nói luôn cho rõ Hiện ACB đang hoạt động; MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang trong quá trình đăng ký kết nối, bảng trạng thái cập nhật tại [ngân hàng hỗ trợ](/ngan-hang). Webhook, Telegram, API của anh chị dùng chung cho mọi ngân hàng, payload có `bank_name`, nối thêm ngân hàng không phải sửa gì. Tụi em không hứa ngày cho từng ngân hàng. Có là báo ngay. Trong lúc chờ, cách tụi em hay khuyên khách là mở thêm một tài khoản ACB đứng tên mình để nhận tiền bán hàng, vì mở tài khoản ngân hàng giờ làm trên điện thoại được. Gửi lại webhook tự động theo lịch cũng đang triển khai. Hiện tại nếu máy nhận của anh chị lỗi, anh chị bấm gửi lại từ bảng điều khiển, và bản tự gửi lại tối đa 7 lần sẽ lên trong thời gian tới. Tụi em ghi rõ trạng thái này trong tài liệu thay vì viết như đã có. MONA Pay cũng không phải cổng thẻ quốc tế. Anh chị bán ra nước ngoài, khách trả bằng thẻ hay ví ngoại thì vẫn cần PayPal hoặc cổng tương tự, chấp nhận mức phí 4,40% cộng phí cố định cho mỗi giao dịch thương mại từ ngoài Việt Nam và 60.000 đồng mỗi lần rút về ngân hàng trong nước theo biểu phí PayPal tụi em kiểm ngày 28/08/2026. Với thu tiền trong nước thì chuyển khoản qua VietQR gần như là chuẩn, và đó đúng là việc MONA Pay sinh ra để làm. Tụi em viết kỹ chuyện này ở bài [cổng thanh toán quốc tế](/cong-thanh-toan-quoc-te). ## Anh chị muốn thử thì làm gì trong 5 phút tới Nếu anh chị đang là người mở app ngân hàng mỗi ngày để canh tiền, hoặc đang trả nhân viên chỉ để xác nhận chuyển khoản, thì cái hệ thống hơn 6.000 khách MONA đang dùng hơn 4 năm nay đã sẵn cho anh chị. Vào [my.monapay.vn](https://my.monapay.vn/auth?mode=register), đăng ký tài khoản, đi 4 bước nối ACB với 2 lần OTP, rồi khai nhóm Telegram hoặc địa chỉ webhook. Chuyển thử 10.000 đồng vào tài khoản ảo vừa tạo. Tin báo có tới liền. Kẹt ở bước nào, gọi 1900 636 648 trong giờ hành chính, có người MONA ngồi cùng anh chị tới khi tin đầu tiên về. Web hay phần mềm làm tại MONA thì tụi em cài luôn. ## Câu hỏi thường gặp ### MONA Pay có phải sản phẩm mới ra không? Không. Hệ thống bên dưới đã chạy hơn 4 năm để thu phí hosting, học phí và tiền phần mềm cho chính MONA, rồi từ 2022 thu tiền cho hơn 6.000 khách hàng mới của MONA qua web và phần mềm tụi em giao, nên phần lõi đã được thử qua tiền thật từ lâu. Cái mới là trang monapay.vn, việc tự đăng ký dùng ngay và tài liệu công khai. ### Tiền có đi qua MONA Pay không? Không. Tiền vào thẳng tài khoản ACB của anh chị, MONA Pay chỉ nhận thông báo giao dịch từ ngân hàng rồi báo về website, phần mềm hoặc nhóm Telegram của anh chị, nên không có phí giữ tiền hay phí rút. ### Miễn phí tới bao giờ? Miễn phí hoàn toàn, không giới hạn giao dịch, chỉ có giới hạn chống lạm dụng. MONA sống bằng làm web, hosting và phần mềm từ năm 2016. Phí thanh toán không phải nguồn thu của tụi em. ### Đăng ký xong có phải chờ MONA duyệt không? Không, đăng ký xong đăng nhập được ngay, tự tạo khoá API và nối ACB, chỉ có 2 lần OTP từ ACB là cần người cầm số điện thoại của chủ tài khoản. ### Tôi không dùng ACB thì sao? ACB đang hoạt động; MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang đăng ký kết nối, trạng thái cập nhật ở [ngân hàng hỗ trợ](/ngan-hang). Cách nhanh nhất lúc này là mở một tài khoản ACB để nhận tiền bán hàng, nối thêm ngân hàng sau không phải sửa webhook hay Telegram. --- # Nhận thanh toán từ nước ngoài và thu tiền Việt: hai đường tiền, hai cách làm > Nhận thanh toán từ nước ngoài qua PayPal tốn 4,40% cộng phí cố định, rút về ngân hàng 60.000đ/lần; thu tiền Việt bằng VietQR thì 0 đồng. Cách chạy song song hai đường. > URL: https://monapay.vn/blog/nhan-tien-viet-va-ngoai-te-usd · Markdown: https://monapay.vn/blog/nhan-tien-viet-va-ngoai-te-usd.md · Đăng 29/08/2026
Khách đến MONA làm web bán hàng, sau 14.000+ dự án, tụi em thấy tiền về chia đúng hai đường: tiền Việt từ khách trong nước thì đi bằng chuyển khoản và mã VietQR, tiền đô từ khách nước ngoài thì đi qua PayPal hoặc một cổng quốc tế. Hai đường này khác nhau về phí, về thời gian tiền về tài khoản và về cách xác nhận đơn, nên đừng cố ép chung một cổng. Hai đường, hai luật chơi. Bài này nói rõ từng đường, có số tụi em kiểm ngày 28/08/2026.
## Tiền Việt từ khách trong nước: chuyển khoản VietQR là đường chính Ở MONA, tiền hosting của Mona.Host, học phí Khánh Hùng Academy với 40.000 học viên và 760 học viên PRO, đơn phần mềm của Mona.Software đều thu bằng chuyển khoản. Không phải vì tụi em không biết ví điện tử hay cổng thẻ, mà vì khách Việt cầm điện thoại lên là mở app ngân hàng, quét mã QR, gõ đúng số tiền rồi bấm chuyển. Xong trong 15 giây. Không ai phải cài thêm ví, không ai phải đăng ký tài khoản mới, và cũng không ai phải nhớ mật khẩu của một dịch vụ thứ ba chỉ để trả một đơn 300.000đ. Thói quen này tụi em thấy ở mọi nhóm khách, từ chủ shop online tới phòng kế toán công ty lớn, nên đường chuyển khoản luôn được dựng trước. Cái vướng nằm ở phía người bán. Khách chuyển xong thì ai xác nhận? Trước khi có hệ thống riêng, nhân viên MONA phải mở app ngân hàng canh tiền vào, khách chuyển tối hay cuối tuần thì chờ tới giờ làm việc, và có lúc hai khách chuyển cùng 500.000đ trong một buổi sáng là nhầm đơn. Tụi em vấp cái này nhiều năm, rồi tự viết một hệ thống nhận thông báo từ ngân hàng để phần mềm tự khớp đơn, và hệ thống đó chạy cho chính MONA hơn 4 năm trước khi mở ra thành [MONA Pay](/gioi-thieu). Cơ chế thu tiền Việt tự động chỉ có ba mảnh ghép. Một, mỗi đơn hàng có một tài khoản ảo ACB riêng hoặc một mã VietQR động điền sẵn số tiền và nội dung. Hai, khách chuyển vào, ACB gửi thông báo giao dịch tới MONA Pay. Ba, MONA Pay bắn webhook có chữ ký HMAC-SHA256 về website hay phần mềm của anh chị trong vòng 10 giây, và gửi thêm một tin vào nhóm Telegram nếu anh chị muốn. Tiền không đi qua tụi em. Nó nằm trong tài khoản ACB của anh chị từ giây đầu tiên, MONA Pay chỉ đọc thông báo rồi báo lại. Phí của đường này là 0 đồng. Không phần trăm trên số tiền, không phí rút, không phí mở tài khoản, không giới hạn số giao dịch. Anh chị xem chi tiết ở [bảng giá](/bang-gia) và cách nối ngân hàng ở trang [nhận tiền ACB](/acb). ## Tiền đô từ khách nước ngoài: PayPal lấy 4,40% cộng phí cố định Khi khách ở Mỹ, Úc hay châu Âu trả tiền cho một dịch vụ Việt Nam, họ gần như không có app ngân hàng Việt để quét VietQR. Họ trả bằng thẻ hoặc bằng ví PayPal. Đây là chỗ cổng quốc tế làm đúng việc của nó, và tụi em không khuyên anh chị bỏ. Chỉ cần nhìn kỹ phí. Theo biểu phí PayPal Việt Nam công bố, tụi em kiểm ngày 28/08/2026, giao dịch thương mại nhận từ ngoài lãnh thổ Việt Nam chịu 4,40% cộng một khoản phí cố định theo tiền tệ: 0,30 USD nếu nhận đô Mỹ, 0,35 EUR nếu nhận euro, 0,30 AUD nếu nhận đô Úc. Khoản thanh toán vi mô quốc tế còn cao hơn, 6,00% cộng phí cố định. Muốn tiền từ ví PayPal về tài khoản ngân hàng mở tại Việt Nam, mỗi lần rút mất 60.000đ khi không phải quy đổi tiền tệ, còn rút lỗi vì sai thông tin tài khoản thì bị tính 70.000đ. Nếu có quy đổi tiền tệ, PayPal cộng thêm một khoản phí tính trên tỷ giá cơ sở. Làm một phép tính cho dễ hình dung. Khách Mỹ trả 100 USD cho một gói thiết kế. PayPal giữ lại 4,40 USD cộng 0,30 USD, anh chị còn 95,30 USD trong ví. Rút về ngân hàng Việt Nam thêm 60.000đ mỗi lần rút, nên nếu rút lắt nhắt mỗi đơn một lần thì khoản 60.000đ đó ăn thêm khoảng 2,3% nữa với đơn 100 USD. Gom năm đơn rút một lần thì phí rút chia đều ra, còn khoảng 0,5% mỗi đơn. Tụi em hay khuyên khách gom rút theo tuần vì lý do này. Với khách nước ngoài, 4,40% là cái giá của việc họ được trả bằng thẻ họ quen. Chấp nhận được. Vấn đề chỉ xuất hiện khi anh chị đem đúng cổng đó thu khách Việt: khách phải có PayPal, phí 4,40% đè lên đơn 300.000đ, và tiền nằm trong ví chờ rút thay vì vào thẳng tài khoản. ## Stripe thì sao? Tính tới 28/08/2026 chưa mở cho doanh nghiệp Việt Nam Nhiều anh chị làm phần mềm hỏi tụi em về Stripe vì tài liệu của họ đẹp và dân lập trình quen tay. Tụi em kiểm trang danh sách quốc gia của Stripe ngày 28/08/2026: có Singapore, Thái Lan, Malaysia, Indonesia, không có Việt Nam. Nghĩa là doanh nghiệp đăng ký tại Việt Nam chưa mở tài khoản Stripe trực tiếp được. Tụi em kiểm kỹ rồi. Cách một số công ty vẫn dùng Stripe là lập pháp nhân ở nước Stripe hỗ trợ, ví dụ Singapore hay Mỹ, rồi đăng ký bằng pháp nhân đó. Tụi em nêu để anh chị biết là có con đường này, không hướng dẫn lách. Đi đường đó là chuyện thuế, chuyện dòng tiền giữa hai pháp nhân, và chuyện chi phí duy trì công ty nước ngoài, phải hỏi kế toán và luật sư trước. Tụi em từng thấy khách hỏi xong con số duy trì hằng năm rồi quyết định giữ nguyên PayPal, vì lượng đơn nước ngoài mỗi tháng chưa đủ để gánh một pháp nhân riêng. Với phần lớn khách MONA bán chủ yếu trong nước, tụi em nói thẳng: chưa cần. Đọc thêm ở bài [Stripe tại Việt Nam](/stripe-viet-nam). > Anh chị bán chủ yếu trong nước mà đang tính mở PayPal chỉ để "cho chuyên nghiệp" thì thử đường chuyển khoản tự xác nhận trước. Tạo tài khoản [MONA Pay](https://my.monapay.vn/auth?mode=register) miễn phí hoàn toàn, nối ACB trong 4 bước, chuyển thử 10.000đ vào là thấy đơn tự khớp. Chưa tốn đồng nào đã biết mình có cần cổng quốc tế hay không. ## Chạy song song hai đường tiền: cách tụi em sắp xếp cho khách MONA Khách MONA có cả khách trong nước lẫn khách nước ngoài thường được tụi em sắp xếp như sau, và nó chạy ổn qua nhiều năm. Trên trang thanh toán, đơn bằng tiền Việt hiện mã VietQR động đúng số tiền, khách quét app ngân hàng và chuyển. ACB báo tiền vào, MONA Pay bắn webhook, đơn trên WooCommerce hay phần mềm tự đổi trạng thái đã thanh toán. Đơn bằng ngoại tệ thì hiện nút trả qua PayPal, và PayPal có cơ chế thông báo riêng của họ để phần mềm khớp đơn. Hai luồng chạy độc lập, kế toán nhìn hai báo cáo, không trộn lẫn. Hai luồng, hai sổ. Ba điểm tụi em dặn khách trước khi bật hai luồng. Thứ nhất, ghi rõ trên trang thanh toán đơn nào trả bằng gì, đừng để khách Việt bấm nhầm sang PayPal rồi mất 4,40%. Thứ hai, với luồng chuyển khoản, mỗi đơn một mã QR riêng hoặc một tài khoản ảo riêng để phần mềm khớp bằng số tài khoản nhận, khỏi phải bắt khách gõ đúng nội dung chuyển khoản. Thứ ba, bật thêm thông báo Telegram vào nhóm kế toán để có người nhìn thấy mọi khoản tiền vào, dù webhook đã tự xử lý. Cách bật tin Telegram nằm ở trang [báo biến động số dư qua Telegram](/chia-se-bien-dong-so-du-telegram). Một chi tiết nhỏ mà tụi em từng vấp khi triển khai: tạo tài khoản ảo ACB xong mà chưa làm bước đăng ký nhận thông báo giao dịch thì tiền vào vẫn im lặng. ACB đòi thêm một lần OTP nữa cho riêng phần thông báo. Tụi em sửa wizard thành 4 bước liền mạch, OTP 2 lần, và ghi lại ở đây để anh chị không mất một buổi đi tìm lý do như tụi em. ## Ba việc cần hỏi kế toán và ngân hàng trước khi nhận ngoại tệ Tụi em làm phần mềm và hạ tầng, không tư vấn pháp lý, nên phần này chỉ là danh sách câu hỏi để anh chị mang đi hỏi đúng người. Một, hỏi kế toán xem doanh thu nhận qua PayPal được hạch toán và xuất hoá đơn thế nào, vì tiền về ví trước rồi mới về ngân hàng, mốc ghi nhận doanh thu cần thống nhất từ đầu. Ví dụ khoản 100 USD về ví hôm nay, năm ngày sau mới rút về ngân hàng còn 95,30 USD trừ thêm 60.000đ phí rút, kế toán ghi nhận số nào, ngày nào, câu đó phải có đáp án trước khi nhận đơn đầu tiên. Hỏi trước, đỡ rối sau. Hai, hỏi ngân hàng nơi anh chị mở tài khoản về việc nhận ngoại tệ và quy đổi, vì mỗi ngân hàng có quy trình riêng cho tiền từ nước ngoài về. Ba, nếu tính chuyện lập pháp nhân nước ngoài để dùng Stripe, hỏi luật sư về nghĩa vụ thuế ở cả hai đầu trước khi nộp bất kỳ hồ sơ nào, vì chi phí duy trì một công ty ở Singapore hay Mỹ tính theo năm, còn phí 4,40% của PayPal chỉ tính theo đơn, hai kiểu chi phí này khác nhau hoàn toàn khi doanh số chưa lớn. Còn đường tiền Việt thì đơn giản hơn nhiều. Tiền vào thẳng tài khoản ACB của anh chị, ngân hàng ghi nhận như một khoản chuyển khoản bình thường, không có bên trung gian giữ tiền, kế toán đối soát bằng mã giao dịch mà MONA Pay gửi kèm trong mỗi webhook. ## Câu hỏi thường gặp ### Nhận thanh toán từ nước ngoài có bắt buộc dùng PayPal không? Không bắt buộc, nhưng với khách lẻ ở Mỹ, Úc, châu Âu thì PayPal là cổng họ quen nhất. Khách quen gì, dùng đó. Phí 4,40% cộng phí cố định theo biểu phí tụi em kiểm ngày 28/08/2026. Với khách doanh nghiệp lớn thì tụi em thấy họ hay chọn chuyển khoản quốc tế qua ngân hàng vì kế toán bên họ quen làm chứng từ kiểu đó hơn là qua ví. Khách doanh nghiệp nước ngoài đôi khi chuyển khoản quốc tế thẳng vào tài khoản ngân hàng, phí do hai ngân hàng thu. ### Thu tiền Việt bằng MONA Pay có mất phí gì không? Không. MONA Pay miễn phí hoàn toàn, không giới hạn giao dịch, không thu phần trăm trên số tiền. Tiền vào thẳng tài khoản ACB của anh chị. Chỉ có giới hạn fair-use để chặn lạm dụng như bắn API dồn dập. Anh chị dùng thật bao nhiêu đơn một tháng cũng không chạm tới ngưỡng đó, vì nó được đặt cho máy chứ không đặt cho người bán hàng. ### Khách Việt trả bằng thẻ quốc tế thì sao? Một phần nhỏ khách Việt vẫn trả bằng thẻ Visa hay Mastercard, và khi đó một cổng thẻ có ích. Tụi em thấy phần lớn đơn trong nước đi bằng chuyển khoản vì nhanh và không mất phí, nên khuyên anh chị mở đường chuyển khoản trước, đo tỷ lệ vài tháng rồi mới quyết có thêm cổng thẻ hay không. ### Tiền về ví PayPal rồi rút về ngân hàng mất bao lâu? Thời gian rút do PayPal và ngân hàng nhận quyết định, tụi em không cam kết thay họ, vì cùng một lệnh rút có khi về trong ngày, có khi phải chờ ngân hàng nhận xử lý xong phần kiểm tra nguồn tiền từ nước ngoài. Điều tụi em kiểm được là phí: 60.000đ mỗi lần rút về ngân hàng mở tại Việt Nam khi không quy đổi tiền tệ. ### MONA Pay có nhận được tiền từ ngân hàng khác ACB không? Tính tới 28/08/2026 MONA Pay chạy thật với ACB; MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang trong quá trình đăng ký kết nối, trạng thái từng ngân hàng cập nhật ở [ngân hàng hỗ trợ](/ngan-hang). Trong lúc chờ, nhiều khách mở thêm một tài khoản ACB chỉ để nhận tiền tự động, số dư vẫn chuyển về tài khoản chính bất cứ lúc nào; webhook và Telegram đã cấu hình dùng lại nguyên khi nối thêm ngân hàng. ## Bắt đầu từ đường tiền Việt, đo rồi mới mở đường thứ hai Nếu anh chị đang thu tiền trong nước mà vẫn phải canh app ngân hàng để xác nhận từng đơn, đó là chỗ tụi em đã đứng nhiều năm trước khi tự viết hệ thống này. Tạo tài khoản MONA Pay tại [my.monapay.vn](https://my.monapay.vn/auth?mode=register), đăng ký xong dùng ngay không cần duyệt, nối ACB theo 4 bước với 2 lần OTP, rồi chuyển thử một khoản nhỏ. Kẹt ở bước nào, gọi 1900 636 648 trong giờ hành chính, kỹ sư MONA ngồi cùng anh chị tới khi tin đầu tiên về nhóm Telegram. Khi nào có khách nước ngoài thật sự, lúc đó mở PayPal thêm, phí 4,40% trả cho đúng khách cần trả. Thử trước, mở sau. --- # Open banking là gì? Ngân hàng mở API thì doanh nghiệp nhỏ được gì > Open banking là việc ngân hàng mở API cho bên thứ ba để đọc giao dịch, cấp tài khoản ảo, sinh mã QR. Tụi em kể vì sao MONA Pay mở cho mọi doanh nghiệp năm 2026. > URL: https://monapay.vn/blog/open-banking-la-gi · Markdown: https://monapay.vn/blog/open-banking-la-gi.md · Đăng 29/08/2026 Open banking là mô hình ngân hàng mở các cửa API cho bên thứ ba được phép kết nối, để phần mềm bên ngoài đọc được giao dịch, cấp tài khoản ảo, sinh mã QR hay khởi tạo dịch vụ thay cho khách hàng, thay vì mọi thứ phải làm trong app của ngân hàng. Khách đến MONA hỏi tụi em open banking là gì, tụi em kể bằng chuyện thật: nhờ ngân hàng thoáng hơn mà hệ thống thu tiền tụi em dùng nội bộ hơn 4 năm mới mở ra được cho mọi doanh nghiệp. Bài này nói phần nhận tiền. ## Trước open banking, phần mềm đứng ngoài cửa ngân hàng Tụi em làm phần mềm từ năm 2016, và trong nhiều năm, ngân hàng với phần mềm là hai thế giới không chạm nhau. Phần mềm bán hàng biết đơn, ngân hàng biết tiền, hai bên không nói chuyện. Người đứng giữa là nhân viên: mở app ngân hàng, nhìn tiền vào, quay sang phần mềm bấm đã thanh toán. Khách chuyển 9 giờ tối thì chờ tới 8 giờ sáng hôm sau. Chính MONA cũng từng có người làm việc này, mỗi ngày mở app vài chục lần. Cách duy nhất để phần mềm biết tiền vào khi đó là những mẹo không chính thức, đọc tin nhắn SMS biến động số dư hay đăng nhập giả lập vào internet banking. Chập chờn, ngân hàng đổi giao diện là gãy, và về bản chất là dùng tài khoản theo cách ngân hàng không cho phép. Tụi em không đi đường đó cho khách, vì một lần ngân hàng khoá tài khoản do đăng nhập bất thường là tiền thu cả tháng kẹt lại, không đáng để đổi lấy vài giây tiện. Open banking đổi cách đặt vấn đề: ngân hàng chủ động mở cửa, có xác thực, có hợp đồng, có tài liệu, và phần mềm được phép đứng trong nhà thay vì rình ngoài cửa sổ. Khác nhau ở chỗ được phép. Được phép thì có tài liệu để đọc, có người để hỏi, và có chỗ để khiếu nại khi sai. ## Ngân hàng mở gì, và mở cho ai? Ở Việt Nam, những gì tụi em thấy đã mở và đang dùng thật với ACB gồm 4 việc: cấp tài khoản ảo theo đầu số cho từng đơn hay từng khách, gửi thông báo mỗi giao dịch về phần mềm ngay khi tiền vào, sinh mã VietQR động điền sẵn số tiền và nội dung, tra cứu giao dịch để đối soát. Đó là mặt "nhận tiền" của open banking, phần tụi em quan tâm nhất vì nó bỏ được việc canh app. Bốn việc, một mục đích. Mặt thanh toán chủ động hay cho vay tụi em không làm nên không bàn ở đây. Cửa mở cho đối tác kỹ thuật, không mở cho từng khách lẻ. Nghĩa là một shop online không tự xin API ngân hàng, mà dùng qua một lớp như MONA Pay đã nối sẵn. Chủ tài khoản vẫn giữ quyền cuối cùng: mọi lần đăng ký tài khoản ảo hay bật nhận thông báo đều cần OTP ngân hàng gửi về số điện thoại của chủ tài khoản, với ACB là 2 lần. Không có OTP thì không ai gắn được gì vào tài khoản của anh chị. Chủ tài khoản giữ chìa. Tới 28/08/2026 MONA Pay chạy thật với ACB; MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang trong quá trình đăng ký kết nối API đối tác, tụi em cập nhật trạng thái từng ngân hàng ở [ngân hàng hỗ trợ](/ngan-hang) thay vì hứa ngày. Có là nói, chưa có thì không hứa, vì khách MONA đã hỏi tên ngân hàng khác nhiều lần và tụi em thà trả lời chậm còn hơn trả lời sai. ## Vì sao open banking là lý do MONA Pay mở public năm 2026 Hệ thống này sinh ra để thu tiền cho chính MONA, chưa từng có ý bán: phí hosting của [Mona.Host](https://mona.host), học phí Khánh Hùng Academy với 40.000 học viên và 760 học viên PRO, đơn phần mềm của Mona.Software. Rồi từ năm 2022 tụi em nhúng vào web bán hàng, phần mềm giao cho khách: tới nay hơn 6.000 khách hàng mới của MONA thu tiền qua nó. Hơn 4 năm đó tụi em vá nó bằng tiền thật của chính mình và của khách MONA trước khi mở cho người ngoài. Năm 2026 tụi em mở ra vì hai lý do. Một, ngân hàng đã thoáng hơn hẳn, API đối tác có tài liệu, có quy trình, không còn là cửa hẹp chỉ dành cho vài công ty lớn. Hai, tụi em muốn phục vụ không chỉ doanh nghiệp lớn mà cả SME, hộ kinh doanh, người làm phần mềm một mình. Với nhóm này, một cổng nhận tiền tự động trước đây là thứ xa xỉ. Giờ thì đăng ký xong dùng ngay, miễn phí hoàn toàn, không giới hạn giao dịch, chi tiết ở [bảng giá](/bang-gia). Chuyện tụi em kể kỹ ở [trang giới thiệu](/gioi-thieu). > Anh chị muốn nếm open banking bằng tiền thật thay vì đọc: tạo tài khoản [MONA Pay](https://my.monapay.vn/auth?mode=register), nối ACB 4 bước, chuyển 10.000 đồng vào mã QR của mình, xem thông báo về Telegram trong vài giây. Không ký hợp đồng, không mất phí. ## Doanh nghiệp nhỏ được gì cụ thể, đo bằng gì? Tụi em đo bằng việc đã biến mất. Việc thứ nhất là canh app ngân hàng: thông báo tiền vào tự bay về phần mềm hoặc nhóm Telegram gồm tên ngân hàng, số tài khoản, số tiền, thời gian, nội dung. Việc thứ hai là dò sao kê cuối ngày: mỗi đơn một tài khoản ảo hoặc một mã QR động, tiền vào là khớp đúng đơn, không đoán. Việc thứ ba là chờ giờ hành chính: khách chuyển 11 giờ đêm, đơn mở 11 giờ đêm. Không ai thức. Có một thứ open banking kiểu này không làm: giữ tiền. Tiền đi thẳng từ khách sang tài khoản ngân hàng của anh chị, MONA Pay chỉ đọc thông báo. Vì vậy không có phí trung gian, khác hẳn cổng quốc tế kiểu PayPal thu 4,40% cộng phí cố định cho giao dịch từ nước ngoài và 60.000 đồng mỗi lần rút về ngân hàng Việt Nam theo biểu phí tụi em kiểm ngày 28/08/2026. Với thu tiền trong nước, tụi em thấy không có lý do gì để trả phần trăm, vì đường đi thẳng ngân hàng sang ngân hàng vốn đã có sẵn và gần như không tốn gì. Ở khách MONA làm web bán hàng, kết quả tụi em thấy lặp lại: người từng ngồi xác nhận đơn chuyển sang làm việc khác, khách hết hỏi "shop nhận được tiền chưa". Tụi em không gán số giờ tiết kiệm vì không đo từng khách, nhưng cái người đó thôi làm thì thấy rõ, và thường sau 1 tháng chủ shop quên luôn mình từng có việc này. Quên là dấu hiệu tốt, tụi em mong thấy ở mọi khách. ## Open banking có rủi ro gì, tụi em phòng thế nào? Rủi ro thật nằm ở phần thông báo đi ra ngoài ngân hàng. Khi thông báo tiền vào được bắn tới website của anh chị, kẻ xấu có thể giả một gói tin "tiền đã vào" để lừa mở đơn. MONA Pay ký mọi gói tin bằng HMAC-SHA256 kèm mốc thời gian, website kiểm chữ ký rồi mới tin, gói tin lệch quá 5 phút bị từ chối. Công thức chữ ký công bố tại [trang bảo mật webhook](/docs/webhooks/bao-mat), anh chị tự kiểm lại được bằng lệnh cURL. Rủi ro thứ hai là bỏ sót: website đang khởi động lại đúng lúc thông báo tới. Lịch sử từng lần gửi trong dashboard ghi mã HTTP và nhãn lỗi, anh chị bấm gửi lại, và phần gửi lại tự động tối đa 7 lần tụi em đang triển khai. Rủi ro thứ ba là lỗi cấu hình phía người dùng, ví dụ tạo tài khoản ảo xong quên bật nhận thông báo, chính tụi em từng vấp rồi sửa wizard thành 4 bước liền mạch. Vấp thật, sửa thật, cùng ngày. ## Câu hỏi thường gặp ### Open banking có nghĩa là ai cũng xem được tài khoản của tôi? Không. Chỉ đối tác được ngân hàng cấp phép mới gọi được API, và mọi kết nối vào tài khoản của anh chị đều phải có OTP do ngân hàng gửi về số điện thoại chủ tài khoản, với ACB là 2 lần. Không có OTP là không nối được. Chủ tài khoản luôn là người giữ chìa khoá cuối cùng. Tụi em cũng không nối hộ được nếu anh chị không bấm OTP. ### Open banking ở Việt Nam đã dùng được chưa? Đã dùng được với ngân hàng đã mở API đối tác. MONA Pay đang chạy thật trên API của ACB từ hơn 4 năm với chính MONA và hơn 6.000 khách hàng MONA, tới 28/08/2026 mở cho mọi doanh nghiệp. Đăng ký xong là dùng ngay, không có bước chờ duyệt. ### Doanh nghiệp nhỏ có tự xin API ngân hàng được không? Thường không, vì cần pháp nhân, hồ sơ và đội kỹ thuật. Cách thực tế là dùng qua lớp trung gian đã nối sẵn như MONA Pay, tự đăng ký tại my.monapay.vn và nối tài khoản ACB trong 4 bước. Cả việc mất khoảng 5 phút kể cả 2 lần OTP. Không cần dev. ### Open banking có làm tiền đi qua bên thứ ba không? Với MONA Pay thì không, tiền không ghé qua tụi em. Tiền vào thẳng tài khoản ACB của anh chị, tụi em chỉ đọc thông báo giao dịch và báo lại qua webhook, Telegram, dashboard. Không giữ đồng nào. ### Ngoài nhận tiền, open banking còn dùng làm gì? Còn nhiều việc như kiểm tra danh tính, cho vay, thanh toán chủ động, nhưng tụi em chỉ làm phần nhận tiền và nói về phần đó, vì đó là thứ tụi em chạy thật hơn 4 năm và dám đứng tên. Chi tiết kỹ thuật ở [tài liệu MONA Pay](/docs). ## Thử open banking bằng tài khoản ACB của anh chị trong 5 phút Đăng ký tại [my.monapay.vn/auth?mode=register](https://my.monapay.vn/auth?mode=register), dùng ngay không chờ duyệt, nối ACB 4 bước với 2 lần OTP, bật Telegram, rồi chuyển thử 10.000 đồng vào mã QR của mình để thấy thông báo về trong vài giây. Cả việc mất chừng 5 phút, tính cả 2 lần OTP. Kẹt ở bước OTP hay bước nào, gọi 1900 636 648 giờ hành chính, kỹ sư MONA làm cùng anh chị tới khi tin đầu tiên báo về. --- # Tài khoản ngân hàng ảo là gì? Vì sao mỗi đơn một số thì tiền tự khớp > Tài khoản ngân hàng ảo (virtual account) là số phụ ngân hàng cấp theo đầu số, tiền vẫn về tài khoản chính nhưng biết ngay của đơn nào. Tụi em kể cách ACB cấp. > URL: https://monapay.vn/blog/tai-khoan-ngan-hang-ao-la-gi · Markdown: https://monapay.vn/blog/tai-khoan-ngan-hang-ao-la-gi.md · Đăng 29/08/2026 Tài khoản ngân hàng ảo, tiếng Anh là virtual account, viết tắt VA, là số tài khoản phụ do ngân hàng cấp theo một đầu số gắn với tài khoản chính của anh chị: tiền chuyển vào số phụ nào cũng về tài khoản chính, nhưng thông báo ghi rõ số phụ đó. Khách đến MONA làm phần mềm hỏi tụi em tài khoản ảo là gì, tụi em trả lời bằng cách tụi em thu học phí: mỗi học viên một số, tiền vào là biết của ai. ## Tài khoản ảo khác tài khoản phụ thông thường chỗ nào? Tài khoản phụ thông thường là một tài khoản thật thứ hai, có số dư riêng, mở tại quầy, số lượng vài cái. Tài khoản ảo không có số dư riêng. Nó là một "nhãn" ngân hàng gắn lên dòng tiền vào: ACB cấp cho anh chị một đầu số, ví dụ tiền tố MONA, rồi mỗi số ảo là đầu số cộng phần đuôi do phần mềm đặt. Tiền chuyển vào số ảo chạy thẳng về tài khoản chính, không nằm ở đâu khác, chỉ khác là thông báo giao dịch có kèm số ảo vừa nhận. Vì không có số dư riêng nên tài khoản ảo tạo được nhiều, tạo bằng API, tạo trong vài giây. Ở [Khánh Hùng Academy](https://khanhhung.academy) với 40.000 học viên, mỗi học viên một số ảo, tiền vào là khoá mở, chạy hơn 4 năm nay. Không ai ngồi đọc nội dung chuyển khoản. Nội dung khách gõ sao cũng được. Tụi em hay ví với khách: tài khoản chính là cái két, tài khoản ảo là các ngăn thư có tên trên mặt két. Tiền rơi vào ngăn nào, biết ngay của ai, nhưng cuối cùng vẫn nằm trong một cái két. ## Vì sao tài khoản ảo giải được bài "hai khách chuyển cùng số tiền"? Cách khớp tiền phổ biến là đọc nội dung chuyển khoản: khách gõ DH10234, phần mềm tìm đơn DH10234. Cách này gãy ở hai chỗ tụi em gặp hoài trong 14.000+ dự án. Một, khách gõ sai hoặc gõ "chuyen tien" cho gọn. Hai, hai khách cùng chuyển 500.000 đồng trong cùng buổi, nội dung đều trống, người bán ngồi đoán. Tài khoản ảo bỏ luôn việc đọc nội dung. Đơn DH10234 được cấp số ảo riêng, tiền vào số đó là của đơn đó, khách gõ gì cũng không sao. Hai khách cùng 500.000 đồng chuyển vào hai số khác nhau, không đoán. Với trung tâm thu học phí theo kỳ, mỗi học viên giữ cố định một số ảo suốt 12 tháng của khoá, phụ huynh chuyển tháng nào cũng đúng em đó. Với chủ nhà trọ, mỗi phòng một số, tiền phòng tháng 9 của phòng 302 không lẫn với phòng 305. Đây là lý do tụi em xếp tài khoản ảo là nền của tự động xác nhận, còn mã QR động là lớp tiện cho khách bên trên. Có VA rồi thì mã QR động chỉ là cách đóng gói số ảo cộng số tiền thành ô vuông cho khách quét. Hai thứ đi cùng nhau. ## ACB cấp tài khoản ảo cho anh chị theo trình tự nào? Tụi em kể đúng trình tự MONA Pay đang làm với ACB, kiểm ngày 28/08/2026. Bước một, trong dashboard my.monapay.vn, anh chị nhập số tài khoản ACB đứng tên mình, số điện thoại đã đăng ký với ACB và loại khách hàng cá nhân hay doanh nghiệp. Bước hai, ACB gửi OTP về số điện thoại đó để xác thực việc đăng ký đầu số tài khoản ảo. Bước ba, đăng ký dịch vụ nhận thông báo giao dịch, ACB gửi OTP lần hai. Bước bốn, xong, từ đây tiền vào số ảo nào là MONA Pay nhận thông báo và báo lại cho anh chị. Bước ba là bước tụi em từng bỏ sót. Chính tụi em từng tạo tài khoản ảo xong mà chưa đăng ký nhận thông báo, tiền vào ACB đúng nhưng hệ thống im lặng. Tìm ra rồi mới hiểu ACB tách hai dịch vụ và cần hai lần OTP. Tụi em sửa wizard thành 4 bước liền mạch ngay hôm đó để không khách nào vấp lại. Vấp một lần là đủ. Với dân kỹ thuật, toàn bộ trình tự có API tương ứng ở [tài liệu tài khoản ảo](/docs/api/tai-khoan-ao-va): đăng ký VA, xác thực OTP, đăng ký thông báo, xác thực OTP lần hai, tra danh sách VA, huỷ VA. > Anh chị có tài khoản ACB thì nối thử hôm nay: tạo tài khoản [MONA Pay](https://my.monapay.vn/auth?mode=register), đi 4 bước với 2 lần OTP, tạo một số ảo, chuyển 10.000 đồng vào và xem thông báo về trong vài giây. Miễn phí hoàn toàn, không giới hạn số tài khoản ảo lẫn số giao dịch. ## Tiền vào tài khoản ảo rồi thì phần mềm nhận tin bằng cách nào? ACB báo cho MONA Pay, MONA Pay báo cho anh chị theo ba đường. Webhook: một gói JSON gồm số tiền, nội dung, mã giao dịch, số tài khoản ảo bay về website hoặc phần mềm, ký HMAC-SHA256, website trả mã 200 trong 10 giây là xong; phần mềm đọc số tài khoản ảo là biết đơn nào, không cần đọc nội dung. Telegram: tin vào nhóm công ty gồm tên ngân hàng, số tài khoản, số tiền, thời gian, nội dung, cấu hình được theo từng số ảo hay mọi tài khoản. Tiền không rời ACB. Dashboard: danh sách giao dịch, lọc, tra tối đa 100 dòng mỗi trang để đối soát. Ở [Mona.Host](https://mona.host), tụi em dùng webhook: mỗi hoá đơn hosting một số ảo, kể cả gói 12 tháng hay 1 tháng, tiền vào là hoá đơn chuyển sang đã thanh toán và dịch vụ tự gia hạn, chạy hơn 4 năm. Ở khách bán hàng, tụi em thường bật thêm Telegram cho thu ngân nhìn. Tiền lúc nào cũng nằm ở ACB, MONA Pay không giữ đồng nào, nên không có phí. Cổng quốc tế kiểu PayPal thì ngược lại, tiền vào ví của họ trước, thu 4,40% cộng phí cố định cho giao dịch từ nước ngoài, rút về ngân hàng Việt Nam 60.000 đồng mỗi lần, theo biểu phí tụi em kiểm 28/08/2026. ## Giới hạn và những điều tụi em nói trước Tài khoản ảo chỉ nhận tiền vào, không dùng để chuyển tiền ra, không có số dư riêng, không rút được từ số ảo. Số ảo phụ thuộc ngân hàng cấp: hiện ACB đang hoạt động (cần tài khoản ACB đứng tên anh chị), các ngân hàng khác đang trong quá trình đăng ký kết nối, xem bảng trạng thái ở [ngân hàng hỗ trợ](/ngan-hang). Phí phía ngân hàng cho dịch vụ tài khoản ảo, nếu có, theo biểu phí ACB, tụi em không nói thay. Hỏi ACB cho chắc. Còn phía MONA Pay: không thu phần trăm, không phí mở, không giới hạn số tài khoản ảo. Một shop 10.000 đơn một tháng tạo 10.000 số ảo cũng như một shop 10 đơn. Chi tiết ở [bảng giá](/bang-gia) và [trang nối ACB](/acb). ## Câu hỏi thường gặp ### Tài khoản ngân hàng ảo có phải là tài khoản giả không? Không. Đây là dịch vụ chính thức của ngân hàng, số ảo do ACB cấp theo đầu số đăng ký sau 2 lần OTP, tiền về tài khoản thật của anh chị trong vài giây, sao kê ngân hàng ghi đầy đủ. ### Một tài khoản chính tạo được bao nhiêu tài khoản ảo? MONA Pay không giới hạn, 10 đơn hay 10.000 đơn một tháng đều tạo được. Anh chị tạo theo đơn, theo khách, theo phòng, theo học viên, tuỳ cách khớp tiền của mình. ### Khách chuyển vào tài khoản ảo có mất thêm phí không? Với khách là chuyển khoản bình thường qua Napas 247 tới một số tài khoản ACB, phí theo ngân hàng của khách như mọi lần chuyển khác. ### Tài khoản ảo và mã QR động dùng cái nào? Dùng cả hai: số ảo là nền để khớp tiền, mã QR động đóng gói số ảo cộng số tiền cho khách quét khỏi gõ. Tạo mã tại [tài liệu QR thanh toán](/docs/api/qr-thanh-toan) hoặc màn Tạo QR trong dashboard. ### Huỷ tài khoản ảo có ảnh hưởng tiền đã nhận không? Không. Tiền đã về tài khoản chính từ lúc chuyển, huỷ số ảo chỉ để khách không chuyển nhầm vào đơn cũ. Tiền đã về rồi. ## Tạo tài khoản ảo đầu tiên trong 5 phút, tụi em ngồi cùng nếu kẹt OTP Đăng ký tại [my.monapay.vn/auth?mode=register](https://my.monapay.vn/auth?mode=register), dùng ngay không chờ duyệt. Vào Ngân hàng và VA, đi 4 bước với 2 lần OTP từ ACB, tạo số ảo cho đơn đầu tiên, chuyển thử 10.000 đồng. Kẹt ở OTP lần hai hay bước nào, gọi 1900 636 648 giờ hành chính, kỹ sư MONA làm cùng anh chị tới khi thông báo đầu tiên về. --- # Báo có tự động: tiền vào tài khoản là cả công ty biết, tụi em làm sao > Báo có tự động khác gì tin nhắn biến động số dư, bật trong 4 bước để tiền vào tài khoản ACB là nhóm Telegram của công ty thấy ngay. Kể từ cách MONA thu học phí. > URL: https://monapay.vn/blog/tu-dong-bao-co-tien-vao-tai-khoan · Markdown: https://monapay.vn/blog/tu-dong-bao-co-tien-vao-tai-khoan.md · Đăng 29/08/2026 Khách đến MONA làm phần mềm quản lý, mười người thì tám người đang "báo có" bằng cách chụp màn hình app ngân hàng gửi vào nhóm chat. Báo có tự động là bỏ cái thao tác đó: tiền vào tài khoản ACB, ngân hàng báo cho MONA Pay, MONA Pay đẩy một tin có số tiền, nội dung, thời gian vào đúng nhóm Telegram của bộ phận cần biết, trong vài giây. Tụi em dùng cách này để thu học phí và phí hosting hơn 4 năm trước khi mở cho mọi doanh nghiệp. Không tốn tiền, bật trong 4 bước. ## Chủ doanh nghiệp đến MONA thường đang báo có bằng cách nào? Tụi em hỏi câu này với gần như mọi khách làm phần mềm, và câu trả lời quanh đi quẩn lại có ba kiểu. Kiểu một, chủ giữ app ngân hàng, tiền vào là chụp màn hình gửi cho nhân viên. Kiểu hai, đưa luôn tài khoản app ngân hàng cho nhân viên kế toán hoặc người quản lý cửa hàng, ai cũng thấy hơi sợ nhưng vẫn làm vì không còn cách nào. Kiểu ba, không báo gì cả, cuối ngày kế toán tải sao kê về đối chiếu, khách chuyển buổi sáng thì chiều mới biết. Ở MONA thời chưa có hệ thống riêng, tụi em ở kiểu một. Anh phụ trách thu phí hosting cầm điện thoại cả ngày, tiền vào là chụp gửi vào nhóm kỹ thuật để mở dịch vụ cho khách. Khách chuyển 10 giờ đêm thì anh ấy cũng phải thức. Khách chuyển sáng chủ nhật thì chờ. Cái phiền nhất là ba người cùng chuyển 1.200.000 đồng phí hosting trong một buổi, nội dung chuyển khoản mỗi người gõ một kiểu, phải nhắn hỏi lại từng người xem ai là ai. Kể chuyện này để anh chị thấy cái mất của "báo có bằng tay" là mất người thật và mất giờ thật, chứ tiền thì vẫn vào đủ. Với chuỗi vài cửa hàng hoặc trung tâm nhiều lớp, mất mát này nhân lên theo số điểm bán. Người giữ app ngân hàng thành nút thắt của cả công ty. Ai cũng phải chờ một người, và người đó thì không thể thức cả đêm lẫn cuối tuần. ## Báo có tự động khác gì tin nhắn biến động số dư của ngân hàng? Nhiều anh chị hỏi tụi em: tôi đã đăng ký tin nhắn biến động số dư rồi, cần gì thêm. Tin nhắn đó chỉ về máy của chủ tài khoản. Nó không chia được cho ai, và nó không biết tiền đó thuộc đơn nào, khách nào, nên nhận xong vẫn phải có người đọc rồi chuyển tiếp. Báo có tự động của MONA Pay khác ở ba điểm. Thứ nhất, tin về nhóm Telegram chứ không về một cái điện thoại, nên bộ phận gói hàng, bộ phận kỹ thuật, kế toán cùng thấy một lúc, không ai phải chuyển tiếp cho ai. Thứ hai, tin được gắn với tài khoản ảo: mỗi đơn hàng, mỗi cửa hàng, mỗi lớp học có một số tài khoản ảo ACB riêng, chuỗi 3 chi nhánh là 3 số khác nhau, tiền vào số nào là biết ngay của ai, khách gõ nội dung sai cũng không sao. Thứ ba, cùng lúc với tin Telegram, MONA Pay gửi được cả webhook về phần mềm để phần mềm tự xử lý, tụi em nói kỹ ở [bài xác nhận thanh toán tự động](/blog/xac-nhan-thanh-toan-tu-dong). Tụi em chọn Telegram thay vì tự làm một app thông báo riêng vì một lý do rất đời: ai cũng đã có Telegram trong máy, nhóm tạo trong 30 giây, thêm bot vào là xong, không phải bắt nhân viên cài thêm gì. Tiền không đi qua MONA Pay. Nó vào thẳng tài khoản ACB của anh chị, MONA Pay chỉ nhận thông báo từ ACB rồi báo lại. ## Mẫu tin báo có tụi em đang dùng ở MONA trông thế nào? Một tin báo có chuẩn của MONA Pay có 5 dòng: tên ngân hàng, số tài khoản nhận, số tiền, thời gian và nội dung chuyển khoản. Ví dụ tin tụi em nhận khi khách đóng phí hosting trông như dưới đây. > Tên ngân hàng: ACB · Số tài khoản: số tài khoản ảo của đơn · Số tiền: 2.500.000 VNĐ · Thời gian: 28/08/2026 10:30 · Nội dung: DH10234 NGUYEN VAN A. Năm dòng là đủ dùng. Nhân viên kỹ thuật đọc xong mở dịch vụ luôn mà không cần hỏi lại ai. Anh chị sửa được mẫu tin trong dashboard, thêm bớt dòng, đổi cách gọi, ví dụ trung tâm dạy học của khách MONA đổi "Nội dung" thành "Học viên" cho dễ đọc. Số tiền luôn hiển thị định dạng có dấu chấm ngăn hàng nghìn kèm VNĐ, tụi em cố ý làm vậy vì đọc 2.500.000 nhanh hơn 2500000 rất nhiều khi tin đến dồn dập. ## Bật báo có tự động trong 4 bước Toàn bộ làm trong dashboard [my.monapay.vn](https://my.monapay.vn), không cần người kỹ thuật, tài liệu chi tiết từng ô ở [hướng dẫn Telegram](/docs/telegram) và tổng quan tính năng ở [trang báo biến động số dư qua Telegram](/chia-se-bien-dong-so-du-telegram). Bước 1, tạo tài khoản MONA Pay và nối tài khoản ACB. Đăng ký xong dùng ngay, không chờ duyệt. Vào mục Ngân hàng và VA, nhập số tài khoản ACB đứng tên anh chị và số điện thoại đăng ký với ACB, nhận OTP tạo tài khoản ảo, rồi nhận OTP lần hai để đăng ký nhận thông báo giao dịch. Hai lần OTP, thiếu lần hai là tiền vào không có tin, chi tiết ở [trang nối ACB](/acb). Bước 2, tạo nhóm Telegram cho bộ phận cần nhận tin, mở thông tin nhóm, thêm bot của MONA Pay vào nhóm, tên bot hiển thị ngay trong mục Telegram của dashboard. Bước 3, vào mục Telegram trong dashboard, bấm thêm cấu hình, dán mã nhóm vào ô group_id. Nhóm có chia chủ đề thì dán thêm topic_id để tin rơi đúng chủ đề. Đặt tên gợi nhớ, chọn nhận tin cho mọi tài khoản hay chỉ một tài khoản ảo, sửa mẫu tin nếu muốn. Bước 4, bấm gửi thử. Tin mẫu về nhóm là cấu hình đúng. Sau đó chuyển thật 10.000 đồng vào tài khoản ảo để thấy trọn vòng: tiền vào, ACB báo, tin về nhóm. Tụi em luôn kêu khách làm bước chuyển thật này, vì gửi thử chỉ chứng minh Telegram nối đúng, còn tiền thật mới chứng minh ngân hàng đã bật thông báo. Anh chị muốn thử trước khi quyết định gì thì cứ làm đủ 4 bước với một nhóm nhỏ hai ba người. Miễn phí, không giới hạn số tin, không giới hạn số nhóm. Thấy hợp thì mở rộng ra từng bộ phận, không hợp thì xoá cấu hình, không mất gì. ## Nhiều cửa hàng, nhiều bộ phận thì chia nhóm báo có ra sao? Đây là phần khách MONA hay hỏi nhất khi đã chạy được nhóm đầu tiên. Cách tụi em chia cho một chuỗi 5 chi nhánh: mỗi chi nhánh 1 tài khoản ảo ACB riêng, mỗi tài khoản ảo gắn với 1 nhóm Telegram của chi nhánh đó, tổng cộng 5 nhóm nhỏ và 1 nhóm chung cho chủ. Tiền vào tài khoản ảo của chi nhánh quận 1 thì chỉ nhóm quận 1 thấy, chi nhánh khác không thấy tiền của nhau, còn chủ vào nhóm tổng nhận tin của mọi tài khoản. Cấu hình 1 lần cho mỗi chi nhánh, sau đó không phải đụng lại. Cách chia này giải quyết được chuyện tế nhị nhất: không phải đưa app ngân hàng cho bất kỳ nhân viên nào, vì nhân viên chỉ thấy tin "tiền vào bao nhiêu, nội dung gì", không thấy số dư, không chuyển tiền được, không đăng nhập được vào đâu. Chủ giữ app như cũ. Không ai khác cần tới nó, và cũng không ai phải xin. Với trung tâm dạy học của khách MONA, tụi em chia theo lớp hoặc theo khoá: tài khoản ảo của khoá nào thì nhóm giáo vụ khoá đó nhận tin, kế toán nhận tất cả. Với công ty phần mềm bán theo gói thì chia theo sản phẩm, mỗi gói 1 tài khoản ảo. Nguyên tắc chung chỉ có một: ai cần biết tiền của việc gì thì nhận đúng tin của việc đó, không hơn, không kém. ## Có báo có tự động rồi, kế toán đối chiếu thế nào? Kế toán không cần ngồi canh tin nữa, nhưng vẫn cần đối chiếu, và tụi em làm phần này trong dashboard. Mục Giao dịch liệt kê mọi giao dịch tiền vào, lọc theo tài khoản ảo, theo ngày, hiển thị tối đa 100 giao dịch trên 1 trang, mỗi dòng có mã giao dịch của ngân hàng để so với sao kê. Kế toán của MONA làm việc này 1 lần mỗi tuần thay vì mỗi ngày như trước: mở dashboard, lọc 7 ngày vừa rồi, so tổng với sao kê ACB. Khớp thì thôi. Với công ty có phần mềm riêng, dev gọi API tra giao dịch để tự đối chiếu, tài liệu ở [hướng dẫn đối soát](/docs/webhooks/doi-soat). Tin Telegram là để người biết ngay, dashboard và API là để sổ sách khớp, hai thứ bổ cho nhau. ## Tin không về nhóm thì kiểm ở đâu? Kiểm đúng ba chỗ. Theo thứ tự tụi em vẫn kiểm cho khách. Một, bấm gửi thử trong mục Telegram: tin mẫu không về nghĩa là bot chưa được thêm vào nhóm hoặc mã nhóm sai, dán lại mã là xong. Hai, tin mẫu về mà tiền thật không về, gần như chắc chắn là chưa làm OTP lần hai để đăng ký nhận thông báo với ACB. Chính tụi em từng dính lỗi này khi triển khai: tiền vào, hệ thống im, kiểm lại mới thấy thiếu bước đăng ký thông báo, làm xong OTP lần hai thì tin về ngay. Ba, mở lịch sử gửi trong dashboard: mỗi lần gửi có trạng thái và nhãn lỗi, có nút gửi lại từng tin. Nối ACB đúng quy trình là 2 lần OTP, thiếu 1 lần là tiền vào không báo. Ngoài ba chỗ đó mà vẫn kẹt thì gọi tụi em qua 1900 636 648, không mất phí, kỹ sư kiểm cùng anh chị trong vài phút. ## Báo có tự động bằng MONA Pay tốn bao nhiêu? Không đồng nào. MONA Pay miễn phí hoàn toàn, không giới hạn số giao dịch, không giới hạn số nhóm Telegram, không thu phần trăm trên số tiền. Tụi em để miễn phí được vì hệ thống đã chạy sẵn hơn 4 năm cho chính MONA và cho hơn 6.000 khách hàng mới của MONA từ 2022, tiền không đi qua MONA Pay nên không có phí trung gian, và MONA sống bằng làm web, hosting, phần mềm chứ không sống bằng phí thu tiền của anh chị. Phí phía ngân hàng, nếu có, theo biểu phí của ACB, anh chị hỏi ACB khi mở tài khoản. Nếu hôm nay anh chị vẫn đang chụp màn hình app ngân hàng gửi vào nhóm, hoặc đang đưa app ngân hàng cho nhân viên vì không còn cách nào khác, thì thử ngay chiều nay: tạo tài khoản tại [my.monapay.vn/auth?mode=register](https://my.monapay.vn/auth?mode=register), nối ACB với 2 lần OTP, thêm bot vào một nhóm nhỏ, chuyển thử 10.000 đồng. Tin về là anh chị cất được cái điện thoại xuống. Kẹt bước nào gọi 1900 636 648 giờ hành chính, kỹ sư MONA ngồi cùng anh chị tới khi tin đầu tiên chạy về. Khách làm phần mềm tại [Mona.Software](https://mona.software) thì tụi em cấu hình luôn trong lúc bàn giao. ## Câu hỏi thường gặp ### Nhân viên trong nhóm có thấy số dư tài khoản không? Không, vì tin chỉ có số tiền vào, nội dung, thời gian và số tài khoản nhận, không có số dư, không có quyền gì với tài khoản ngân hàng của anh chị, app ngân hàng vẫn chỉ mình anh chị giữ. ### Tin báo có về chậm không? ACB báo là MONA Pay đẩy tin ngay, thường tính bằng giây. Máy nhận webhook của anh chị có 10 giây để trả lời, còn tin Telegram thì không cần anh chị trả lời gì. Khi tụi em kiểm toàn bộ luồng, tin Telegram về gần như cùng lúc với thông báo của ngân hàng, và webhook về phần mềm cũng trong cùng khoảng đó. ### Một nhóm nhận tin của nhiều tài khoản ảo được không? Được. Chọn "mọi tài khoản" khi tạo cấu hình. Ngược lại, một tài khoản ảo cũng gửi được vào nhiều nhóm bằng cách tạo nhiều cấu hình, mỗi cấu hình 1 nhóm. Cấu hình 1 lần, nhóm kế toán và nhóm bán hàng cùng nhận một nguồn tin. ### Tiền ra khỏi tài khoản có báo không? Hiện tại thì chưa. MONA Pay đang báo giao dịch tiền vào, phần tiền ra tụi em đang làm, có là ghi trên trang tài liệu. ### Tôi dùng ngân hàng khác ACB thì sao? Hiện ACB đang hoạt động; MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang trong quá trình đăng ký kết nối, bảng trạng thái cập nhật tại [ngân hàng hỗ trợ](/ngan-hang). Webhook, Telegram, API của anh chị dùng chung cho mọi ngân hàng, payload có `bank_name`, nối thêm ngân hàng không phải sửa gì. --- # Tự động xác nhận thanh toán hosting: chuyện tụi em tự xây hệ thống thu tiền cho mình > MONA kể cách tự xây hệ thống tự động xác nhận thanh toán hosting, học phí, phần mềm hơn 4 năm trước: tài khoản ảo, webhook ký HMAC, rồi nhúng vào web khách. > URL: https://monapay.vn/blog/tu-xay-cong-thanh-toan-de-thu-tien-hosting · Markdown: https://monapay.vn/blog/tu-xay-cong-thanh-toan-de-thu-tien-hosting.md · Đăng 29/08/2026 Tự động xác nhận thanh toán hosting ở MONA nghĩa là khách chuyển khoản xong, hệ thống nhận thông báo từ ngân hàng, khớp đúng hoá đơn rồi tự gia hạn gói mà không cần người bấm gì. Tụi em xây cơ chế này hơn 4 năm trước cho chính Mona.Host, sau đó dùng luôn cho học phí Khánh Hùng Academy và tiền phần mềm Mona.Software, rồi từ năm 2022 nhúng vào web, phần mềm giao khách, tới nay hơn 6.000 khách hàng mới của MONA thu tiền qua đó. Sau 14.000 dự án tụi em thấy chủ web nào cũng kẹt đúng chỗ này, nên bài này kể cụ thể tụi em vấp cái gì, học được gì, và vì sao giờ nó thành [MONA Pay](/) cho mọi doanh nghiệp. Kể thật, không tô màu. ## Khách gia hạn hosting lúc 11 giờ đêm và cái web phải chờ tới sáng Hosting có một đặc điểm khiến khâu thu tiền khó hơn bán hàng thường: hết hạn là web tắt. Khách của Mona.Host nhớ ra gia hạn thường vào lúc web đã ngừng chạy, nhiều khi là 11 giờ đêm hay sáng chủ nhật. Họ chuyển khoản ngay, chụp màn hình gửi vào Zalo, rồi ngồi chờ. Hồi chưa có hệ thống, người mở lại web cho họ là kỹ thuật viên trực, và người xác nhận khoản tiền là kế toán, hai người đó không ngồi cùng nhau lúc 11 giờ đêm. Kế toán bên tụi em hồi đó mở app ngân hàng gần như cả ngày. Mỗi khoản tiền vào phải dò xem của tài khoản hosting nào, vì nội dung chuyển khoản khách gõ đủ kiểu: sai tên miền, thiếu mã hoá đơn, có người ghi mỗi chữ "gia hạn". Có ngày hai khách chuyển cùng 1.200.000 đồng cho hai gói giống nhau, kế toán phải nhắn hỏi từng người mới dám gán. Chỗ đó tốn người nhất, vì việc lặp đi lặp lại cả ngày mà lại không được phép sai một khoản nào. Mệt mà không thấy. Tụi em từng thử cách mọi người vẫn thử: bật SMS biến động số dư về điện thoại kế toán rồi chuyển tiếp vào nhóm. Chạy được một thời gian thì thấy hai lỗ hổng, một là tin nhắn không có mã đơn nên vẫn phải dò tay, hai là đưa tin biến động số dư của toàn công ty cho nhiều người xem là chuyện chủ doanh nghiệp nào cũng ngại. ## Bài học một: mỗi hoá đơn cần một tài khoản ảo riêng thì máy mới khớp được Cái gỡ được bài toán dò tay là tài khoản ảo. Ngân hàng cấp cho tụi em một đầu số, từ đó sinh ra mỗi hoá đơn hosting một số tài khoản ảo riêng, tiền chuyển vào số nào thì tài khoản chính vẫn nhận, nhưng thông báo giao dịch có kèm số ảo đó. Khách gõ nội dung gì cũng được, thậm chí để trống. Máy nhìn số ảo là biết đơn. Từ lúc chuyển sang cách này, chuyện hai khách chuyển cùng 1.200.000 đồng không còn là vấn đề, vì mỗi khoản đi vào một số khác nhau. Mã VietQR in trên hoá đơn cũng điền sẵn số tài khoản ảo và số tiền, khách quét là xong, không cần gõ. Đây là thứ tụi em thấy sau nhiều năm thu tiền: đừng bắt khách gõ đúng nội dung, hãy làm cho nội dung không còn quan trọng. Hiện MONA Pay làm đúng việc này với ACB (các ngân hàng khác đang đăng ký kết nối, xem [ngân hàng hỗ trợ](/ngan-hang)), anh chị xem cách đăng ký tài khoản ảo ở trang [nhận tiền ACB theo thời gian thực](/acb). Đăng ký cần 2 lần OTP về số điện thoại chủ tài khoản, lần đầu để tạo tài khoản ảo, lần hai để đăng ký nhận thông báo giao dịch cho nó. ## Bài học hai: web phải nhận tin từ máy chủ bằng webhook có chữ ký, không phải từ người Khớp được hoá đơn rồi thì phải làm gì đó với kết quả, và tụi em muốn máy làm. Cách tụi em chọn là webhook: hệ thống thu tiền gửi một gói tin tới phần mềm quản lý hosting, phần mềm tự gia hạn, tự gửi email cho khách. Với Khánh Hùng Academy thì gói tin đó mở khoá học cho học viên vừa đóng tiền, với Mona.Software thì đổi trạng thái đợt thanh toán của hợp đồng. Ngay tuần đầu tụi em học được bài đắt. Không chữ ký, ai gọi cũng được. Một gói tin giả "đã nhận 5.000.000 đồng" có thể mở hosting cho người chưa trả tiền. Nên từ đó mọi gói tin đều ký HMAC-SHA256 bằng khoá bí mật riêng của từng nơi nhận, kèm dấu thời gian, và nơi nhận từ chối gói tin lệch giờ quá 5 phút để chặn kẻ phát lại gói cũ. Cơ chế đó giữ nguyên tới bản MONA Pay hôm nay, công thức ký công bố trong tài liệu [bảo mật webhook](/docs/webhooks/bao-mat), có sẵn mẫu PHP và Node để anh chị dán vào. Còn phần thông báo cho người, tụi em đưa vào Telegram. Mỗi khoản tiền vào là một tin nhắn trong nhóm có tên ngân hàng, số tài khoản, số tiền, thời gian, nội dung. Kế toán, kỹ thuật trực, chủ đều thấy cùng lúc mà không ai phải cầm app ngân hàng. Anh chị xem cách cấu hình ở trang [báo biến động số dư qua Telegram](/chia-se-bien-dong-so-du-telegram). Anh chị đang tự thu tiền cho phần mềm hay dịch vụ định kỳ của mình? Tạo tài khoản ở [my.monapay.vn](https://my.monapay.vn/auth?mode=register), nối ACB, khai địa chỉ webhook của phần mềm, và chuyển thử 10.000 đồng để xem gói tin về. Miễn phí hoàn toàn, không giới hạn giao dịch. ## Bài học ba: máy nhận sẽ có lúc chết, nên phải có lịch sử và nút gửi lại Cái tụi em không lường lúc đầu là máy nhận webhook cũng hỏng. Máy chủ web bảo trì, chứng chỉ SSL hết hạn, đổi tên miền quên đổi địa chỉ nhận, hoặc đơn giản là phần mềm phía nhận trả lời chậm quá. Lần đầu gặp, một loạt hoá đơn hosting đã có tiền mà không được gia hạn, và tụi em chỉ biết khi khách gọi. Đau ở chỗ đó. Từ đó hệ thống ghi lại từng lần gửi: gửi lúc nào, máy nhận trả mã gì, mất bao nhiêu mili giây, lỗi thuộc loại nào trong nhóm hết giờ chờ, lỗi SSL, lỗi tên miền, lỗi kết nối hay lỗi phía máy nhận. Nơi nhận trả lời 200, 201 hoặc 202 trong 10 giây thì tính thành công, còn lại tính thất bại và hiện đỏ trên bảng. Người trực nhìn bảng là biết đường ống gãy ở đâu, bấm gửi lại là gói tin đi lại với đúng mã giao dịch cũ, phần mềm nhận nhìn mã là biết đã xử lý hay chưa, không gia hạn hai lần. Phần gửi lại tự động theo lịch tối đa 7 lần tụi em đang triển khai cho bản public, hiện vẫn là bấm tay. Tụi em ghi rõ trong tài liệu [gửi lại và xử lý lỗi](/docs/webhooks/gui-lai-va-xu-ly-loi) thay vì viết như đã có. ## Từ thu tiền cho mình sang thu tiền cho web của khách Khi cơ chế chạy ổn cho hosting, học phí và phần mềm, tụi em nhìn lại các web bán hàng đang giao cho khách và thấy chúng đều kẹt đúng chỗ tụi em từng kẹt. Khách của chủ shop chuyển khoản, chụp màn hình, nhắn Zalo, rồi chờ. Chủ shop hoặc nhân viên mở app ngân hàng đối chiếu, rồi vào trang quản trị đổi trạng thái đơn bằng tay. Tụi em mang nguyên cơ chế tài khoản ảo và webhook nhúng vào web WooCommerce và phần mềm quản lý giao khách, đơn tự sang đã thanh toán khi tiền về. Hệ MONA từ năm 2016 tới giờ có hơn 14.000 dự án, phần lớn là web bán hàng và phần mềm nghiệp vụ, nên số nơi chạy cơ chế này lớn dần theo từng dự án bàn giao. Từ năm 2022 tới nay hơn 6.000 khách hàng mới của MONA đã dùng, nhưng suốt thời gian đó nó vẫn là thứ chỉ khách MONA có. Người ngoài không đăng ký được. Tới tháng 8 năm 2026 tụi em mở ra thành MONA Pay để ai cũng tự đăng ký được, vì bài toán này không phân biệt công ty lớn hay tiệm nhỏ. Anh chị đọc chuyện mở public ở bài [MONA Pay mở cho mọi doanh nghiệp](/blog/mona-pay-mo-public-sau-4-nam). Điều tụi em muốn nói với chủ web hay chủ phần mềm đang tự thu tiền: đừng bắt đầu bằng cách canh app ngân hàng rồi tính tự động hoá sau. Bắt đầu bằng tài khoản ảo riêng cho từng đơn và một địa chỉ nhận webhook có chữ ký. Hai thứ đó tụi em mất hơn 4 năm để làm cho chắc, và giờ anh chị lấy dùng miễn phí. ## Anh chị thu tiền định kỳ thì làm ngay hôm nay Nếu anh chị bán hosting, khoá học, phần mềm, hội viên hay bất cứ thứ gì thu tiền lặp lại, và vẫn có người ngồi đối chiếu chuyển khoản mỗi ngày, thì đây là việc đáng làm trong tuần này. Tạo tài khoản tại [my.monapay.vn](https://my.monapay.vn/auth?mode=register), nối ACB qua 4 bước với 2 lần OTP, khai webhook về phần mềm của anh chị hoặc nhóm Telegram, chuyển thử 10.000 đồng. Kẹt ở bước nào gọi 1900 636 648 trong giờ hành chính, tụi em ngồi cùng tới khi gói tin đầu tiên về đúng chỗ. Web hay phần mềm làm tại MONA thì tụi em cài luôn. Không tính thêm. ## Câu hỏi thường gặp ### Tự động xác nhận thanh toán hosting cần những gì? Một tài khoản ACB đứng tên anh chị, số điện thoại nhận OTP, và phần mềm quản lý hosting có một địa chỉ nhận webhook. MONA Pay cấp tài khoản ảo riêng cho từng hoá đơn, tiền vào là gửi gói tin về địa chỉ đó để phần mềm tự gia hạn, còn tiền vẫn nằm nguyên trong tài khoản ngân hàng của anh chị. ### Khách gõ sai nội dung chuyển khoản thì có khớp được không? Được. Máy khớp bằng số tài khoản ảo riêng của từng hoá đơn, không dựa vào nội dung khách gõ, nên khách để trống nội dung vẫn khớp trong vài giây. ### Webhook về phần mềm có an toàn không? Mỗi gói tin ký HMAC-SHA256 bằng khoá riêng của anh chị, kèm dấu thời gian, phần mềm nhận từ chối gói lệch giờ quá 5 phút và phải trả lời trong 10 giây. Mã giao dịch không đổi qua các lần gửi lại. Không xử lý trùng. ### Tôi bán khoá học hay phần mềm chứ không phải hosting thì dùng được không? Được. Cùng cơ chế đó tụi em dùng cho học phí Khánh Hùng Academy và tiền phần mềm Mona.Software. ### Có mất phí không? Không. MONA Pay miễn phí hoàn toàn, không giới hạn giao dịch, xem [bảng giá](/bang-gia). --- # Vì sao MONA Pay miễn phí hoàn toàn, không thu phần trăm trên tiền của anh chị > Tụi em nói thẳng vì sao MONA Pay miễn phí không giới hạn giao dịch: hạ tầng đã chạy hơn 4 năm cho 6.000+ khách MONA, tiền không qua trung gian, MONA sống bằng web, hosting, phần mềm. > URL: https://monapay.vn/blog/vi-sao-mona-pay-mien-phi · Markdown: https://monapay.vn/blog/vi-sao-mona-pay-mien-phi.md · Đăng 29/08/2026 MONA Pay miễn phí hoàn toàn: không phí mở tài khoản, không phí nối ngân hàng, không thu phần trăm trên mỗi giao dịch, không giới hạn số giao dịch mỗi tháng. Tụi em làm được vậy vì hệ thống này đã chạy hơn 4 năm để thu tiền cho chính MONA và cho hơn 6.000 khách hàng mới của MONA từ 2022, hạ tầng có sẵn, tiền không đi qua tụi em nên không có phí trung gian, và MONA sống bằng làm web, hosting, phần mềm chứ không sống bằng phí thanh toán. Tụi em thấy khách đến MONA hỏi câu này nhiều nhất, nhiều hơn cả hỏi về tính năng, nên bài này nói rõ từng lý do, kèm cả giới hạn duy nhất tụi em đặt ra và cả 3 thứ tụi em chưa làm được. Miễn phí thật, 0 đồng. ## Khách đến MONA hỏi câu đầu tiên: miễn phí thì tiền ở đâu ra Từ lúc mở [monapay.vn](/) cho người ngoài, câu tụi em nghe nhiều nhất là "miễn phí thật hả, vậy mấy anh sống bằng gì". Hỏi vậy là đúng, vì ở Việt Nam chữ miễn phí hay đi kèm một chỗ thu tiền giấu ở đâu đó: miễn phí mở tài khoản nhưng thu 1,5% mỗi giao dịch, miễn phí 3 tháng đầu rồi tính gói, miễn phí tới 50 đơn rồi khoá. Tụi em trả lời thế này với từng khách. MONA làm web, phần mềm và hosting từ năm 2016, tới giờ hơn 14.000 dự án và 85% khách quay lại. Doanh thu của tụi em nằm ở ba mảng đó. MONA Pay sinh ra để khâu thu tiền trong web và phần mềm tụi em giao chạy trọn vẹn, tức là nó làm sản phẩm chính bán được hơn, chứ bản thân nó không cần bán. Nhìn theo cách đó thì thu phí trên từng giao dịch của anh chị là tự đánh vào cái tụi em đang bán. Nói ngắn gọn thì MONA Pay là một phần của dịch vụ MONA giao cho khách, chứ không phải một dịch vụ tách riêng để tính tiền, và tụi em muốn giữ nó đúng vai đó. Anh chị xem [bảng giá](/bang-gia) sẽ thấy đúng một gói. Giá 0 đồng, đủ tính năng. ## Lý do một: hạ tầng đã chạy hơn 4 năm cho 6.000+ khách, thêm khách gần như không thêm chi phí Hệ thống bên dưới MONA Pay chạy từ hơn 4 năm trước để thu phí hosting của Mona.Host, học phí của Khánh Hùng Academy với 40.000 học viên, và tiền đợt của từng đơn phần mềm Mona.Software. Máy chủ, cơ sở dữ liệu, kết nối ngân hàng, đội kỹ sư trực đều đã có từ lâu vì tụi em cần cho việc của mình trước. Từ năm 2022, hơn 6.000 khách hàng mới của MONA thu tiền qua đúng hệ thống này trong web, phần mềm tụi em giao. Cái gì cũng sẵn rồi. Việc một giao dịch đi qua hệ thống này rất nhẹ. Ngân hàng gửi một thông báo, hệ thống khớp đơn, bắn một gói tin webhook tới máy nhận của anh chị và một tin nhắn Telegram, xong. Không xử lý thẻ, không giữ tiền, không đối soát dòng tiền của ai. Thêm một nghìn tài khoản dùng thì phần việc máy tăng lên không đáng kể so với thứ tụi em đang chạy sẵn cho hệ 14.000+ dự án. Vậy nên không có lý do gì để đẩy một khoản chi phí gần bằng không thành một mức phí phần trăm. Tụi em nói rõ: đây là lý do tụi em không thu, chứ tụi em không hứa rằng chi phí vận hành bằng không tuyệt đối, vì máy chủ vẫn tốn điện và kỹ sư vẫn tốn lương như mọi công ty khác. Chỉ là nó nhỏ tới mức tụi em coi là phần việc của mình. Giống như tiền điện cho cái máy đã bật sẵn. ## Lý do hai: tiền không đi qua MONA Pay, nên không có phí trung gian nào để thu Đây là điểm khác về bản chất so với ví điện tử hay cổng thẻ. Khi khách của anh chị quét mã VietQR và chuyển khoản, tiền đi thẳng từ tài khoản của khách vào tài khoản ACB của anh chị. MONA Pay chỉ nhận thông báo giao dịch từ ngân hàng, khớp với đơn, rồi báo cho web hoặc nhóm Telegram. Tụi em chưa từng cầm một đồng nào của anh chị, kể cả 1 giây. Với cổng giữ tiền thì mọi thứ khác hẳn. Tiền vào ví hoặc tài khoản đối soát của bên cổng trước, họ trừ phí phần trăm theo hợp đồng, rồi chuyển phần còn lại về ngân hàng của anh chị theo chu kỳ. Với cổng quốc tế như PayPal, biểu phí công bố mà tụi em kiểm ngày 28/08/2026 ghi 4,40% cộng phí cố định cho mỗi giao dịch thương mại từ ngoài Việt Nam, thêm 60.000 đồng mỗi lần rút về ngân hàng trong nước. Họ thu được vì họ đứng giữa dòng tiền. Tụi em không đứng giữa. Không cầm tiền thì không có phí giữ tiền, không có phí rút, không có chu kỳ chờ tiền về. Anh chị đọc thêm so sánh ở bài [cổng thanh toán quốc tế](/cong-thanh-toan-quoc-te) để thấy hai bài toán khác nhau thế nào. Anh chị muốn kiểm chứng thay vì tin lời tụi em? Đăng ký tại [my.monapay.vn](https://my.monapay.vn/auth?mode=register), nối ACB, chuyển thử 10.000 đồng vào tài khoản ảo. Tiền nằm nguyên trong ACB của anh chị, tin báo có về Telegram, và không có dòng phí nào. ## Lý do ba: khách dùng MONA Pay là khách tụi em muốn làm web và phần mềm cùng Tụi em không giấu chuyện này. Người đăng ký MONA Pay là chủ shop, chủ trung tâm, chủ phần mềm, tức đúng nhóm khách MONA vẫn làm web bán hàng, [cổng thanh toán WooCommerce](/cong-thanh-toan-woocommerce), phần mềm quản lý suốt 10 năm nay. Khi anh chị thấy khâu thu tiền chạy ổn, tới lúc cần làm web mới hay viết phần mềm quản lý thì tụi em là cái tên anh chị nhớ tới. Đó là cách MONA Pay trả tiền cho tụi em, gián tiếp và tự nguyện. Cách này không có gì mới với MONA. Mona.Host bán hosting cũng vậy, tụi em tự thu phí hosting bằng đúng hệ thống này nên cài cho khách hosting là chuyện đương nhiên. Khánh Hùng Academy thu học phí của 760 học viên PRO cũng bằng nó. Khi tụi em tự dùng thứ mình cho không, anh chị dễ tin hơn là khi tụi em bán thứ mình không xài. Còn nếu anh chị chỉ dùng MONA Pay rồi thôi, không bao giờ làm web hay phần mềm với MONA, cũng không sao, vì tụi em đã tính phần đó vào từ lúc quyết định mở ra cho người ngoài. Không có điều kiện ngầm. ## Miễn phí thì có giới hạn gì, và giới hạn đó là gì Có đúng một giới hạn, gọi là fair-use, để chặn lạm dụng chứ không để bán gói cao hơn. Anh chị bắn API dồn dập hàng nghìn lần một phút, tạo tài khoản hàng loạt bằng máy, hoặc dùng hệ thống cho việc không phải nhận tiền của chính mình thì tụi em chặn. Dùng thật bao nhiêu giao dịch cũng không chạm ngưỡng, và tụi em không công bố một con số giới hạn thương mại nào vì không có. Còn lại là đủ tính năng, không có tính năng nào giấu sau gói trả phí. Tụi em liệt kê để anh chị đối chiếu. Tài khoản ảo ACB riêng cho từng đơn, mã VietQR động điền sẵn số tiền, webhook ký HMAC-SHA256 với gói tin từ chối nếu lệch giờ quá 5 phút, thông báo Telegram theo từng nhóm, khoá API có hạn 24 giờ, tra giao dịch 100 dòng mỗi trang, bảng điều khiển xem lại từng lần gửi webhook. Hỗ trợ là người thật qua 1900 636 648 trong giờ hành chính. Tụi em có bán một thứ liên quan, nhưng không phải phí dùng: dịch vụ kèm theo cho doanh nghiệp cần kỹ sư MONA ngồi tích hợp trực tiếp vào phần mềm có sẵn, hỗ trợ ưu tiên và hoá đơn. Cái đó là công người, tính riêng, và anh chị không dùng cũng không mất tính năng nào. Tính năng không bao giờ bị khoá. ## Cái tụi em chưa có, nói trước để anh chị khỏi kỳ vọng sai Miễn phí không có nghĩa là làm được mọi thứ. ACB đang hoạt động, MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang đăng ký kết nối (bảng trạng thái ở [ngân hàng hỗ trợ](/ngan-hang)), tụi em không hứa ngày. Gửi lại webhook tự động tối đa 7 lần đang triển khai, hiện anh chị bấm gửi lại từ bảng điều khiển khi máy nhận lỗi. Không có thẻ quốc tế, không có trả góp, không có ví. Anh chị bán ra nước ngoài thì vẫn cần một cổng như PayPal chạy song song, tụi em viết rõ ở bài [PayPal tại Việt Nam](/paypal-viet-nam). Nếu anh chị cần nhiều ngân hàng ngay hôm nay, hoặc cần nhận thẻ Visa của khách nước ngoài, thì MONA Pay chưa phải lựa chọn, và tụi em thà nói thẳng ở đây còn hơn để anh chị mất 5 phút đăng ký rồi thất vọng. Cái gì chưa có thì nói chưa có. Vậy cho nhẹ. ## Anh chị muốn dùng thì bắt đầu thế nào Nếu anh chị đang trả phí phần trăm cho một cổng chỉ để nhận chuyển khoản trong nước, hoặc đang trả lương cho một người ngồi canh app ngân hàng, thì hôm nay có cách không tốn đồng nào. Vào [my.monapay.vn](https://my.monapay.vn/auth?mode=register) tạo tài khoản, đi 4 bước nối ACB với 2 lần OTP về số điện thoại chủ tài khoản, khai nhóm Telegram hoặc địa chỉ webhook, xong. Cần kỹ sư MONA làm cùng thì gọi 1900 636 648 trong giờ hành chính, tụi em ngồi tới khi tin báo có đầu tiên về nhóm của anh chị. Không tốn đồng nào. ## Câu hỏi thường gặp ### MONA Pay miễn phí tới khi nào? Không có mốc hết hạn. Miễn phí hoàn toàn, không giới hạn giao dịch, chỉ có giới hạn chống lạm dụng, và hệ thống này đã chạy hơn 4 năm cho chính MONA và hơn 6.000 khách MONA trước khi mở ra nên không phải chương trình khuyến mãi tính theo tháng. ### Có thu phần trăm trên số tiền giao dịch không? Không. Tiền vào thẳng tài khoản ACB của anh chị trong vài giây sau khi khách chuyển, MONA Pay không cầm tiền nên không có gì để trừ, khác với cổng quốc tế thu 4,40% cộng phí cố định mỗi giao dịch. ### Có tính năng nào phải trả tiền mới mở không? Không, đủ tính năng ở gói duy nhất, còn dịch vụ kèm theo như kỹ sư tích hợp trực tiếp, hỗ trợ ưu tiên, hoá đơn là công người, tính riêng và không bắt buộc. ### MONA kiếm tiền bằng gì? Bằng làm web, phần mềm theo yêu cầu và hosting, ba mảng tụi em làm từ năm 2016. MONA Pay giúp ba mảng đó chạy trọn vẹn hơn. ### Fair-use là gì? Là giới hạn chống lạm dụng: bắn API dồn dập, tạo tài khoản hàng loạt, dùng cho việc không phải nhận tiền của chính mình. Dùng thật bao nhiêu cũng không chạm ngưỡng. --- # VietQR là gì? Mã QR ngân hàng điền sẵn số tiền và nội dung hoạt động ra sao > VietQR là chuẩn mã QR chuyển khoản chung của ngân hàng Việt Nam, app nào quét cũng ra đúng tài khoản, số tiền, nội dung. Tụi em giải thích mã tĩnh, mã động. > URL: https://monapay.vn/blog/vietqr-la-gi · Markdown: https://monapay.vn/blog/vietqr-la-gi.md · Đăng 29/08/2026 VietQR là chuẩn mã QR chuyển khoản dùng chung cho các ngân hàng Việt Nam, xây trên định dạng EMVCo: trong mã có mã ngân hàng, số tài khoản, số tiền và nội dung, app ngân hàng nào quét cũng điền sẵn lệnh chuyển qua Napas 247. Khách đến MONA làm web bán hàng hỏi tụi em VietQR là gì thì tụi em trả lời gọn vậy, rồi chỉ luôn vì sao mã động theo từng đơn mới là thứ giúp tiền tự khớp. ## Trong một mã VietQR có gì mà app nào quét cũng hiểu? Mã VietQR thực chất là một chuỗi ký tự được vẽ thành ô vuông. Chuỗi đó viết theo định dạng EMVCo, mỗi mục có mã số, độ dài và giá trị nối đuôi nhau. Mục quan trọng nhất ghi mã BIN của ngân hàng nhận, 6 chữ số, ví dụ ACB là 970416, Vietcombank là 970436, kèm số tài khoản nhận, tụi em kiểm danh sách BIN ngày 28/08/2026. Sau đó là mã tiền tệ 704 tức đồng Việt Nam, số tiền nếu có, nội dung chuyển khoản nếu có, và cuối cùng là 4 ký tự kiểm tra CRC để app biết mã không bị hỏng. Chuẩn chung, app nào cũng đọc. Vì là chuẩn chung do Napas ban hành nên app của 36 ngân hàng trong danh sách tụi em hỗ trợ đều đọc được, khách dùng app nào cũng vậy. Đây là điểm khác với mã QR của từng ví điện tử, chỉ app ví đó mới quét được. Khách không cần cài thêm gì, có app ngân hàng là đủ, và tụi em thấy đó là lý do lớn nhất khiến chuyển khoản QR thắng ví điện tử ở khâu thanh toán đơn hàng trong nước: không ai phải hỏi khách có ví X hay không. Tụi em tự viết bộ tạo mã chạy trên trình duyệt tại [công cụ tạo mã QR ngân hàng](/tao-ma-qr-ngan-hang), và trước khi công bố đã kiểm bằng cách giải mã ngược: mã tạo ra được đọc lại đúng chuỗi, 4 ký tự CRC khớp, thử 3 trường hợp gồm mã tĩnh, mã có số tiền 2.500.000 đồng và mã có nội dung 10 ký tự. Số tài khoản không rời khỏi trình duyệt của anh chị, tạo xong trong 1 giây, tải PNG in được từ 4 cm mỗi cạnh. ## Mã VietQR tĩnh và mã động khác nhau ở chỗ nào? Mã tĩnh chỉ chứa ngân hàng và số tài khoản. Khách quét xong tự gõ số tiền, tự gõ nội dung. Đây là mã dán ở quầy, in trên biển, mã của rất nhiều quán hiện nay. Rẻ, dễ, nhưng khách gõ sai là chuyện thường. Sai một số là lệch đơn. Tụi em từng thấy khách chuyển 250.000 đồng thay vì 2.500.000 đồng, hoặc gõ nội dung "chuyen tien" thay vì mã đơn, người bán ngồi dò sao kê không biết của ai. Mã động sinh riêng cho từng đơn, điền sẵn số tiền và nội dung, ví dụ 2.500.000 đồng với nội dung DH10234. Khách chỉ bấm xác nhận, mất chừng 5 giây. Vì nội dung là mã đơn nên khi tiền vào, phần mềm đọc thông báo là khớp được ngay đơn nào, không cần người dò. Ở MONA, học phí [Khánh Hùng Academy](https://khanhhung.academy) với 40.000 học viên thu theo cách này hơn 4 năm nay: mỗi học viên một mã, tiền vào là khoá mở. Bảng dưới là cách tụi em hay giải thích cho khách chọn: | Tình huống | Mã tĩnh | Mã động từng đơn | |---|---|---| | Quán nhỏ, vài đơn mỗi ngày | Đủ dùng | Chưa cần | | Shop online 50 đơn mỗi ngày | Dò sao kê mệt | Tự khớp đơn | | Khách gõ sai số tiền | Xảy ra thường | Không xảy ra | | Cần phần mềm tự xác nhận | Không làm được | Làm được | ## Mã VietQR động ai sinh ra, sinh lúc nào? Câu này quyết định tự động hoá có thật hay không. Sinh mã động bằng tay từng đơn thì vẫn là người làm, chỉ đổi từ gõ số tiền sang gõ mã. MONA Pay sinh mã tự động qua API ngay khi đơn được tạo: phần mềm của anh chị gọi một lệnh với mã đơn, số tiền, nội dung, MONA Pay trả về mã QR động gắn với tài khoản ảo ACB, phần mềm hiện mã cho khách. Tất cả trong 1 giây. Đường đi cụ thể tụi em ghi ở [tài liệu QR thanh toán](/docs/api/qr-thanh-toan): gọi POST tạo mã với `orderId`, `amount` tối đa 1.000.000.000 đồng, `description` tối đa 255 ký tự; huỷ mã khi đơn bị huỷ. Với web làm tại MONA, phần này tụi em cài sẵn khi giao, khách chỉ nối tài khoản ACB theo 4 bước với 2 lần OTP, mất chừng 5 phút. Dashboard my.monapay.vn cũng có màn Tạo QR để anh chị sinh tay khi cần, ví dụ đơn đặt qua điện thoại. > Anh chị muốn thấy mã động chạy thật: tạo tài khoản [MONA Pay](https://my.monapay.vn/auth?mode=register), nối ACB 4 bước, vào màn Tạo QR sinh một mã 10.000 đồng rồi tự quét bằng app ngân hàng của mình. Tiền vào, tin Telegram báo, webhook bay về. Miễn phí hoàn toàn, không giới hạn giao dịch. ## Quét VietQR xong, tiền đi đâu và ai xác nhận? Tiền đi thẳng từ tài khoản của khách sang tài khoản ngân hàng của anh chị qua Napas 247, không qua ví nào, không qua MONA Pay. Đây là lý do VietQR gần như không có phí cho người nhận, khác hẳn cổng thanh toán quốc tế: PayPal thu 4,40% cộng phí cố định cho giao dịch thương mại từ nước ngoài và 60.000 đồng mỗi lần rút về ngân hàng Việt Nam, theo biểu phí tụi em kiểm ngày 28/08/2026. Tiền vào thẳng tài khoản, không mất phần trăm nào. Phần xác nhận mới là chỗ khác nhau giữa quán dùng mã tĩnh và shop dùng MONA Pay. Với mã tĩnh, anh chị mở app ngân hàng xem. Với tài khoản ảo ACB nối vào MONA Pay, ACB báo có giao dịch, MONA Pay đọc thông báo, bắn webhook về website và gửi tin vào nhóm Telegram gồm tên ngân hàng, số tài khoản, số tiền, thời gian, nội dung. Thu ngân thấy tin là giao hàng. Kế toán cuối ngày mở dashboard, thay vì mở sao kê. Tụi em nói rõ giới hạn: tới 28/08/2026 MONA Pay mới nối ACB, phần tự xác nhận cần tài khoản ACB đứng tên anh chị. Các ngân hàng khác đang trong quá trình đăng ký kết nối, bảng trạng thái ở [ngân hàng hỗ trợ](/ngan-hang). Mã VietQR cho ngân hàng khác thì tool tạo được bình thường, chỉ phần tự báo là chưa. ## Những lỗi tụi em gặp khi khách tự làm VietQR Lỗi thứ nhất là nội dung có dấu tiếng Việt hoặc ký tự lạ. Nhiều app ngân hàng cắt hoặc từ chối nội dung như vậy, khách chuyển được nhưng nội dung về trống, phần mềm không khớp đơn. Tụi em giới hạn nội dung 25 ký tự không dấu trong tool, và trong API thì phần mềm nên sinh mã đơn dạng DH10234. Lỗi thứ hai là dùng mã tĩnh rồi kỳ vọng tự động. Không có mã đơn trong nội dung thì không có cách nào khớp, dù phần mềm giỏi tới đâu. Lỗi thứ ba là tạo tài khoản ảo xong mà quên đăng ký nhận thông báo giao dịch, một bước cần OTP lần hai từ ACB, tiền vào mà hệ thống im lặng. Chính tụi em từng vấp lỗi này khi triển khai, và sửa wizard nối ngân hàng thành 4 bước liền mạch với 2 lần OTP, thiếu 1 lần là tiền vào không báo. Vấp rồi mới hiểu. Anh chị đi sau thì đỡ vấp đúng chỗ tụi em đã vấp. ## Câu hỏi thường gặp ### VietQR có phải là ví điện tử không? Không. VietQR chỉ là cách đóng gói thông tin chuyển khoản vào mã QR, và tiền đi thẳng từ tài khoản ngân hàng của khách sang tài khoản ngân hàng của anh chị, không nằm trong ví trung gian nào. ### Tạo mã VietQR có mất phí không? Không. Tool tại monapay.vn tạo miễn phí, chạy trên trình duyệt, không lưu số tài khoản, tạo bao nhiêu mã cũng được. Không cần đăng ký. Phí chuyển khoản nếu có là phí phía ngân hàng của người chuyển theo biểu phí ngân hàng đó. ### Mã VietQR có hạn dùng không? Mã tĩnh dùng vô thời hạn chừng nào số tài khoản còn hoạt động. Mã động do MONA Pay sinh gắn với một đơn, anh chị huỷ mã qua API hoặc dashboard khi đơn bị huỷ để tránh khách chuyển nhầm cho đơn cũ. ### Khách ở nước ngoài quét VietQR được không? Không, VietQR chỉ chạy giữa các ngân hàng Việt Nam qua Napas 247. Khách nước ngoài trả bằng thẻ hay PayPal, tụi em phân tích ở [bài cổng thanh toán quốc tế](/cong-thanh-toan-quoc-te). ### Làm sao để mã VietQR tự xác nhận tiền vào? Nối tài khoản ACB vào MONA Pay theo 4 bước với 2 lần OTP, dùng tài khoản ảo và mã động theo đơn, rồi nhận webhook hoặc Telegram trong vài giây. Hướng dẫn tại [trang nối ACB](/acb). ## Từ mã QR trên biển tới mã tự khớp đơn, chỉ cách nhau một lần nối ACB Anh chị đang dán mã tĩnh ở quầy thì cứ giữ, nó vẫn chạy. Còn muốn đơn tự xác nhận thì tạo tài khoản tại [my.monapay.vn/auth?mode=register](https://my.monapay.vn/auth?mode=register), nối ACB 4 bước với 2 lần OTP, sinh mã động cho đơn đầu tiên. Mất khoảng 5 phút. Kẹt ở bước nào, gọi 1900 636 648 giờ hành chính, tụi em làm cùng tới khi tin đầu tiên báo về. --- # Webhook là gì? Hiểu bằng ví dụ tiền vào tài khoản ngân hàng > Webhook là cách máy chủ tự báo sang máy chủ khác ngay khi có sự kiện, ví dụ tiền vào tài khoản ACB. Tụi em giải thích bằng gói tin thật, chữ ký HMAC, gửi lại. > URL: https://monapay.vn/blog/webhook-la-gi · Markdown: https://monapay.vn/blog/webhook-la-gi.md · Đăng 29/08/2026 Webhook là cách một hệ thống tự gửi thông báo sang hệ thống khác ngay khi có sự kiện, bằng một yêu cầu HTTP POST tới địa chỉ do anh chị khai sẵn. Khách đến MONA làm web hay hỏi tụi em webhook là gì, và ví dụ tụi em hay dùng nhất là tiền vào tài khoản ACB: ngân hàng báo, MONA Pay lập tức POST một gói JSON về website của anh chị trong vòng 10 giây, đơn hàng tự đổi trạng thái, không ai phải mở app ngân hàng. ## Webhook khác gì cách "hỏi đi hỏi lại" mà nhiều phần mềm đang làm? Trước khi có webhook, cách phổ biến để biết một việc đã xảy ra chưa là hỏi liên tục, dân kỹ thuật gọi là polling. Phần mềm cứ mỗi 30 giây lại gọi sang hỏi "có giao dịch mới không", 2.880 lần một ngày, đa số lần trả về là không có gì. Tốn tài nguyên, mà tin vẫn chậm tới 30 giây. Webhook đảo chiều. Bên có sự kiện chủ động gọi sang bên cần biết, đúng lúc sự kiện xảy ra, và chỉ gọi khi có chuyện, nên máy chủ hai bên rảnh gần như cả ngày mà tin vẫn tới trong vài giây. Ở MONA, hệ thống nhận tiền chạy hơn 4 năm cho chính tụi em và hơn 6.000 khách hàng MONA từ 2022 đi theo đúng nguyên tắc này. Tiền vào tài khoản ảo ACB, ACB báo cho MONA Pay, MONA Pay POST về phần mềm thu phí hosting của [Mona.Host](https://mona.host), hoá đơn tự chuyển sang đã thanh toán và dịch vụ tự gia hạn. Cả chuỗi tính bằng giây, và không có ai ngồi bấm F5 trang sao kê, kể cả khi khách gia hạn lúc 2 giờ sáng. Điểm khác biệt quan trọng thứ hai là hướng kết nối. Với polling, website của anh chị phải biết cách gọi API ngân hàng, giữ token, xử lý giới hạn số lần gọi. Với webhook, website chỉ cần mở một địa chỉ nhận, ví dụ `https://shop.vn/webhook/monapay`, phần còn lại là việc của bên gửi. Việc nhẹ đi rất nhiều, và đó là lý do webhook thành cách chuẩn để nối thanh toán, vận chuyển, chat, gần như mọi dịch vụ có sự kiện. ## Một webhook thật trông ra sao: payload MONA Pay gửi về Tụi em lấy đúng gói tin MONA Pay đang gửi cho khách, kiểm ngày 28/08/2026, để anh chị nhìn thay vì tưởng tượng. Khi có tiền vào, MONA Pay POST một chuỗi JSON gồm 7 trường: ```json {"amount":2500000,"description":"DH10234 NGUYEN VAN A","transfer_date":"10:30:00 28/08/2026","transaction_code":"FT26240001234","account_number":"1234567890","bank_name":"ACB","type":"income"} ``` Bảy trường, đủ dùng. Mỗi trường có một việc riêng và phần mềm chỉ cần đọc đúng trường mình cần. `amount` là số tiền, `description` là nội dung khách gõ khi chuyển, `transaction_code` là mã giao dịch phía ngân hàng, `account_number` là số tài khoản ảo nhận tiền. Phần mềm của anh chị đọc `description`, thấy DH10234, tìm đơn DH10234, thấy số tiền 2.500.000 đồng khớp, đánh dấu đã thanh toán, cả vòng chưa tới 10 giây kể từ lúc ACB báo. Một chi tiết mà khách MONA hay bỏ qua rồi sau này hối: `transaction_code` không đổi qua mọi lần gửi lại. Nghĩa là nếu cùng một giao dịch được gửi hai lần, phần mềm phải dùng mã này làm khoá để không ghi nhận tiền hai lần. Tụi em từng thấy web bán hàng cộng đôi doanh thu chỉ vì thiếu một dòng kiểm tra trùng, và phải ngồi đối chiếu lại cả tháng để tìm ra 3 đơn bị tính 2 lần. Thiếu là cộng đôi. ## Làm sao biết webhook thật là của MONA Pay, không phải ai đó giả mạo? Đây là câu tụi em thấy dân kỹ thuật hỏi ngay sau khi hiểu webhook là gì, và hỏi là đúng, vì địa chỉ nhận webhook nằm trên internet, ai cũng gọi được. Nếu phần mềm tin mọi gói tin POST tới, kẻ xấu chỉ cần gửi một JSON giả với `amount` 50.000.000 đồng là đơn được mở khoá. MONA Pay ký mỗi gói tin bằng HMAC-SHA256. Trong header có `X-Mona-Timestamp` là mốc thời gian, và `X-Mona-Signature` là chữ ký tính từ khoá bí mật của anh chị cộng với chuỗi "mốc thời gian.nội dung gói tin". Phần mềm nhận tính lại chữ ký bằng cùng khoá, khớp thì tin. Sai thì bỏ. Kèm theo đó là luật chống phát lại: gói tin có mốc thời gian lệch quá 5 phút so với giờ máy nhận thì từ chối, dù chữ ký đúng. Công thức chữ ký công bố đầy đủ trong [trang bảo mật webhook](/docs/webhooks/bao-mat), anh chị tự kiểm lại được bằng lệnh cURL. Khoá bí mật đó anh chị tự đặt trong dashboard khi khai webhook, không ai khác biết, kể cả tụi em cũng chỉ lưu bản đã băm. Giữ nó như giữ mật khẩu ngân hàng, và đổi khoá ngay nếu nghi ngờ đã lộ, MONA Pay cho đổi trong 1 phút không cần khai lại địa chỉ. > Muốn nhìn một webhook thật chạy trên chính website của mình, anh chị tạo tài khoản [MONA Pay](https://my.monapay.vn/auth?mode=register), khai địa chỉ nhận, bấm gửi thử trong dashboard. Gói tin mẫu bay về trong vài giây, miễn phí hoàn toàn, không giới hạn giao dịch, không cần chờ ai duyệt. ## Webhook gửi tới mà website không nhận được thì sao? Đây là phần tách người làm thật với người chỉ đọc định nghĩa. Webhook là một yêu cầu HTTP, và HTTP thì có lúc hỏng: máy chủ của anh chị đang khởi động lại, chứng chỉ SSL hết hạn, tên miền trỏ sai, hay code xử lý bị lỗi trả về 500. Sự kiện đã xảy ra mà bên nhận không biết, tiền vào mà đơn không mở. MONA Pay xử lý chuyện này bằng ba lớp. Lớp một là luật thành công rõ ràng: máy nhận phải trả mã 200, 201 hoặc 202 trong vòng 10 giây, khác đi là tính thất bại. Lớp hai là lịch sử từng lần gửi trong dashboard, mỗi dòng ghi mã HTTP, thời gian phản hồi và nhãn lỗi thuộc loại nào: TIMEOUT, SSL, DNS, CONNECTION, HTTP_4XX hay HTTP_5XX. Nhìn nhãn là biết sửa ở đâu. Lớp ba là gửi lại: anh chị bấm gửi lại từ dashboard, còn phần gửi lại tự động tối đa 7 lần tụi em đang triển khai, công bố tại [trang gửi lại và xử lý lỗi](/docs/webhooks/gui-lai-va-xu-ly-loi) khi lên. Kinh nghiệm tụi em rút ra sau nhiều lần nối cho khách: trả 200 ngay khi nhận, rồi mới xử lý đơn. Nếu code xử lý đơn chạy 15 giây mới trả, MONA Pay đã tính thất bại ở giây thứ 10 dù đơn vẫn được mở, và lịch sử gửi sẽ đầy lỗi giả. Nhận trước rồi làm sau, đó là luật tụi em áp cho mọi endpoint. ## Tự viết phần nhận webhook có khó không, mất bao lâu? Không khó, dev quen PHP hay Node làm trong khoảng 2 giờ là chạy. Một endpoint nhận webhook đúng chuẩn gồm 4 việc: đọc nguyên văn nội dung gói tin, kiểm chữ ký HMAC và mốc thời gian, trả 200 ngay, rồi đọc JSON để xử lý đơn. Với PHP hay Node, tụi em viết mẫu sẵn khoảng 30 dòng trong [tài liệu tích hợp webhook](/docs/webhooks/tich-hop-webhook), có cả lệnh cURL để giả lập MONA Pay bắn vào máy của anh chị mà chưa cần chuyển tiền thật. Web làm tại MONA thì phần này tụi em cài luôn khi giao, khách chỉ việc nối tài khoản ACB theo 4 bước trong dashboard. Anh chị không phải thuê thêm ai cho phần này. Web do đội khác làm thì dev của anh chị đọc tài liệu, khoảng 2 giờ là xong, thử bằng lệnh cURL trong 5 phút, hoặc nếu dùng WooCommerce thì xem thẳng [trang cổng thanh toán WooCommerce](/cong-thanh-toan-woocommerce). Còn AI agent như Claude Code, Codex đọc được tài liệu dạng máy tại [llms.txt](/llms.txt) và tự viết phần nhận, tụi em thiết kế tài liệu cho đúng việc đó. Tụi em không khuyên ráp webhook nhận tiền bằng công cụ kéo thả kiểu Zapier, Make, n8n. Với việc đếm like thì được, với tiền thì một lần dịch vụ trung gian chập chờn là mất đơn, mà anh chị không biết mất lúc nào. Tiền phải đi thẳng vào phần mềm. ## Câu hỏi thường gặp ### Webhook và API khác nhau chỗ nào? API là cửa để anh chị chủ động gọi sang hỏi hoặc ra lệnh, còn webhook là bên kia chủ động gọi sang báo khi có sự kiện. MONA Pay có cả hai: API để tạo tài khoản ảo, tạo mã QR, tra giao dịch tối đa 100 dòng mỗi trang, và webhook để báo tiền vào ngay khi ACB thông báo. Hai cửa bổ nhau. ### Webhook có cần máy chủ riêng không? Cần một địa chỉ HTTPS nhận được yêu cầu POST, tức website hay phần mềm của anh chị đang chạy trên máy chủ. Web WordPress đang chạy trên hosting thường là đủ điều kiện. Máy chạy trong mạng nội bộ không có địa chỉ công khai thì MONA Pay không gọi tới được, khi đó dùng Telegram để nhận báo tiền vào là cách đơn giản hơn. Đường Telegram thì không cần máy chủ hay địa chỉ công khai nào. ### Nhận webhook thì có phải trả lời gì không? Phải trả mã HTTP 200, 201 hoặc 202 trong 10 giây để MONA Pay ghi nhận là thành công. Không cần nội dung gì đặc biệt trong phản hồi, nhưng trả ngay rồi xử lý đơn sau. Trả mã trước rồi mới xử lý đơn phía sau, đơn nặng thì đẩy vào hàng đợi. ### Một giao dịch có bị gửi hai lần không? Có khi anh chị bấm gửi lại hoặc khi hai cấu hình webhook cùng trỏ một địa chỉ. `transaction_code` giữ nguyên qua các lần gửi, phần mềm dùng nó làm khoá duy nhất để không ghi nhận tiền hai lần. Một khoá duy nhất cho một giao dịch, ghi một lần. ### Webhook của MONA Pay có mất phí không? Không. MONA Pay miễn phí hoàn toàn, không giới hạn số giao dịch lẫn số webhook, tiền vào thẳng tài khoản ACB của anh chị, tụi em chỉ đọc thông báo rồi báo lại, không thu phần trăm trên số tiền. Chi tiết từng dòng ở [bảng giá](/bang-gia). ## Nối webhook đầu tiên trong 5 phút, không cần chờ duyệt Tạo tài khoản tại [my.monapay.vn/auth?mode=register](https://my.monapay.vn/auth?mode=register), đăng ký xong dùng ngay. Nối tài khoản ACB theo 4 bước, khai địa chỉ nhận webhook và khoá HMAC, bấm gửi thử. Rồi chuyển 10.000 đồng vào mã QR của chính mình để thấy gói tin thật bay về trong vài giây. Kẹt chỗ nào, gọi 1900 636 648 giờ hành chính, kỹ sư MONA ngồi cùng anh chị tới khi gói tin đầu tiên hiện trong lịch sử gửi. --- # Xác nhận thanh toán tự động khi khách chuyển khoản, tụi em làm thế nào > Xác nhận thanh toán tự động chạy bằng cơ chế nào, làm trong 4 bước với tài khoản ACB, lỗi hay gặp. MONA kể từ chuyện tự thu tiền hosting, học phí hơn 4 năm. > URL: https://monapay.vn/blog/xac-nhan-thanh-toan-tu-dong · Markdown: https://monapay.vn/blog/xac-nhan-thanh-toan-tu-dong.md · Đăng 29/08/2026 Khách đến MONA làm web bán hàng hay phần mềm quản lý, sau 14.000+ dự án tụi em thấy gần như ai cũng kẹt đúng một khâu: khách chuyển khoản xong, phải có người mở app ngân hàng lên coi rồi mới dám giao hàng. Xác nhận thanh toán tự động là cách bỏ hẳn người đó ra khỏi quy trình. Tiền vào tài khoản ACB, ngân hàng báo, phần mềm tự đổi trạng thái đơn trong vài giây. Không ai phải canh. Tụi em tự xây hệ thống này để thu tiền cho chính mình hơn 4 năm, giờ mở cho mọi doanh nghiệp, [miễn phí hoàn toàn](/bang-gia). ## Vì sao khách đến MONA làm web đều kẹt ở khâu xác nhận chuyển khoản? Ở chính MONA, trước khi có hệ thống này, tụi em thu phí hosting, thu học phí, thu tiền phần mềm bằng đúng cái cách mà đa số chủ shop đang làm: một nhân viên kế toán mở app ngân hàng, đối chiếu số tiền với nội dung chuyển khoản, rồi nhắn cho bộ phận kỹ thuật mở dịch vụ. Nghe đơn giản. Làm mỗi ngày mới thấy mệt. Khách chuyển lúc 10 giờ tối thì sáng hôm sau mới được xác nhận, vì không ai trực app ngân hàng ban đêm. Khách chuyển thứ bảy, chủ nhật thì chờ tới thứ hai. Hai khách cùng chuyển 500.000 đồng trong một buổi chiều, nội dung một người gõ đúng mã đơn, một người gõ mỗi tên mình, thế là phải gọi điện hỏi lại từng người. Cái mất lớn nhất là chỗ này, chứ tiền thì vẫn vào tài khoản đầy đủ, chỉ có người bị giữ chân. Khách của MONA làm web bán hàng cũng kể y hệt. Chủ shop quần áo đêm nào cũng cầm điện thoại chụp màn hình app ngân hàng gửi vào nhóm để nhân viên gói hàng. Trung tâm dạy học có hai nhân viên hành chính chỉ để đối chiếu học phí cuối tháng. Đây là chỗ mà một khâu tưởng nhỏ lại ăn mất người thật, giờ thật, và mất luôn cả những đơn khách đợi lâu quá rồi thôi. ## Xác nhận thanh toán tự động chạy bằng cơ chế gì? Có ba mảnh ghép, và tụi em dùng đủ cả ba cho khách MONA. Mảnh thứ nhất là tài khoản ảo, gọi tắt là VA: ACB cấp cho anh chị một dãy số tài khoản phụ theo đầu số đăng ký, mỗi đơn hàng hoặc mỗi khách gắn một số riêng, tiền chuyển vào số đó vẫn về tài khoản chính của anh chị. Khách chuyển đúng số là hệ thống biết tiền của đơn nào, không cần khách gõ đúng nội dung. Mảnh thứ hai là mã VietQR động. Thay vì bắt khách gõ số tài khoản, số tiền và nội dung, anh chị đưa ra một mã QR đã điền sẵn tất cả, khách quét bằng app ngân hàng nào cũng được. Sai số tiền, sai nội dung gần như không còn. Mảnh thứ ba là thông báo từ ngân hàng và webhook. Khi tiền vào, ACB gửi thông báo giao dịch tới MONA Pay theo bộ API dành cho đối tác của ACB, MONA Pay khớp giao dịch với đơn rồi gửi một gói tin HTTP về website hoặc phần mềm của anh chị. Gói tin đó tụi em ký bằng HMAC-SHA256, kèm dấu thời gian, phía anh chị từ chối nếu lệch quá 5 phút, nên không ai giả mạo được thông báo tiền vào. Máy chủ của anh chị trả mã 200, 201 hoặc 202 trong 10 giây là xong một vòng. Tiền không đi qua MONA Pay. Nó vào thẳng tài khoản ACB của anh chị, MONA Pay chỉ đọc thông báo rồi báo lại, đây là điểm tụi em nói đi nói lại với mọi khách vì ai cũng hỏi. ## Trước khi có hệ thống riêng, tụi em đã thử cách nào và vì sao bỏ? Tụi em từng thử cách mà nhiều anh chị đang tính: ráp bằng công cụ tự động không cần lập trình, đọc email thông báo của ngân hàng rồi bắn sang bảng tính. Chạy được vài tuần. Rồi ngân hàng đổi mẫu email, luồng gãy, không ai canh, ba ngày sau mới phát hiện có hai chục giao dịch chưa được xác nhận. Đó là lý do tụi em không khuyến khích đưa mấy luồng tự ráp kiểu này vào việc thu tiền thật của doanh nghiệp: không phải vì công cụ dở, mà vì bảo trì tốn người hơn cái nó tiết kiệm được, và một lỗi là mất đơn ngay. Cách thứ hai là thuê thêm người trực. Cách này chạy, nhưng chỉ chạy trong giờ hành chính. Khách của tụi em thì mua hosting lúc nửa đêm, đóng học phí cuối tuần. Nên tụi em viết phần mềm riêng, nối thẳng vào ngân hàng, để thông báo tiền vào chạy thẳng vào phần mềm quản lý chứ không đi vòng qua email hay bảng tính. Chạy hơn 4 năm, thu tiền cho [Mona.Host](https://mona.host), cho Khánh Hùng Academy với 40.000 học viên, cho đơn phần mềm của Mona.Software, rồi từ 2022 nhúng vào web bán hàng và phần mềm giao cho khách, tới nay hơn 6.000 khách hàng mới của MONA dùng. Năm 2026 tụi em mở nó ra thành [MONA Pay](/gioi-thieu) cho mọi doanh nghiệp. ## Làm xác nhận thanh toán tự động với MONA Pay trong 4 bước Tụi em đã rút quy trình xuống còn 4 bước trong dashboard, người không rành kỹ thuật vẫn tự đi được, chỉ khâu nối website là cần người làm web. Bước 1, tạo tài khoản tại [my.monapay.vn](https://my.monapay.vn/auth?mode=register). Đăng ký xong dùng ngay, không có ai duyệt, không chờ giờ làm việc. Cần đúng ba thứ: tên đăng nhập, mật khẩu, tên doanh nghiệp. Bước 2, nối tài khoản ACB. Vào mục Ngân hàng và VA, bấm nối tài khoản, nhập số tài khoản ACB đứng tên anh chị, số điện thoại đã đăng ký với ACB và loại khách hàng. ACB gửi OTP về điện thoại, nhập mã để tạo tài khoản ảo. Rồi ACB gửi OTP lần thứ hai để đăng ký nhận thông báo giao dịch. Hai lần OTP là bắt buộc, thiếu lần hai thì tiền vào không có thông báo, tụi em nói kỹ ở phần dưới vì chính tụi em từng dính. Bước 3, chọn cách nhận thông báo. Muốn cả công ty thấy tiền vào thì vào mục Telegram, thêm bot vào nhóm, dán mã nhóm, bấm gửi thử. Muốn website hay phần mềm tự xử lý đơn thì vào mục Webhooks, dán địa chỉ nhận của anh chị, chọn ký HMAC-SHA256, đặt khoá bí mật, bấm gửi thử để thấy gói tin mẫu chạy về. Bước 4, chuyển thử một khoản tiền thật. Tụi em luôn kêu khách làm bước này, đừng tin màn hình gửi thử, phải thấy tiền thật đi trọn một vòng. Nếu website của anh chị chạy WooCommerce hay WordPress, phần nhận webhook có mã mẫu sẵn ở [trang WooCommerce](/cong-thanh-toan-woocommerce) và [trang WordPress](/cong-thanh-toan-wordpress), dev dán vào là chạy. Anh chị chưa muốn đụng tới code, chỉ muốn thử xem tiền vào có báo không, thì cứ làm bước 1 tới bước 3 với Telegram trước. Không mất tiền, không giới hạn số giao dịch, cả nhóm thấy thông báo là biết hệ thống hợp với mình hay không, rồi hãy tính chuyện nối website sau. ## Hố tụi em từng vấp: tạo tài khoản ảo xong mà tiền vào không báo Kể chuyện này vì nó sẽ tiết kiệm cho anh chị một buổi chiều. Khi tự đi lại toàn bộ luồng như một khách mới: chuyển khoản vào tài khoản ảo, chờ ACB báo, xem MONA Pay ghi nhận, xem webhook bắn về máy nhận và kiểm chữ ký. Lần đầu chuyển, tiền vào tài khoản mà hệ thống im re. Thiếu một cái OTP. Nguyên nhân nằm ở phía ngân hàng, và hoàn toàn hợp lý: tạo tài khoản ảo là một dịch vụ, nhận thông báo giao dịch là một dịch vụ khác, ACB yêu cầu xác thực OTP riêng cho từng dịch vụ. Bản cũ của tụi em chỉ làm OTP lần đầu. Sửa xong, wizard nối ngân hàng đi liền 4 bước với 2 lần OTP, chuyển lại, thông báo về, webhook về, chữ ký khớp đúng công thức tụi em công bố trong [tài liệu bảo mật](/docs/webhooks/bao-mat). Từ hôm đó đến giờ quy trình là vậy, anh chị cứ làm đủ hai lần OTP là yên tâm. Bài học rút ra là đừng tin cái gì chưa chạy tiền thật. Mọi thứ trên màn hình có thể xanh hết mà tiền vẫn không báo, chỉ vì thiếu một cái OTP. ## Website và phần mềm nhận được tín hiệu rồi làm gì tiếp? Gói tin webhook của MONA Pay chỉ có 7 trường: số tiền, nội dung chuyển khoản, thời gian, mã giao dịch, số tài khoản nhận, tên ngân hàng và loại giao dịch. Phần mềm của anh chị đọc mã giao dịch, so với đơn, đổi trạng thái sang đã thanh toán, rồi làm việc tiếp theo của riêng mình: mở khoá học, kích hoạt hosting, in phiếu gói hàng, gửi hoá đơn. Với khách làm web tại MONA, tụi em nhúng sẵn phần này. Với web làm ở nơi khác, dev của anh chị đọc [hướng dẫn tích hợp webhook](/docs/webhooks/tich-hop-webhook), có mã mẫu cURL, PHP và Node, dán vào là chạy trong buổi sáng. Có một chi tiết tụi em luôn dặn: dùng mã giao dịch làm khoá chống trùng trong cơ sở dữ liệu, vì cùng một giao dịch có thể được gửi lại nhiều lần khi anh chị bấm gửi lại từ dashboard. Gửi thất bại thì sao? Mỗi lần gửi đều có lịch sử trong dashboard: mã HTTP, thời gian phản hồi và nhãn lỗi rõ ràng như TIMEOUT, SSL, DNS, HTTP_5XX. Máy chủ anh chị sập lúc 2 giờ sáng, sáng ra mở lịch sử, bấm gửi lại từng dòng là đơn được xác nhận đủ, không mất giao dịch nào vì tiền vẫn nằm ở ngân hàng, MONA Pay giữ bản ghi. Gửi lại tự động theo lịch tối đa 7 lần tụi em đang triển khai, có là công bố tại trang tài liệu. ## Xác nhận thanh toán tự động tốn bao nhiêu tiền? Với MONA Pay là 0 đồng. Miễn phí hoàn toàn, không giới hạn số giao dịch, không thu phần trăm trên số tiền, không phí mở tài khoản, không phí nối ngân hàng. Giới hạn duy nhất là fair-use chống lạm dụng, ai dùng bình thường không chạm tới. Tụi em để miễn phí được vì ba lẽ. Hạ tầng này đã chạy sẵn hơn 4 năm cho chính MONA và cho hơn 6.000 khách hàng MONA từ 2022, mở thêm người dùng gần như không tốn thêm. Tiền không đi qua MONA Pay nên không có phí trung gian để thu. Và MONA sống bằng làm web, hosting, phần mềm, cổng thanh toán tự động chỉ làm mấy thứ đó chạy trọn vẹn hơn. Để anh chị dễ hình dung, cổng thanh toán quốc tế như PayPal thu 4,40% cộng phí cố định cho mỗi giao dịch thương mại từ ngoài Việt Nam, rút tiền về ngân hàng Việt Nam thêm 60.000 đồng mỗi lần, theo biểu phí PayPal công bố mà tụi em kiểm ngày 28/08/2026. Một đơn 10 triệu đồng mất hơn 440.000 đồng phí. Với khách trong nước chuyển khoản, khoản đó bằng 0. Bài so sánh đầy đủ ở [trang cổng thanh toán quốc tế](/cong-thanh-toan-quoc-te). Nếu anh chị đang trả người ngồi canh app ngân hàng, hoặc đang mất đơn vì khách chờ xác nhận quá lâu, thì thử ngay hôm nay: tạo tài khoản tại [my.monapay.vn/auth?mode=register](https://my.monapay.vn/auth?mode=register), nối ACB theo 4 bước với 2 lần OTP, chuyển thử 10.000 đồng rồi xem thông báo về Telegram. Kẹt bước nào, gọi 1900 636 648 giờ hành chính, kỹ sư MONA ngồi cùng anh chị tới khi tin đầu tiên chạy về. Web làm tại MONA thì tụi em cài luôn phần nối website, anh chị không phải chạm code. ## Câu hỏi thường gặp ### Xác nhận thanh toán tự động có cần khách cài app gì không? Không. Khách chuyển khoản bằng app ngân hàng bất kỳ, quét mã VietQR hoặc chuyển vào số tài khoản ảo. Phía tự động nằm ở anh chị, khách không thấy khác gì so với chuyển khoản bình thường. ### Tiền có đi qua MONA Pay không? Không. Tiền vào thẳng tài khoản ACB của anh chị, còn MONA Pay chỉ nhận thông báo giao dịch từ ACB theo bộ API dành cho đối tác rồi báo lại cho website, phần mềm hoặc nhóm Telegram của anh chị trong vài giây. Tụi em không giữ tiền. ### Mất bao lâu để tiền vào được xác nhận? Ngân hàng báo là hệ thống báo ngay, thường tính bằng giây. Phía website của anh chị trả lời trong 10 giây là hoàn tất một vòng; chữ ký kèm dấu thời gian, lệch quá 5 phút là từ chối, nên webhook giả không lọt. Khi tụi em tự kiểm luồng, thông báo Telegram và webhook về gần như cùng lúc. ### Tôi chưa có tài khoản ACB thì sao? Hiện ACB đang hoạt động, anh chị mở tài khoản tại ACB rồi nối vào. MB, BIDV, VietinBank, OCB, MSB, KienlongBank, TPBank đang đăng ký kết nối, bảng trạng thái ở [ngân hàng hỗ trợ](/ngan-hang); webhook, Telegram dùng chung mọi ngân hàng nên nối thêm sau không phải sửa gì. ### Có mất phí gì không? Không. MONA Pay miễn phí hoàn toàn, không giới hạn giao dịch. Phí phía ngân hàng theo biểu phí của ACB, anh chị hỏi ACB khi mở tài khoản.