Files
thijooree/docs/mfaisaapi/05-qr-pay.md
T
shihaam 00e6b40ee0
Auto Tag on Version Change / check-version (push) Failing after 14m24s
update docs
2026-06-27 18:35:38 +05:00

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 transactionCurrency field 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