From 7f87c9e13f97739ffb0faf5e82041b5b59238ad7 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Fri, 29 May 2026 18:58:43 +0500 Subject: [PATCH] update docs --- docs/bmlapi/11-foreign-limits.md | 2 +- docs/bmlapi/12-tap-to-pay.md | 254 +++++++++++++++++++++++++++++++ docs/bmlapi/13-qr-payment.md | 241 +++++++++++++++++++++++++++++ docs/bmlapi/README.md | 2 + 4 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 docs/bmlapi/12-tap-to-pay.md create mode 100644 docs/bmlapi/13-qr-payment.md diff --git a/docs/bmlapi/11-foreign-limits.md b/docs/bmlapi/11-foreign-limits.md index d907641..f4a6404 100644 --- a/docs/bmlapi/11-foreign-limits.md +++ b/docs/bmlapi/11-foreign-limits.md @@ -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) diff --git a/docs/bmlapi/12-tap-to-pay.md b/docs/bmlapi/12-tap-to-pay.md new file mode 100644 index 0000000..a31a84a --- /dev/null +++ b/docs/bmlapi/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 ` | +| `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": "", + "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": "", + "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": "", + "quantity": 3, + "channel": "token", + "otp": "" +} +``` + +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 + 84 325041592E5359532E4444463031 ← DF Name (PPSE) + A5 + BF0C + 61 + 4F ← ADF Name + 87 01 01 ← Application Priority Indicator +9000 +``` + +### SELECT AID Response + +``` +6F + 84 ← Dedicated File Name + A5 + 50 ← 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 + 57 ← 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) diff --git a/docs/bmlapi/13-qr-payment.md b/docs/bmlapi/13-qr-payment.md new file mode 100644 index 0000000..6558182 --- /dev/null +++ b/docs/bmlapi/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/ +``` + +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>... +``` + +### 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 ` | +| `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/' \ + --header 'Authorization: Bearer ' \ + --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": "", + "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 ` | +| `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": "", + "requestId": "", + "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": "", + "requestId": "", + "amount": 1.03, + "currency": "MVR", + "channel": "token" +} +``` + +Expected response: `{ "success": true, "code": 22 }` (OTP generated) + +### Step 2c — Confirm with TOTP + +```json +{ + "action": "approve", + "debitAccount": "", + "requestId": "", + "amount": 1.03, + "currency": "MVR", + "channel": "token", + "otp": "" +} +``` + +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) diff --git a/docs/bmlapi/README.md b/docs/bmlapi/README.md index abaf659..7106e9e 100644 --- a/docs/bmlapi/README.md +++ b/docs/bmlapi/README.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 | ---