8.2 KiB
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.
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.
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.
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)