Files
thijooree/docs/mfaisaapi/04-transfer.md
T
shihaam 43f3cca2aa
Auto Tag on Version Change / check-version (push) Failing after 13m41s
ooredoo mfaisa
2026-06-27 14:25:46 +05:00

9.1 KiB

Transfer Money (Wallet-to-Wallet)

Send MVR from the user's M-Faisa pocket to another M-Faisa subscriber, identified by phone number.

There is no "account number" concept on M-Faisa — recipients are addressed by mobile number, and the server resolves the destination pocket itself. The flow is three calls, ending with an OTP delivered by SMS to the sender.

Currency / pocket constraints: Thijooree only sends from the user's MVR (EMONEY) pocket to the recipient's MVR pocket. PayPal-USD pockets are out of scope (the HAR captures don't cover them and we have no Frida-extracted recipe).


Flow

Client                                         Server
  |                                              |
  |  POST /Pocket/basicBeneDetails               |   ← user typed a phone and tapped 🔍
  |  formData = { beneficaryDetails, initiator }  |
  |--------------------------------------------->|
  |  [{ success, response:[[pocket, pocket,…]]}] |
  |<---------------------------------------------|
  |                                              |
  |   (show recipient name, accept amount + remarks)
  |                                              |
  |  POST /initiateFTRequest                     |
  |  formData = { sourceDetails, recipient,      |
  |               transactionAmount, … }         |
  |--------------------------------------------->|
  |  { 2FARequired:"OTP", response:[{            |
  |       responseObject:{ referenceId,          |
  |       chargeDetails, … } }] }                |
  |<---------------------------------------------|
  |                                              |
  |   (server sends SMS OTP to sender's phone)
  |   (user types OTP)                           |
  |                                              |
  |  POST /confirmFTRequest                      |
  |  formData = { referenceId }                  |
  |  transactionAuthDetails = { OTP encrypted }  |
  |--------------------------------------------->|
  |  { success:true,                             |
  |    message:"Transfer Completed Successfully" }
  |<---------------------------------------------|

All three endpoints carry the same anti-replay pair (rndValue + csValue) derived from the request's formData JSON — see 01-encryption.md → rndValue / csValue.


Step 1: Pocket/basicBeneDetails — recipient lookup

Endpoint

POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/Pocket/basicBeneDetails

Request

Content-Type: application/x-www-form-urlencoded

Field Value
role RETAIL_SUBSCRIBER
channel SubscriberApp
loginExchangeKey From login
rndValue / csValue Standard anti-replay
formData JSON below (html-safe = escaping)
{
  "beneficaryDetails": {
    "MDNId":         "<encryptMobile(recipientMsisdn), base64>",
    "actorRoleType": "RETAIL_SUBSCRIBER"
  },
  "initiatorDetailsDTO": {
    "initiatingMDN":    "<encryptMobile(myMsisdn), base64>",
    "initiatingRoleId": "<my suscriberId>",
    "initiatorRole":    "RETAIL_SUBSCRIBER"
  }
}

suscriberId (note the server's typo) comes from the top level of the doMobileLogin response — see 02-login.md.

Response — happy path

[
  {
    "success": true,
    "message": "Operation Completed Successfully",
    "response": [
      [
        { "pocketId": "<paypal pocket id>", "pocketCurrency": "USD",
          "pocketValueType": "PAYPAL_USD", "name": "<Recipient Name>", "MDNId": "<recipient msisdn>",
          "walletId": "<recipient wallet id>", "actorId": "<recipient actor id>", "...": "..." },
        { "pocketId": "<mvr pocket id>", "pocketCurrency": "MVR",
          "pocketValueType": "EMONEY",  "name": "<Recipient Name>", "MDNId": "<recipient msisdn>",
          "walletId": "<recipient wallet id>", "actorId": "<recipient actor id>", "...": "..." }
      ]
    ]
  }
]

Note the nesting — response is an array (one element only seen in practice) of arrays of pocket objects (one per pocket the recipient owns).

Response — recipient not found

[{ "success": false, "message": "Pocket details not found." }]

Step 2: initiateFTRequest — initiate, server SMSes OTP

Endpoint

POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/initiateFTRequest

Request

Content-Type: application/x-www-form-urlencoded

Field Value
identifier <encryptMobile(recipientMsisdn), base64> — independent encryption from formData.MDNId
role RETAIL_SUBSCRIBER
transferMode MOBILE
channel C03 (the top-level value differs from the inner formData.channel, which is SubscriberApp)
tPin empty string "" (a relic — the OTP step authenticates)
loginExchangeKey From login
rndValue / csValue Standard anti-replay (derived from the formData below)
formData JSON below
{
  "MDNId":        "960<recipientMsisdn>",          /* PLAINTEXT  '960' + recipient phone */
  "beneDetails":  {
    "miscDetails": "<remarks>",
    "transferMode":"MOBILE"
  },
  "channel":              "SubscriberApp",
  "commodityType":        "WALLET",
  "description":          "<remarks>",
  "inputDetailsDTO":      { "deviceId": "…", "simId": "…" },
  "mfs-transactionType":  "send-money-to-mobile",
  "pocketId":             "",
  "sourceDetails": {
    "MDNId":        "960<myMsisdn>",               /* PLAINTEXT  '960' + my phone */
    "actorRoleType":"RETAIL_SUBSCRIBER",
    "pocketId":     "<my source pocket id>"        /* from login.pocketDetails[0].pocketSummaryDetailsArrayDTO */
  },
  "transactionAmount":  "<amount>",                /* string, MVR */
  "transactionCurrency":"MVR",
  "transferMode":       "MOBILE"
}

deviceId and simId are both Settings.Secure.ANDROID_ID in Thijooree's implementation — matching the device-info pattern from login.

Response — happy path

{
  "2FARequired": "OTP",
  "authenticationType": "OTP",
  "success": true,
  "message": "Operation Completed Successfully",
  "response": [
    {
      "requestObject": { "...": "..." },
      "responseObject": {
        "referenceId": "<reference id>",
        "transactionAmount":  { "amount": 1.0, "currencyCode": "MVR" },
        "netAmount":          { "amount": 1.0, "currencyCode": "MVR" },
        "chargeDetailsDTO":   { "totalFeesInTenantCurrency": { "amount": 0.0, "...": "..." }, "...": "..." },
        "isCompleted":        false,
        "...": "..."
      }
    }
  ]
}

The server SMSes a 6-digit OTP to the sender's phone immediately. Cache referenceId for step 3.


Step 3: confirmFTRequest — submit OTP

Endpoint

POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/confirmFTRequest

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 JSON below
{
  "authenticationType": "OTP",
  "authenticationValue": "<encryptPin(otpCode), hex>",
  "otpTransactionType": "TRANSACTION",
  "referenceId":        "<from step 2>"
}

The OTP code is encrypted with the same encryptPin routine used for the mPIN — i.e. RSA-OAEP-SHA1 against the 2048-bit mPin key, with a fresh 6-character salt. See 01-encryption.md.

Response — happy path

{
  "success": true,
  "message": "Transfer Completed Successfully.",
  "response": [
    {
      "responseObject": {
        "isCompleted": true,
        "balanceInquiryDTO": {
          "currencyCode": "MVR",
          "pocketAmount": 0.45,
          "pocketId":     "<source pocket id>",
          "pocketBalanceMap": { "...": "..." }
        },
        "status": { "replyCode": 0.0, "replyText": "Success" },
        "...": "..."
      }
    }
  ]
}

Response — wrong OTP

The server returns its standard error envelope as a JSON array:

[{
  "success": false, "message": "validation errors",
  "error": [{ "attributeName":"OTP", "errorMessage":"<details>" }]
}]

MfaisaTransferClient parses this into MfaisaInvalidOtpException so the caller can re-prompt without losing the referenceId.

Session expiry

Same envelope as elsewhere — attributeValue: "SESSION_EXPIRED" with HTTP 200; the client throws MfaisaSessionExpiredException. See 03-history.md → Session expiry.


curl Reference

# Step 1 (search a recipient)
python tmp/mfaisa_transfer.py <myMsisdn> <myMpin> <recipientMsisdn>
# Steps 2 + 3 require a live phone OTP and are documented in tmp/mfaisa_transfer.py

 


← Back to Transaction History | README