Webhook configuration API
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 and Security; 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.
curl https://api.monapay.vn/api/v1/client-webhooks -H "Authorization: Bearer $MONA_TOKEN"
{
"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
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
$ch = curl_init('https://api.monapay.vn/api/v1/client-webhooks');
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([
'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
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.
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.
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:
{"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.
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) |
curl "https://api.monapay.vn/api/v1/webhook-logs?status=failed&from_date=2026-08-01&limit=50" \
-H "Authorization: Bearer $MONA_TOKEN"
{
"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.
GET /api/v1/webhook-logs/stats
Statistics over the last N days (days parameter, e.g. 7 or 30). Needs Bearer.
curl "https://api.monapay.vn/api/v1/webhook-logs/stats?days=7" -H "Authorization: Bearer $MONA_TOKEN"
{
"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 |