Files
android/docs/thijooree/18-paymv-qr-format.md
T

12 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"). Open-amount QRs: absent (Thijooree BML) or "***" (BML's and Fahipay's own QRs)
58 Country code "MV"
59 Merchant / recipient name Max 25 characters, uppercase in every real QR seen
60 Merchant city / store code LD + 4 digits (e.g. LD0442, LD0745). Seen in BML POS QRs and in BML's and Fahipay's own receive QRs; meaning of the digits unknown
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 Bank-specific — see Reference (tag 62→05). Also printed vertically on the QR card
08 Payment purpose Free-form text entered by the payee. BML's app defaults it to Quickpay Transfer, Fahipay to PAYMENT

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"
02 Unknown "0005" — only seen in Fahipay's own QR with an amount; not generated by Thijooree

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>                          ← "%.2f". Open amount: omit (BML) / "***" (Fahipay)
58 02 MV
59 <len> <NAME UP TO 25 CHARS>             ← Uppercased
60 06 LD<4 random digits>                  ← Fahipay only
62 <len>
  05 <len> <reference>                     ← Bank-specific, see below
  08 <len> <purpose text>                  ← Omit if blank (BML) / "PAYMENT" (Fahipay)
80 <len>
  00 15 mv.favara.mpqr
  01 <len> <yyyy-MM-dd'T'HH:mm:ss.00000>  ← Timestamp
6304<CRC>

For Fahipay, tag 01 is 12 (dynamic) when an amount is set.


Reference (tag 62→05)

The reference is also the vertical text printed beside the QR on both banks' cards, so it is calculated once and used for both (PayMvQrFragment.generateQr()).

BML — base-32 account number + amount

reference = base32(accountNumber) + amountAsTyped
  • base32 is BML's AccountNumbertoBase32: treat the account number as an integer and convert it to base 32 with the alphabet 0123456789ABCDEFGHIJKLMNOPQRSTUV, most significant digit first
  • amountAsTyped is the amount field with commas removed and no forced decimals (100 stays 100, 100.5 stays 100.5). Empty for open-amount QRs
  • Capped at 25 characters. A non-numeric account number falls back to 9 random characters
Account Amount Reference / vertical text
7730000188362 — 70V3UKKUA
7730000188362 100 70V3UKKUA100

Confirmed by decoding a QR from BML's app: 62→05 = 70V3UKKUA, the same as the vertical text on its card.

Fahipay — P + 9 random characters

Fahipay's server issues references like P135KOKXJY and P2KVTPYL4E: P followed by 9 uppercase alphanumerics. The vertical text shows the reference only — the amount is not appended (confirmed on a QR carrying amount 55). Thijooree generates "P" + 9 random chars.

Others

9 random uppercase alphanumeric characters.


Real Receive QRs (Reference Samples)

Decoded from QR images generated by the official apps (CRC verified).

BML app (open amount):

00020101021126920014mv.favara.mpqr0108MALBMVMV0208MALBMVMV031377300001883620511+96091980261004IPAY6006LD04425204000053034625403***5802MV5915SHIHAM A.RAHMAN6234050970V3UKKUA0817Quickpay Transfer80470014mv.favara.mpqr01252026-09-26T02:29:53.000006304F8E6

Note that BML's app places tag 60 inside tag 26 here (after 10 IPAY, as 6006LD0442).

Fahipay app, open amount:

00020101021126810014mv.favara.mpqr0108FAHIMVMV0208FAHIMVMV03125008500611080511+96098074051004IPAY5204000053034625403***5802MV5912MOHAMED RAIF6006LD074562250510P135KOKXJY0807PAYMENT80470014mv.favara.mpqr01252026-09-26T03:44:36.0000063042707

Fahipay app, amount 55:

00020101021226810014mv.favara.mpqr0108FAHIMVMV0208FAHIMVMV03125003600510030511+96091980261004IPAY5204000053034625402555802MV5919SHIHAM ABDUL RAHMAN6006LD097062250510P2KVTPYL4E0807PAYMENT80550014mv.favara.mpqr01252026-09-26T03:22:08.000000204000563045304

Fahipay (Work in Progress)

⚠️ Fahipay QRs generated by Thijooree do not work yet. The Fahipay app rejects them as "Invalid QR". BML QRs from Thijooree scan fine in the BML app.

Fahipay's app doesn't build its QR locally: it fetches it from GET api/app/qr/?lang=…&type=p2p&amount=…, and the server returns the finished card image and payload. Things tried so far, with the Fahipay app still reporting invalid:

Change Status
Mobile 26→05 normalised from Fahipay's stored 960XXXXXXX to +960XXXXXXX Done (was a real bug)
Name 59 uppercased Done
54 = *** for open amount, 01 = 12 with an amount Done
60 = LD + 4 random digits Done
62→08 defaults to PAYMENT Done
62→05 shaped P + 9 chars Done
80→02 = 0005 (amount QRs only) Not done

The CRC is correct (verified against all samples). With no amount, Thijooree's payload now has the same fields in the same order as Fahipay's own. The leading theory is that Fahipay's scanner looks up the P… reference on Fahipay's server, which issued it. If so, no locally generated QR can pass, and the fix would be to fetch the payload from api/app/qr/ and render the card around it.


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 BML QR for account 7730000188362, holder "AHMED ALI", open amount, purpose "Rent":

000201010211268...520400005303462
5802MV5909AHMED ALI6221050970V3UKKUA0804Rent
80...63044A2B

(values abbreviated for clarity — actual tags are concatenated with no whitespace)


 


← README     Next → Account Parser Architecture