A Vietnamese bank API
that an AI agent can integrate alone
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. Everything an agent needs sits on the main domain: llms.txt, a raw .md version of every docs page, openapi.json, and a prompt you paste into Claude Code, Codex or Cursor. Accounts work immediately after sign-up; the only human step is the bank's OTP.
Paste this into Claude Code, Codex or Cursor
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 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: X-Mona-Signature = "sha256=" + hex(HMAC-SHA256(secret, X-Mona-Timestamp + "." + raw_body)).
Reject if |now - X-Mona-Timestamp| > 300 seconds. Timing-safe compare. 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 within 10 seconds, process asynchronously. Match the order by account_number (VA) or the order code in description; compare amount with the order total.
5. Register the webhook: POST /api/v1/client-webhooks {name, webhook_url, auth_type:"HMAC_SHA256", secret_key} (Bearer + X-Client-Secret), then POST /api/v1/client-webhooks/test {webhook_url, auth_type, secret_key, is_dummy:true}.
Read MONA_WEBHOOK_SECRET and MONA_SECRET from environment variables; never hard-code them.Minimal webhook endpoint (cURL → PHP → Node)
# Simulate MONA Pay firing a webhook at your endpoint
SECRET='your_hmac_secret'
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":"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"<?php
$secret = getenv('MONA_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_MONA_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_MONA_SIGNATURE'] ?? '';
if (abs(time() - (int) $ts) > 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 and compare amountapp.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
});What the agent reads
/llms.txt
Index of every page with a one-line summary, following llmstxt.org. Load it first to know where everything is.
/en/docs/*.md
Every documentation page has a raw markdown twin: append .md to the URL. No HTML parsing, no navigation noise.
/openapi.json
The API v1 spec with security schemes, so a client can be generated instead of hand-written.
What an agent can do without a human
- Sign up (
POST /api/v1/client/register-client) and log in immediately; no approval queue. - Generate an API key and register webhook configurations through the API.
- Fire a simulated payload (
POST /api/v1/client-webhooks/test) to verify the endpoint and signature code. - Read delivery logs and statistics to debug.
The one step that needs a person: linking the ACB account, because ACB sends two OTPs to the phone number registered with the bank. Full guide: For AI agents (docs) · Quick start.