# Ooredoo M-Faisa API Documentation Reverse-engineered from traffic captures and live Frida hooks of the official Ooredoo SuperApp (`com.mventus.ooredoomaldives`). [Play Store](https://play.google.com/store/apps/details?id=com.mventus.ooredoomaldives) --- ## Overview M-Faisa is Ooredoo Maldives' mobile wallet, exposed via a JSON/form-encoded REST API on `superapp.ooredoo.mv`. The wire format is unusual in three ways: 1. **Field-level RSA encryption.** The MSISDN (`mdnId`, `mobileNumber`, `userName`, `initiatingMDN`, `identifier`) and the mPIN (`mPin`) are each encrypted with a different RSA public key before being placed in the request body. See [01-encryption.md](01-encryption.md). 2. **Anti-replay envelope.** Every session-scoped form-encoded POST carries an `rndValue` (RSA-encrypted timestamp) and a `csValue` (Adler32 of `formDataJson + nonceStr`). See [01-encryption.md → `rndValue` / `csValue`](01-encryption.md#anti-replay-envelope-rndvalue--csvalue). 3. **Cloudflare-fingerprinted header order.** A `User-Agent` header sent explicitly (instead of letting OkHttp add it last) returns HTTP 400. --- ## Base URL ``` https://superapp.ooredoo.mv ``` All M-Faisa endpoints are mounted at `/api/mfaisaa-bff/mfino/v1.1/web/...`. --- ## Authentication Model | Value | How obtained | How used | |---|---|---| | `loginExchangeKey` | Returned by `doMobileLogin` on success | Held in memory only; identifies the session | | Session timeout | `mobileLoginSessionTimeout` field, default **240 seconds** | After expiry the user must re-login (no refresh-token flow) | Because there is no refresh, Thijooree re-runs `fetchSubscriberByMDN` + `doMobileLogin` on every cold-start refresh, using the saved msisdn + mPIN from `CredentialStore`. The `SESSION_EXPIRED` error envelope is also caught at runtime and the session is silently re-established before retrying the failed request. --- ## Common Request Headers ``` Content-Type: application/json; charset=UTF-8 (fetchSubscriberByMDN) Content-Type: application/x-www-form-urlencoded (every other endpoint) Host: superapp.ooredoo.mv Connection: Keep-Alive Accept-Encoding: gzip User-Agent: okhttp/4.12.0 ``` > **Do NOT set User-Agent in code.** Cloudflare fingerprints the header order; an explicit `User-Agent` header is pushed to the front of the request and the request is rejected with HTTP 400. Let OkHttp's `BridgeInterceptor` add the default `okhttp/4.12.0` at the end. --- ## Login Flow ``` Client Server | | | POST /fetchSubscriberByMDN | | { mdnId: encryptMobile(msisdn) } | |---------------------------------------------------->| | { success, subscriberRegistered, kycStatus, ... } | |<----------------------------------------------------| | | | (abort if subscriberRegistered=false | | or kycStatus != "Full KYC") | | | | POST /doMobileLogin | | channel=C03 | | formData={ deviceGeoInfo, mPin: encryptPin(mpin), | | mobileNumber: ..., userName: ..., | | role:"RETAIL_SUBSCRIBER", | | tenantCode:"ooredoo" } | | formDataCs=null | |---------------------------------------------------->| | { success, loginExchangeKey, pocketDetails: [...]} | |<----------------------------------------------------| ``` --- ## Documents | # | File | Description | |---|---|---| | 1 | [Encryption & Anti-Replay](01-encryption.md) | Mobile / mPin RSA, the `rndValue` + `csValue` envelope, the Gson `=` quirk, key-extraction story | | 2 | [Login](02-login.md) | Subscriber lookup + mPIN login | | 3 | [Transaction History](03-history.md) | Paginated history per session | | 4 | [Transfer Money](04-transfer.md) | Three-step wallet-to-wallet send: recipient lookup → initiate (server SMSes OTP) → confirm | | 5 | [QR Merchant Payment](05-qr-pay.md) | Three-step "smart pay" scan-to-merchant: QR lookup → initiate → confirm. **No OTP** (`2FARequired=NONE`) | ---   --- > **Next →** [Encryption & Anti-Replay](01-encryption.md)