# 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](01-encryption.md#anti-replay-envelope-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](01-encryption.md#anti-replay-envelope-rndvalue--csvalue) | | `formData` | JSON below ([html-safe `=` escaping](01-encryption.md#html-safe-gson--escape)) | ```json { "beneficaryDetails": { "MDNId": "", "actorRoleType": "RETAIL_SUBSCRIBER" }, "initiatorDetailsDTO": { "initiatingMDN": "", "initiatingRoleId": "", "initiatorRole": "RETAIL_SUBSCRIBER" } } ``` > `suscriberId` (note the server's typo) comes from the top level of the `doMobileLogin` response — see [02-login.md](02-login.md#step-2-domobilelogin). ### Response — happy path ```json [ { "success": true, "message": "Operation Completed Successfully", "response": [ [ { "pocketId": "", "pocketCurrency": "USD", "pocketValueType": "PAYPAL_USD", "name": "", "MDNId": "", "walletId": "", "actorId": "", "...": "..." }, { "pocketId": "", "pocketCurrency": "MVR", "pocketValueType": "EMONEY", "name": "", "MDNId": "", "walletId": "", "actorId": "", "...": "..." } ] ] } ] ``` 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 ```json [{ "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` | `` — 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 | ```json { "MDNId": "960", /* PLAINTEXT — '960' + recipient phone */ "beneDetails": { "miscDetails": "", "transferMode":"MOBILE" }, "channel": "SubscriberApp", "commodityType": "WALLET", "description": "", "inputDetailsDTO": { "deviceId": "…", "simId": "…" }, "mfs-transactionType": "send-money-to-mobile", "pocketId": "", "sourceDetails": { "MDNId": "960", /* PLAINTEXT — '960' + my phone */ "actorRoleType":"RETAIL_SUBSCRIBER", "pocketId": "" /* from login.pocketDetails[0].pocketSummaryDetailsArrayDTO */ }, "transactionAmount": "", /* 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 ```json { "2FARequired": "OTP", "authenticationType": "OTP", "success": true, "message": "Operation Completed Successfully", "response": [ { "requestObject": { "...": "..." }, "responseObject": { "referenceId": "", "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": ""}` | | `transactionAuthDetails` | JSON below | ```json { "authenticationType": "OTP", "authenticationValue": "", "otpTransactionType": "TRANSACTION", "referenceId": "" } ``` 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](01-encryption.md#mpin-key-2048-bit). ### Response — happy path ```json { "success": true, "message": "Transfer Completed Successfully.", "response": [ { "responseObject": { "isCompleted": true, "balanceInquiryDTO": { "currencyCode": "MVR", "pocketAmount": 0.45, "pocketId": "", "pocketBalanceMap": { "...": "..." } }, "status": { "replyCode": 0.0, "replyText": "Success" }, "...": "..." } } ] } ``` ### Response — wrong OTP The server returns its standard error envelope as a JSON array: ```json [{ "success": false, "message": "validation errors", "error": [{ "attributeName":"OTP", "errorMessage":"
" }] }] ``` `MfaisaTransferClient` parses this into [`MfaisaInvalidOtpException`](../../app/src/main/java/sh/sar/basedbank/api/mfaisa/MfaisaModels.kt) 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](03-history.md#session-expiry). --- ## curl Reference ```bash # Step 1 (search a recipient) python tmp/mfaisa_transfer.py # Steps 2 + 3 require a live phone OTP and are documented in tmp/mfaisa_transfer.py ``` ---   --- > **← Back to** [Transaction History](03-history.md) | [README](README.md)