7.8 KiB
QR Payment
BML supports QR-based payments via the PayMV network. There are two QR types — static merchant QRs (no preset amount) and gateway QRs (amount preset by merchant). Both are paid via the same 3-step TOTP-authenticated flow.
QR Code Types
| Type code | Name | Amount |
|---|---|---|
QRS |
Static QR | 0.00 — user enters amount |
QRR |
Gateway / dynamic QR | Preset by merchant |
QR Code Formats
BML QR codes appear in two formats.
1. Plain URL QR
https://pay.bml.com.mv/app/<base64-encoded-url>
The entire URL is base64-encoded and passed directly to the payrequest lookup API.
2. Combined EMV-style QR
Used in Fahipay/PayMV combo QRs that embed multiple payment networks. The BML gateway URL is embedded as a TLV value at a fixed path.
TLV path: root tag 35 → sub-tag 20 → sub-sub-tag 01
The value at tag 01 is the full https://pay.bml.com.mv/app/... URL.
3. POS QR (mv.com.bml.qtr)
BML POS terminals emit an EMVCo-style dynamic QR with no tag 26 and supplementary domain
mv.com.bml.qtr. The same TLV path (35 → 20 → 01) holds a bare reference such as
02:215c7b9f15ce4ed28e15697ea976db99 instead of a URL. That bare reference — not the whole payload,
which is rejected with code 112 — is what gets Base64-encoded into the payrequest lookup below.
The reference does not resolve as a pay.bml.com.mv/app/ short code in a browser; only the API
understands it. See
PayMV QR Format → BML POS QR.
PayMV QR Format (TLV)
PayMV QRs (static, PayMV-native) use a decimal TLV encoding (not BER-TLV):
<2-digit decimal tag><2-digit decimal length><value>...
Root-level tags (key fields for scanning)
| Tag | Field |
|---|---|
26 |
Merchant account information (container) |
54 |
Transaction amount |
59 |
Merchant / recipient name |
62 |
Additional data (container) |
Sub-tags
| Parent | Tag | Field |
|---|---|---|
26 |
03 |
Account number |
62 |
08 |
Payment purpose / reference |
For the full PayMV QR format spec including generation (receive-payment QRs), acquirer BIC mapping, CRC algorithm, and all tags — see PayMV QR Format.
Step 1 — Resolve QR to Merchant Details
Endpoint
GET https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/payrequest/{base64Url}
{base64Url} is the lookup key, base64-encoded with standard encoding — the full QR URL
(e.g. https://pay.bml.com.mv/app/...), or for POS QRs the bare 35 → 20 → 01 reference.
BML's own app omits the = padding; the padded form resolves as well, so the client matches the app
rather than relying on either being required.
Headers
| Header | Value |
|---|---|
accept |
application/json — required in practice, see below |
Authorization |
Bearer <access_token> |
User-Agent |
bml-mobile-banking/348 ({manufacturer}; Android {version}; {model}) |
x-app-version |
2.1.44.348 |
accept: application/jsonis not optional. Handled errors (codes 103, 112, …) come back as JSON regardless, but when the route throws, the server renders the Internet Banking HTML login page — under HTTP 200 — instead of a JSON error body. A client without the header then sees a "successful" HTML response it cannot parse. All BML API requests set it inbmlApiRequest().
curl --request GET \
--url 'https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/payrequest/<base64Url>' \
--header 'accept: application/json' \
--header 'Authorization: Bearer <access_token>' \
--header 'User-Agent: bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})' \
--header 'x-app-version: 2.1.44.348'
Response
{
"success": true,
"payload": {
"trxn_hash": "<base64Url>",
"narrative1": "Merchant Name",
"narrative2": "Address Line 1",
"narrative3": "Address Line 2",
"amount": "1.03",
"currency": "MVR"
}
}
Response Fields
| Field | Description |
|---|---|
trxn_hash |
The base64 URL — used as requestId in payment steps |
narrative1 |
Merchant name |
narrative2 |
Merchant address line 1 |
narrative3 |
Merchant address line 2 |
amount |
Payment amount ("0.00" for static QRS) |
currency |
Currency code (typically "MVR") |
Failure Responses
success: false comes back with a code and a user-facing message; the client shows BML's own
wording and keeps the user on the Transfer screen.
| Code | Message |
|---|---|
103 |
The payment request has expired |
112 |
Unsupported payment link |
Step 2 — Pay (3-Step TOTP Flow)
All three steps POST to the same endpoint:
POST https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/pay
Headers
| Header | Value |
|---|---|
Authorization |
Bearer <access_token> |
User-Agent |
bml-mobile-banking/348 ({manufacturer}; Android {version}; {model}) |
x-app-version |
2.1.44.348 |
Content-Type |
application/json |
Accept |
application/json |
Step 2a — Initiate (no channel)
{
"action": "approve",
"debitAccount": "<internalAccountId>",
"requestId": "<trxn_hash>",
"amount": 1.03,
"currency": "MVR"
}
Expected response: { "success": true, "code": 99 } (OTP required)
When this step is used: the client only calls
preInitiatePaymentfor gateway QRs — QR URLs that begin withhttps://pay.bml.com.mv/app/(TransferFragment.kt:349, 1419-1423). For PayMV-native static QRs (QRS), Step 2a is skipped and the flow starts at Step 2b.
Step 2b — Request OTP Channel
{
"action": "approve",
"debitAccount": "<internalAccountId>",
"requestId": "<trxn_hash>",
"amount": 1.03,
"currency": "MVR",
"channel": "token"
}
Expected response: { "success": true, "code": 22 } (OTP generated)
Step 2c — Confirm with TOTP
{
"action": "approve",
"debitAccount": "<internalAccountId>",
"requestId": "<trxn_hash>",
"amount": 1.03,
"currency": "MVR",
"channel": "token",
"otp": "<TOTP>"
}
Expected response:
{
"success": true,
"code": 0,
"payload": {
"merchant": "Merchant Name",
"amount": "1.03",
"currency": "MVR"
}
}
Currency fallback: if the server's
payload.currencyis blank, the client falls back to thecurrencyvalue sent in the request (BmlQrPayClient.kt:148). The same applies tomerchantandamountat the UI layer.
On failure:
{
"success": false,
"message": "Payment failed"
}
Request Fields
| Field | Type | Description |
|---|---|---|
action |
string |
Always "approve" |
debitAccount |
string |
Internal account UUID (not the display account number) — from dashboard internalId field |
requestId |
string |
The trxn_hash from the payrequest lookup |
amount |
number |
Payment amount as a number (e.g. 1.03) |
currency |
string |
Currency code (e.g. "MVR") |
channel |
string |
"token" — present in steps 2b and 2c only |
otp |
string |
TOTP code — present in step 2c only |
The
debitAccountfield takes the internal UUID from the dashboard response, not the displayed account number. See Dashboard for the account object structure.
OTP
The OTP is a standard TOTP (RFC 6238, SHA-1, 30-second window, 6 digits) derived from the stored BML authenticator seed — the same seed used for login 2FA.
Prerequisites
- Valid
access_tokenfrom OAuth Token Exchange - TOTP seed enrolled via BML app
- Account
internalIdfrom Dashboard
← Tap-to-Pay Next → Notifications