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 thedoMobileLoginresponse — 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 | Next → QR Merchant Payment