Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
57bc488b98
|
|||
|
7f87c9e13f
|
|||
|
cc15ab1c6c
|
@@ -28,7 +28,7 @@ class BmlHostCardEmulatorService : HostApduService() {
|
||||
INS_SELECT -> handleSelect(apdu)
|
||||
INS_GPO -> handleGpo()
|
||||
INS_READ -> handleReadRecord()
|
||||
else -> SW_UNKNOWN_ERROR
|
||||
else -> SW_INS_NOT_SUPPORTED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class BmlHostCardEmulatorService : HostApduService() {
|
||||
val data: ByteArray?
|
||||
|
||||
init {
|
||||
if (raw.size < 4) {
|
||||
if (raw.size < 5) {
|
||||
isError = true; ins = -1; data = null
|
||||
} else {
|
||||
isError = false
|
||||
@@ -166,8 +166,9 @@ class BmlHostCardEmulatorService : HostApduService() {
|
||||
0x32,0x50,0x41,0x59,0x2E,0x53,0x59,0x53,0x2E,0x44,0x44,0x46,0x30,0x31
|
||||
)
|
||||
|
||||
private const val SW_OK_HEX = "9000"
|
||||
private val SW_UNKNOWN_ERROR = byteArrayOf(0x6F.toByte(), 0x00.toByte())
|
||||
private const val SW_OK_HEX = "9000"
|
||||
private val SW_UNKNOWN_ERROR = byteArrayOf(0x6F.toByte(), 0x00.toByte())
|
||||
private val SW_INS_NOT_SUPPORTED = byteArrayOf(0x6D.toByte(), 0x00.toByte())
|
||||
|
||||
@Volatile var activeToken: BmlWalletToken? = null
|
||||
@Volatile var onTransactionComplete: ((success: Boolean) -> Unit)? = null
|
||||
|
||||
@@ -875,7 +875,7 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
|
||||
val dp = resources.displayMetrics.density
|
||||
val progress = animator.animatedFraction
|
||||
val cx = w / 2f; val cy = h / 2f - 20 * dp
|
||||
val cx = w / 2f; val cy = h / 2f + 24 * dp
|
||||
|
||||
val colorOnSurface = MaterialColors.getColor(this,
|
||||
com.google.android.material.R.attr.colorOnSurface, android.graphics.Color.BLACK)
|
||||
@@ -884,70 +884,59 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
val colorSurfaceVariant = MaterialColors.getColor(this,
|
||||
com.google.android.material.R.attr.colorSurfaceVariant, android.graphics.Color.LTGRAY)
|
||||
|
||||
// Phone (left of center)
|
||||
val phoneW = 36 * dp; val phoneH = 62 * dp
|
||||
val phoneX = cx - 72 * dp - phoneW; val phoneY = cy - phoneH / 2f
|
||||
// POS terminal (top center)
|
||||
val posW = 44 * dp; val posH = 72 * dp
|
||||
val posX = cx - posW / 2f; val posY = cy - 170 * dp
|
||||
|
||||
// POS terminal (right of center)
|
||||
val posW = 30 * dp; val posH = 50 * dp
|
||||
val posX = cx + 72 * dp; val posY = cy - posH / 2f
|
||||
|
||||
// Phone body
|
||||
paint.style = Paint.Style.FILL; paint.color = colorSurfaceVariant
|
||||
canvas.drawRoundRect(phoneX, phoneY, phoneX + phoneW, phoneY + phoneH, 6 * dp, 6 * dp, paint)
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2 * dp; paint.color = colorOnSurface
|
||||
canvas.drawRoundRect(phoneX, phoneY, phoneX + phoneW, phoneY + phoneH, 6 * dp, 6 * dp, paint)
|
||||
|
||||
// Phone screen
|
||||
paint.style = Paint.Style.FILL; paint.color = colorPrimary; paint.alpha = 70
|
||||
canvas.drawRoundRect(phoneX + 3 * dp, phoneY + 8 * dp,
|
||||
phoneX + phoneW - 3 * dp, phoneY + phoneH - 12 * dp, 3 * dp, 3 * dp, paint)
|
||||
paint.alpha = 255
|
||||
|
||||
// Static NFC arcs on the right side of phone
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2 * dp; paint.color = colorPrimary
|
||||
val arcOriginX = phoneX + phoneW
|
||||
for (i in 1..3) {
|
||||
val r = i * 10 * dp
|
||||
paint.alpha = 220 - i * 50
|
||||
canvas.drawArc(RectF(arcOriginX - r, cy - r, arcOriginX + r, cy + r),
|
||||
-70f, 140f, false, paint)
|
||||
}
|
||||
paint.alpha = 255
|
||||
// Phone (bottom center)
|
||||
val phoneW = 52 * dp; val phoneH = 90 * dp
|
||||
val phoneX = cx - phoneW / 2f; val phoneY = cy + 30 * dp
|
||||
|
||||
// POS terminal body
|
||||
paint.style = Paint.Style.FILL; paint.color = colorSurfaceVariant
|
||||
canvas.drawRoundRect(posX, posY, posX + posW, posY + posH, 5 * dp, 5 * dp, paint)
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2 * dp; paint.color = colorOnSurface
|
||||
canvas.drawRoundRect(posX, posY, posX + posW, posY + posH, 5 * dp, 5 * dp, paint)
|
||||
canvas.drawRoundRect(posX, posY, posX + posW, posY + posH, 7 * dp, 7 * dp, paint)
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2.5f * dp; paint.color = colorOnSurface
|
||||
canvas.drawRoundRect(posX, posY, posX + posW, posY + posH, 7 * dp, 7 * dp, paint)
|
||||
|
||||
// POS screen
|
||||
paint.style = Paint.Style.FILL; paint.color = colorPrimary; paint.alpha = 70
|
||||
canvas.drawRoundRect(posX + 3 * dp, posY + 4 * dp,
|
||||
posX + posW - 3 * dp, posY + posH * 0.45f, 3 * dp, 3 * dp, paint)
|
||||
canvas.drawRoundRect(posX + 4 * dp, posY + 6 * dp,
|
||||
posX + posW - 4 * dp, posY + posH * 0.45f, 4 * dp, 4 * dp, paint)
|
||||
paint.alpha = 255
|
||||
|
||||
// POS card slot
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 1.5f * dp; paint.color = colorOnSurface
|
||||
canvas.drawLine(posX + 4 * dp, posY + posH * 0.72f, posX + posW - 4 * dp, posY + posH * 0.72f, paint)
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2 * dp; paint.color = colorOnSurface
|
||||
canvas.drawLine(posX + 6 * dp, posY + posH * 0.72f, posX + posW - 6 * dp, posY + posH * 0.72f, paint)
|
||||
|
||||
// Animated NFC rings travelling from phone toward POS
|
||||
val gapStart = arcOriginX + 28 * dp
|
||||
val gapEnd = posX - 4 * dp
|
||||
val midX = (gapStart + gapEnd) / 2f
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2.5f * dp
|
||||
// Phone body
|
||||
paint.style = Paint.Style.FILL; paint.color = colorSurfaceVariant
|
||||
canvas.drawRoundRect(phoneX, phoneY, phoneX + phoneW, phoneY + phoneH, 8 * dp, 8 * dp, paint)
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2.5f * dp; paint.color = colorOnSurface
|
||||
canvas.drawRoundRect(phoneX, phoneY, phoneX + phoneW, phoneY + phoneH, 8 * dp, 8 * dp, paint)
|
||||
|
||||
// Phone screen
|
||||
paint.style = Paint.Style.FILL; paint.color = colorPrimary; paint.alpha = 70
|
||||
canvas.drawRoundRect(phoneX + 4 * dp, phoneY + 10 * dp,
|
||||
phoneX + phoneW - 4 * dp, phoneY + phoneH - 15 * dp, 4 * dp, 4 * dp, paint)
|
||||
paint.alpha = 255
|
||||
|
||||
// Animated NFC rings originating from phone top, travelling upward toward POS
|
||||
val gapTop = posY + posH + 4 * dp
|
||||
val originY = phoneY
|
||||
val maxR = (originY - gapTop) - 4 * dp
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 3 * dp
|
||||
for (i in 0..2) {
|
||||
val p = ((progress + i / 3f) % 1f)
|
||||
val r = p * (gapEnd - gapStart) / 2f + 6 * dp
|
||||
val r = (p * maxR + 6 * dp).coerceAtMost(maxR)
|
||||
paint.color = colorPrimary; paint.alpha = ((1f - p) * 200).toInt().coerceIn(0, 255)
|
||||
canvas.drawArc(RectF(midX - r, cy - r, midX + r, cy + r), -80f, 160f, false, paint)
|
||||
canvas.drawArc(RectF(cx - r, originY - r, cx + r, originY + r), -160f, 140f, false, paint)
|
||||
}
|
||||
paint.alpha = 255
|
||||
|
||||
// Label
|
||||
paint.style = Paint.Style.FILL; paint.color = colorOnSurface; paint.alpha = 160
|
||||
paint.textSize = 14 * dp; paint.textAlign = Paint.Align.CENTER
|
||||
canvas.drawText(context.getString(R.string.card_pay_nfc), cx, cy + 60 * dp, paint)
|
||||
paint.textSize = 15 * dp; paint.textAlign = Paint.Align.CENTER
|
||||
canvas.drawText(context.getString(R.string.card_pay_nfc), cx, phoneY + phoneH + 28 * dp, paint)
|
||||
paint.alpha = 255; paint.textAlign = Paint.Align.LEFT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,4 +144,4 @@ Each channel object:
|
||||
|
||||
---
|
||||
|
||||
[← Account Validation](10-validate.md)
|
||||
[← Account Validation](10-validate.md) · [Next → Tap-to-Pay](12-tap-to-pay.md)
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
# Tap-to-Pay (NFC / HCE)
|
||||
|
||||
BML supports contactless NFC payments via Host Card Emulation (HCE). The app fetches single-use payment tokens from the server, then emulates an EMV mag-stripe contactless card using Android's `HostApduService`.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
1. Fetch tokens → POST /api/mobile/walletpayments/gettoken (TOTP-authenticated)
|
||||
2. HCE exchange → Android NFC subsystem drives the APDU exchange with the POS terminal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Fetch Payment Tokens
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
POST https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/gettoken
|
||||
```
|
||||
|
||||
### 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` |
|
||||
|
||||
### Three-Step OTP Flow
|
||||
|
||||
Token retrieval requires TOTP verification and completes in three POSTs to the same endpoint.
|
||||
|
||||
#### Step 1a — Initiate
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "track2",
|
||||
"cardid": "<cardId>",
|
||||
"quantity": 3
|
||||
}
|
||||
```
|
||||
|
||||
Expected response: `{ "code": 99 }` (OTP required)
|
||||
|
||||
If `"code": 0` is returned directly the payload contains tokens immediately (skip to parsing).
|
||||
|
||||
#### Step 1b — Request OTP Channel
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "track2",
|
||||
"cardid": "<cardId>",
|
||||
"quantity": 3,
|
||||
"channel": "token"
|
||||
}
|
||||
```
|
||||
|
||||
Expected response: `{ "code": 22 }` (OTP generated on BML side; TOTP is used locally)
|
||||
|
||||
#### Step 1c — Submit TOTP
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "track2",
|
||||
"cardid": "<cardId>",
|
||||
"quantity": 3,
|
||||
"channel": "token",
|
||||
"otp": "<TOTP>"
|
||||
}
|
||||
```
|
||||
|
||||
Expected response: `{ "code": 0, "payload": [...] }`
|
||||
|
||||
The OTP is a standard TOTP (RFC 6238, SHA-1, 30-second window, 6 digits) derived from the stored BML authenticator seed.
|
||||
|
||||
### Token Response
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"payload": [
|
||||
{
|
||||
"token": "4761360000000000",
|
||||
"expiry": "2512",
|
||||
"app_code": "A0000000031010",
|
||||
"service_code": "000",
|
||||
"data": "0960919802623742",
|
||||
"valid_until": "2025-12-01 12:00:00.000"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Token Fields
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `token` | PAN-equivalent single-use token (used as Track 2 primary account number) |
|
||||
| `expiry` | Expiry in `YYMM` format (e.g. `"2512"` = December 2025) |
|
||||
| `app_code` | AID (Application Identifier) hex string — identifies the card network |
|
||||
| `service_code` | 3-digit service code for Track 2 |
|
||||
| `data` | Discretionary data appended to Track 2 |
|
||||
| `valid_until` | Server-side expiry timestamp for the token |
|
||||
|
||||
### AID to Card Network Mapping
|
||||
|
||||
| AID prefix | Network |
|
||||
|---|---|
|
||||
| `A0000000031010` | Visa |
|
||||
| `A0000000041010` | Mastercard |
|
||||
| `A000000025...` | Amex |
|
||||
| (other) | BML |
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — HCE APDU Exchange
|
||||
|
||||
Once a token is set, Android's NFC subsystem routes contactless commands to the app's `HostApduService`. The flow follows the EMV mag-stripe contactless profile.
|
||||
|
||||
### APDU Exchange Flow
|
||||
|
||||
```
|
||||
POS Terminal Android HCE
|
||||
| |
|
||||
| SELECT PPSE (INS=A4) |
|
||||
|--------------------------------------->|
|
||||
| FCI Template (6F) + 9000 |
|
||||
|<---------------------------------------|
|
||||
| |
|
||||
| SELECT AID (INS=A4) |
|
||||
|--------------------------------------->|
|
||||
| FCI Template (6F) + 9000 |
|
||||
|<---------------------------------------|
|
||||
| |
|
||||
| GET PROCESSING OPTIONS (INS=A8) |
|
||||
|--------------------------------------->|
|
||||
| Response Message Template (80) + 9000 |
|
||||
|<---------------------------------------|
|
||||
| |
|
||||
| READ RECORD (INS=B2) |
|
||||
|--------------------------------------->|
|
||||
| Record Template (70) + 9000 |
|
||||
|<---------------------------------------|
|
||||
```
|
||||
|
||||
### APDU Command Bytes
|
||||
|
||||
| INS | Hex | Command |
|
||||
|---|---|---|
|
||||
| `SELECT` | `0xA4` | Select PPSE or AID |
|
||||
| `GET PROCESSING OPTIONS` | `0xA8` | Request AIP + AFL |
|
||||
| `READ RECORD` | `0xB2` | Read Track 2 data |
|
||||
|
||||
### SELECT PPSE Response
|
||||
|
||||
PPSE AID: `2PAY.SYS.DDF01` = `325041592E5359532E4444463031`
|
||||
|
||||
```
|
||||
6F <len>
|
||||
84 <len> 325041592E5359532E4444463031 ← DF Name (PPSE)
|
||||
A5 <len>
|
||||
BF0C <len>
|
||||
61 <len>
|
||||
4F <len> <AID> ← ADF Name
|
||||
87 01 01 ← Application Priority Indicator
|
||||
9000
|
||||
```
|
||||
|
||||
### SELECT AID Response
|
||||
|
||||
```
|
||||
6F <len>
|
||||
84 <len> <AID> ← Dedicated File Name
|
||||
A5 <len>
|
||||
50 <len> <label-ascii-as-hex> ← Application Label (e.g. "VISA")
|
||||
9F38 02 9F6602 ← PDOL: TTQ (2 bytes)
|
||||
9000
|
||||
```
|
||||
|
||||
The application label is derived from the AID prefix (see mapping table above).
|
||||
|
||||
### GET PROCESSING OPTIONS Response
|
||||
|
||||
```
|
||||
80 06 0080 08010100
|
||||
9000
|
||||
```
|
||||
|
||||
| Field | Value | Meaning |
|
||||
|---|---|---|
|
||||
| Tag `80` | — | Response Message Template 1 |
|
||||
| AIP | `0080` | Mag-stripe mode |
|
||||
| AFL | `08010100` | SFI=1, records 1–1, 0 offline auth records |
|
||||
|
||||
### READ RECORD Response
|
||||
|
||||
```
|
||||
70 <len>
|
||||
57 <len> <track2-data> ← Track 2 Equivalent Data
|
||||
9000
|
||||
```
|
||||
|
||||
Track 2 format:
|
||||
```
|
||||
{token} D {expiry} {serviceCode} {data} [F]
|
||||
```
|
||||
|
||||
The trailing `F` nibble is appended when the total length is odd (standard Track 2 padding).
|
||||
|
||||
Example from a real token:
|
||||
```
|
||||
4761360000000000 D 2512 000 0960919802623742
|
||||
→ 4761360000000000D2512000096091980262374 2F (padded)
|
||||
```
|
||||
|
||||
### Status Words
|
||||
|
||||
| SW | Meaning |
|
||||
|---|---|
|
||||
| `9000` | Success |
|
||||
| `6F00` | Generic / unknown error |
|
||||
| `6D00` | Instruction not supported |
|
||||
|
||||
---
|
||||
|
||||
## TLV Encoding
|
||||
|
||||
All APDU responses use BER-TLV encoding. Tags are 1 or 2 bytes (hex string). Length follows DER short/long form:
|
||||
|
||||
| Length range | Encoding |
|
||||
|---|---|
|
||||
| 0–127 bytes | `LL` (1 byte) |
|
||||
| 128–255 bytes | `81 LL` (2 bytes) |
|
||||
| 256–65535 bytes | `82 HH LL` (3 bytes) |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Valid `access_token` from [OAuth Token Exchange](03-oauth-token.md)
|
||||
- TOTP seed enrolled via BML app (same seed used for login 2FA)
|
||||
- `cardId` from the dashboard — see [Dashboard](04-dashboard.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
[← Foreign Limits](11-foreign-limits.md) · [Next → QR Payment](13-qr-payment.md)
|
||||
@@ -0,0 +1,241 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Resolve QR to Merchant Details
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
GET https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/payrequest/{base64Url}
|
||||
```
|
||||
|
||||
`{base64Url}` is the full QR URL (e.g. `https://pay.bml.com.mv/app/...`) base64-encoded with standard encoding (with padding).
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | Value |
|
||||
|---|---|
|
||||
| `Authorization` | `Bearer <access_token>` |
|
||||
| `User-Agent` | `bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})` |
|
||||
| `x-app-version` | `2.1.44.348` |
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url 'https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/payrequest/<base64Url>' \
|
||||
--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
|
||||
|
||||
```json
|
||||
{
|
||||
"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"`) |
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "approve",
|
||||
"debitAccount": "<internalAccountId>",
|
||||
"requestId": "<trxn_hash>",
|
||||
"amount": 1.03,
|
||||
"currency": "MVR"
|
||||
}
|
||||
```
|
||||
|
||||
Expected response: `{ "success": true, "code": 99 }` (OTP required)
|
||||
|
||||
> **Note:** This step may be skipped. The app proceeds directly to Step 2b if the gateway already indicates OTP is required.
|
||||
|
||||
### Step 2b — Request OTP Channel
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "approve",
|
||||
"debitAccount": "<internalAccountId>",
|
||||
"requestId": "<trxn_hash>",
|
||||
"amount": 1.03,
|
||||
"currency": "MVR",
|
||||
"channel": "token",
|
||||
"otp": "<TOTP>"
|
||||
}
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 0,
|
||||
"payload": {
|
||||
"merchant": "Merchant Name",
|
||||
"amount": "1.03",
|
||||
"currency": "MVR"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On failure:
|
||||
|
||||
```json
|
||||
{
|
||||
"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 `debitAccount` field takes the internal UUID from the dashboard response, **not** the displayed account number. See [Dashboard](04-dashboard.md) 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_token` from [OAuth Token Exchange](03-oauth-token.md)
|
||||
- TOTP seed enrolled via BML app
|
||||
- Account `internalId` from [Dashboard](04-dashboard.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
[← Tap-to-Pay](12-tap-to-pay.md)
|
||||
@@ -188,6 +188,8 @@ The access token expires after `expires_in` seconds (typically 3600). On a `401`
|
||||
| 9 | [Contacts](09-contacts.md) | Saved beneficiaries — list, save, delete |
|
||||
| 10 | [Account Validation](10-validate.md) | Validate BML accounts, aliases, and MIB accounts |
|
||||
| 11 | [Foreign Limits](11-foreign-limits.md) | USD foreign transaction limits by card and channel |
|
||||
| 12 | [Tap-to-Pay](12-tap-to-pay.md) | NFC HCE contactless payment — token fetch and EMV APDU exchange |
|
||||
| 13 | [QR Payment](13-qr-payment.md) | PayMV QR payment — QR formats, payrequest lookup, 3-step pay flow |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user