Files
android/docs/thijooree/18-paymv-qr-format.md
T
2026-09-21 15:26:18 +05:00

239 lines
8.2 KiB
Markdown

# PayMV QR Format
Documents the decimal TLV QR format used by the PayMV/Favara payment network in the Maldives — both generation (to receive payment) and parsing (to initiate a transfer by scanning).
---
## Encoding
PayMV QRs use a **decimal TLV** encoding — not binary BER-TLV. Every field is represented as ASCII text:
```
<2-digit decimal tag><2-digit decimal length><value>...
```
Example — tag `59`, value `"AHMED ALI"` (9 chars):
```
5909AHMED ALI
```
Tags and lengths are always exactly 2 decimal digits. Fields are concatenated directly with no separator.
---
## Root-Level Tags
| Tag | Field | Notes |
|---|---|---|
| `00` | Format indicator | Always `"01"` |
| `01` | Point-of-initiation method | `"11"` = static QR, `"12"` = dynamic QR |
| `26` | Merchant account information | Container — see sub-tags below |
| `35` | BML/gateway merchant info | Container — present in combined EMV+BML QRs and in BML POS QRs |
| `52` | Merchant category code | `"0000"` (generic) |
| `53` | Transaction currency | `"462"` = MVR (ISO 4217 numeric) |
| `54` | Transaction amount | Decimal string (e.g. `"1.50"`); absent for open-amount QRs |
| `58` | Country code | `"MV"` |
| `59` | Merchant / recipient name | Max 25 characters |
| `60` | Merchant city / store code | BML POS QRs only |
| `62` | Additional data field | Container — see sub-tags below |
| `63` | CRC | `6304` prefix + 4-char hex checksum — always last |
| `80` | Supplementary data | Container — timestamp and domain |
---
## Merchant Account Info — Tag `26` Sub-Tags
| Sub-Tag | Field | Example value |
|---|---|---|
| `00` | Domain | `"mv.favara.mpqr"` |
| `01` | Acquirer BIC | `"MALBMVMV"` (see table below) |
| `02` | Acquirer BIC (repeated) | Same as `01` |
| `03` | Account number | Beneficiary account number |
| `05` | Mobile number | E.164 format (e.g. `"+9607654321"`); optional |
| `10` | Network indicator | `"IPAY"` |
### Acquirer BIC Mapping
| Bank | Acquirer BIC |
|---|---|
| BML | `MALBMVMV` |
| MIB | `MADVMVMV` |
| Fahipay | `FAHIMVMV` |
---
## Additional Data — Tag `62` Sub-Tags
| Sub-Tag | Field | Notes |
|---|---|---|
| `05` | Reference / bill number | 9 random uppercase alphanumeric characters |
| `08` | Payment purpose | Free-form text entered by the payee |
---
## Supplementary Data — Tag `80` Sub-Tags
| Sub-Tag | Field | Notes |
|---|---|---|
| `00` | Domain | `"mv.favara.mpqr"` |
| `01` | Timestamp | ISO 8601 format: `"yyyy-MM-dd'T'HH:mm:ss.00000"` |
---
## CRC-16
The checksum uses **CRC-16/CCITT-FALSE** (polynomial `0x1021`, initial value `0xFFFF`). The CRC is computed over the entire payload string up to and including `"6304"`, then the 4-digit uppercase hex result is appended.
```python
def crc16(data: str) -> str:
crc = 0xFFFF
for c in data:
crc ^= (ord(c) & 0xFF) << 8
for _ in range(8):
if crc & 0x8000:
crc = ((crc << 1) & 0xFFFF) ^ 0x1021
else:
crc = (crc << 1) & 0xFFFF
return format(crc, '04X')
```
---
## Generating a Receive-Payment QR
To create a QR that others can scan to pay you:
```
00 02 01 ← Format indicator
01 02 11 ← Static QR
26 <len> ← Merchant account info
00 15 mv.favara.mpqr
01 08 MALBMVMV ← Acquirer BIC (BML example)
02 08 MALBMVMV ← Repeated
03 <len> <accountNumber>
05 <len> <+960XXXXXXX> ← Optional phone
10 04 IPAY
52 04 0000 ← MCC
53 03 462 ← MVR
54 <len> <amount> ← Omit tag entirely if open-amount
58 02 MV
59 <len> <name up to 25 chars>
62 <len>
05 09 <9 random alphanum chars> ← Reference
08 <len> <purpose text>
80 <len>
00 15 mv.favara.mpqr
01 <len> <yyyy-MM-dd'T'HH:mm:ss.00000> ← Timestamp
6304<CRC>
```
---
## Parsing a PayMV QR (Incoming Scan)
When scanning a QR code, extract the relevant fields:
| Field | TLV path | Used for |
|---|---|---|
| Account number | root→`26`→`03` | Transfer destination |
| Amount | root→`54` | Pre-fill transfer amount (may be absent) |
| Merchant name | root→`59` | Display recipient name |
| Purpose | root→`62`→`08` | Pre-fill transfer remarks |
---
## Extracting a BML Gateway URL from a Combined QR
Combined QRs (e.g. Fahipay card QRs that embed a BML gateway payment URL) encode the BML URL at a fixed TLV path:
```
root tag 35 → sub-tag 20 → sub-sub-tag 01
```
The value at sub-sub-tag `01` is a full `https://pay.bml.com.mv/app/...` URL. Extract it and hand off to the [BML QR Payment flow](../bmlapi/13-qr-payment.md).
Plain BML QR codes (not combined) start with `https://pay.bml.com.mv/app/` directly.
`PaymvQrParser.bmlQrPayTarget()` covers all of these: it returns the URL for plain URL QRs and for
combined QRs, the whole EMV payload for POS QRs (below, recognised by the `mv.com.bml.qtr`
supplementary domain), and null for everything else — PayMV QRs keep falling through to `parse()`.
---
## BML POS QR (`mv.com.bml.qtr`)
BML POS terminals emit a third shape — an EMVCo-style dynamic QR whose supplementary data domain
(tag `80` → `00`) is `mv.com.bml.qtr` and which has **no tag `26`**, so there is no PayMV account
number to transfer to. The same `35` → `20` container is used as in combined QRs, but sub-tag `01`
holds a bare reference instead of a URL.
Example (CRC verified, same CRC-16/CCITT-FALSE as above):
```
00020101021235752071000202013502:215c7b9f15ce4ed28e15697ea976db9902109809724081030874009538520400005303462540436005802MV5911BEST BANANA6006LD044262220510dtyams497d0804POPE80470014mv.com.bml.qtr01252026-09-21T13:33:22.00000630443FA
```
| TLV path | Value | Notes |
|---|---|---|
| `00` | `01` | Format indicator |
| `01` | `12` | Dynamic QR |
| `35`→`20`→`00` | `02` | Version / format of the container (combined QRs use the URL form) |
| `35`→`20`→`01` | `02:215c7b9f15ce4ed28e15697ea976db99` | Payment reference — `<type>:<32 hex>` |
| `35`→`20`→`02` | `9809724081` | Merchant identifier (10 digits) |
| `35`→`20`→`03` | `74009538` | Terminal identifier (8 digits) |
| `52` | `0000` | MCC |
| `53` | `462` | MVR |
| `54` | `3600` | Amount — **no decimal point**, unlike PayMV's `"1.50"` |
| `58` / `59` / `60` | `MV` / `BEST BANANA` / `LD0442` | Country, merchant name, merchant city/store code |
| `62`→`05` | `dtyams497d` | Reference / bill number |
| `62`→`08` | `POPE` | Purpose / terminal label |
| `80`→`00` | `mv.com.bml.qtr` | Domain — identifies the POS format |
| `80`→`01` | `2026-09-21T13:33:22.00000` | Timestamp |
### Pay-Request Lookup Key
The lookup key is the **bare reference**, Base64-encoded without padding:
```
GET .../walletpayments/payrequest/MDI6MjE1YzdiOWYxNWNlNGVkMjhlMTU2OTdlYTk3NmRiOTk
```
BML's own app omits the `=` padding, so `BmlQrPayClient.lookupPayRequest()` encodes with
`NO_WRAP or NO_PADDING` for every QR type; the padded form resolves too. What the request *must*
carry is `accept: application/json` — see
[QR Payment → Step 1 headers](../bmlapi/13-qr-payment.md#headers).
Confirmed against the live API (the example QR had already expired, so BML answered `103` rather
than with merchant details — but `103` means the reference itself resolved):
| Key tried | Response |
|---|---|
| `02:215c7b9f15ce4ed28e15697ea976db99` | `103` — "The payment request has expired" ✅ recognised |
| the whole EMV payload | `112` — "Unsupported payment link" ❌ |
| `https://pay.bml.com.mv/app/02:215c…` | `103` — recognised too; the host itself 400s on that path, so the backend must strip the prefix |
`PaymvQrParser.bmlPayRequestKey()` returns the URL for URL QRs and the reference for POS QRs, so
exactly one request is made either way.
---
## Example Payload
Static QR for account `7700000000123`, holder `"AHMED ALI"`, open amount, purpose `"Rent"`:
```
000201010211268...520400005303462
5802MV5909AHMED ALI6225050912345ABCDEF0804Rent
80...63044A2B
```
(values abbreviated for clarity — actual tags are concatenated with no whitespace)
---
&nbsp;
---
[← README](README.md) &nbsp;&nbsp;&nbsp; **Next →** [Account Parser Architecture](19-parsers.md)