9.0 KiB
QR Merchant Payment ("Smart Pay")
Pay an Ooredoo M-Faisa merchant by scanning their QR. The QR encodes only a numeric qrCodeId (e.g. 1594103440350) — no URL, no EMV TLV envelope. The flow is three calls and does not require OTP (2FARequired=NONE).
Currency / pocket constraints: captures cover MVR→MVR purchases from the user's EMONEY pocket only. The
transactionCurrencyfield is taken from the QR lookup; we have not seen a non-MVR variant.
Flow
Client Server
| |
| POST /QRCodeUtility/fetchQRCodeById | ← user scanned a QR
| formData = { qrCodeId, tenantCode } |
|------------------------------------------------>|
| [{ success, response:[{ commercialName, |
| customerId, mobileNumber, currencyCode, |
| txnAmount, status, ... }] }] |
|<------------------------------------------------|
| |
| (show merchant, accept amount + remarks)
| |
| POST /initiateNewBuy |
| formData = { merchantId, mobileNumber, |
| sourceDetails, transactionAmount, |
| transactionType:"PURCHASE", … } |
|------------------------------------------------>|
| [{ 2FARequired:"NONE", |
| authenticationType:"NONE", |
| success:true, |
| response:[{ responseObject:{ referenceId, |
| chargeDetails, … } }] }] |
|<------------------------------------------------|
| |
| (no OTP — go straight to confirm)
| |
| POST /confirmNewBuy |
| formData = { referenceId } |
| transactionAuthDetails = "null" ← literal |
|------------------------------------------------>|
| [{ success:true, |
| message:"Payment Completed Successfully", |
| response:[{ responseObject:{ isCompleted, |
| balanceInquiryDTO, ... } }] }] |
|<------------------------------------------------|
All three endpoints carry the standard anti-replay pair (rndValue + csValue) derived from each request's formData JSON — see 01-encryption.md → rndValue / csValue.
Unlike the transfer flow, every endpoint here returns its envelope as a JSON array [{...}] for both success and error.
Step 1: QRCodeUtility/fetchQRCodeById — resolve merchant
Endpoint
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/QRCodeUtility/fetchQRCodeById
Request
Content-Type: application/x-www-form-urlencoded
| Field | Value |
|---|---|
role |
R01 (the other two endpoints use RETAIL_SUBSCRIBER — this one does not) |
channel |
C03 |
loginExchangeKey |
From login |
rndValue / csValue |
Standard anti-replay |
formData |
JSON below (html-safe = escaping) |
{
"qrCodeId": "<numeric id from QR>",
"tenantCode": "ooredoo"
}
Response — happy path
[
{
"success": true,
"message": "QRCode fetched Successfully.",
"response": [
{
"mobileNumber": "9609569506", /* merchant's '960' + msisdn */
"customerId": "72518", /* used as merchantId in step 2 */
"commercialName": "Family Room", /* merchant display name */
"qrCodeId": "1594103440350",
"qrImageString": "<base64 PNG>", /* unused */
"accountNumber": null,
"txnAmount": null, /* static QR; dynamic QRs put a number here */
"currencyCode": "MVR",
"status": "Active",
"role": "AGENT",
"tenantCode": "ooredoo",
"...": "..."
}
]
}
]
accountNumber / txnAmount are JSON null for static QRs (the user chooses the amount). For dynamic QRs the server returns the fixed amount in txnAmount and the client should lock the amount field.
The mobileNumber field already includes the 960 country prefix.
Response — QR not found / inactive
[{ "success": false, "message": "QRCode not found." }]
The client also rejects entries with status != "Active".
Step 2: initiateNewBuy — initiate purchase (no OTP triggered)
Endpoint
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/initiateNewBuy
Request
Content-Type: application/x-www-form-urlencoded
| Field | Value |
|---|---|
role |
RETAIL_SUBSCRIBER |
channel |
C03 (top-level differs from inner formData.channel, which is SubscriberApp) |
loginExchangeKey |
From login |
rndValue / csValue |
Standard anti-replay (derived from the formData below) |
formData |
JSON below |
{
"channel": "SubscriberApp",
"commodityType": "WALLET",
"description": "<remarks>", /* free text, may be empty */
"merchantId": "<customerId from step 1>",
"mobileNumber": "<merchant '960' msisdn from step 1>",
"sourceDetails": {
"MDNId": "960<myMsisdn>", /* PLAINTEXT — '960' + my phone */
"actorRoleType": "RETAIL_SUBSCRIBER",
"pocketId": "<my source pocket id>" /* EMONEY pocket from login */
},
"transactionAmount": "<amount>", /* string, e.g. "7.56" */
"transactionCurrency": "MVR",
"transactionType": "PURCHASE"
}
Unlike the transfer flow's initiateFTRequest, this endpoint does not take an identifier header field or tPin.
Response — happy path
[
{
"2FARequired": "NONE", /* ← the key difference */
"authenticationType": "NONE",
"success": true,
"message": "Purchase Initiated Successfully",
"response": [
{
"requestObject": { "...": "..." },
"responseObject": {
"referenceId": "685011023630",
"transactionAmount": { "amount": 7.56, "currencyCode": "MVR" },
"netAmount": { "amount": 7.56, "currencyCode": "MVR" },
"chargeDetailsDTO": { "totalFeesInTenantCurrency": { "amount": 0.0, "...": "..." }, "...": "..." },
"isCompleted": false,
"authenticationType":"NONE",
"...": "..."
}
}
]
}
]
Cache referenceId for step 3. No SMS is sent — proceed straight to confirm.
Defensive check: the Thijooree client throws if it ever sees
2FARequired != "NONE"on this endpoint so a no-op confirm can't silently complete. If Ooredoo ever turns 2FA on for QR pay, you'll see a clear error instead of a partial transaction.
Step 3: confirmNewBuy — settle purchase
Endpoint
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/confirmNewBuy
Request
Content-Type: application/x-www-form-urlencoded
| Field | Value |
|---|---|
role |
RETAIL_SUBSCRIBER |
channel |
C03 |
loginExchangeKey |
From login |
rndValue / csValue |
Anti-replay derived from formData below |
formData |
{"referenceId": "<from step 2>"} |
transactionAuthDetails |
literal string "null" (not a JSON null, not an empty string — the captured request sends the four-character string null) |
Response — happy path
[
{
"success": true,
"message": "Payment Completed Successfully",
"response": [
{
"responseObject": {
"isCompleted": true,
"balanceInquiryDTO": {
"currencyCode": "MVR",
"pocketAmount": 92.85,
"pocketId": "<source pocket id>",
"pocketBalanceMap": { "...": "..." }
},
"status": { "replyCode": 0.0 },
"...": "..."
}
}
]
}
]
Session expiry
Same envelope as elsewhere — attributeValue: "SESSION_EXPIRED" with HTTP 200; the client throws MfaisaSessionExpiredException. See 03-history.md → Session expiry.
Optional: save/smart-pay-recipient — bookkeeping
After a successful confirm the official Ooredoo app saves the merchant to a server-side "recent recipients" list:
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/save/smart-pay-recipient
This is not required for the payment itself — the transfer is final once confirmNewBuy succeeds. Thijooree skips it; the merchant is kept in the local picker RecentsCache under an mfaisaqr:<qrCodeId> synthetic accountNumber (mirroring the BML QR bmlqr: scheme).
← Back to Transfer Money | README