diff --git a/app/src/main/java/sh/sar/basedbank/api/dhiraagu/DhiraaguClient.kt b/app/src/main/java/sh/sar/basedbank/api/dhiraagu/DhiraaguClient.kt new file mode 100644 index 0000000..d485b6e --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/dhiraagu/DhiraaguClient.kt @@ -0,0 +1,71 @@ +package sh.sar.basedbank.api.dhiraagu + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.util.concurrent.TimeUnit + +class DhiraaguClient { + + private val client = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .build() + + enum class CustType { RELOAD, BILL_PAY, UNSUPPORTED } + + companion object { + private const val UA = "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0" + } + + data class Result(val type: CustType, val ownerName: String = "") + + fun validateNumber(number: String): Result { + // Step 1: fetch Easy Pay page to extract the nonce + val pageResp = client.newCall( + Request.Builder() + .url("https://www.dhiraagu.com.mv/services/easy-pay") + .header("User-Agent", UA) + .get() + .build() + ).execute() + val html = pageResp.body?.string() ?: return Result(CustType.UNSUPPORTED) + pageResp.close() + + val nonce = Regex("""var nonce = "([^"]+)"""").find(html) + ?.groupValues?.getOrNull(1) ?: return Result(CustType.UNSUPPORTED) + + // Step 2: look up number + val body = """{"number":"$number"}""" + .toRequestBody("application/json".toMediaType()) + val resp = client.newCall( + Request.Builder() + .url("https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-3A22-485B-A518-DA6B6DE653A5&sub=dhiraaguIO&act=infoUnlisted") + .post(body) + .header("User-Agent", UA) + .header("nonce", nonce) + .header("Origin", "https://dhiraagu.com.mv") + .build() + ).execute() + val json = resp.body?.string() ?: return Result(CustType.UNSUPPORTED) + resp.close() + + return try { + val obj = JSONObject(json) + if (obj.optString("respStatus") != "OK") return Result(CustType.UNSUPPORTED) + val prepaid = obj.optJSONArray("serviceDetails") + ?.optJSONObject(0) + ?.optString("prepaidIndicator") + val ownerName = obj.optJSONObject("accountOwnerInfo") + ?.optString("name") ?: "" + val type = when (prepaid) { + "Y" -> CustType.RELOAD + "N" -> CustType.BILL_PAY + else -> CustType.UNSUPPORTED + } + Result(type, ownerName) + } catch (_: Exception) { Result(CustType.UNSUPPORTED) } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/OoredooClient.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/OoredooClient.kt new file mode 100644 index 0000000..c4e27ae --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/OoredooClient.kt @@ -0,0 +1,43 @@ +package sh.sar.basedbank.api.fahipay + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import java.util.concurrent.TimeUnit + +class OoredooClient { + + private val client = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .build() + + enum class CustType { PRE, POST, HYBRID, UNSUPPORTED } + + /** + * Validates an Ooredoo number and returns which services are supported. + * @param number 7-digit Maldivian phone number (without country code) + */ + fun validateNumber(number: String): CustType { + val resp = client.newCall( + Request.Builder() + .url("https://www.ooredoo.mv/ooredoo-prod/QuickPayPackage/v1/numberTypeValidation?action=cust_details&msisdn=960$number") + .get() + .build() + ).execute() + val json = resp.body?.string() ?: return CustType.UNSUPPORTED + resp.close() + return try { + val custType = JSONObject(json) + .optJSONObject("data") + ?.optString("custType") + ?.takeIf { it.isNotBlank() && it != "null" } + when (custType) { + "PRE" -> CustType.PRE + "POST" -> CustType.POST + "HYBRID" -> CustType.HYBRID + else -> CustType.UNSUPPORTED + } + } catch (_: Exception) { CustType.UNSUPPORTED } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt index 94b0431..ab80e3a 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt @@ -32,6 +32,8 @@ import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R import sh.sar.basedbank.api.bml.BmlLoginFlow +import sh.sar.basedbank.api.dhiraagu.DhiraaguClient +import sh.sar.basedbank.api.fahipay.OoredooClient import sh.sar.basedbank.api.bml.BmlTransferResult import sh.sar.basedbank.api.mib.MibAccount import sh.sar.basedbank.api.mib.MibBeneficiary @@ -65,6 +67,10 @@ class TransferFragment : Fragment() { private var resolvedRecipientName = "" private var resolvedBankName = "" + // Selected Fahipay service when source is Fahipay and destination is a phone number + // Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL" + private var selectedFahipayService: String? = null + private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult @@ -220,7 +226,9 @@ class TransferFragment : Fragment() { binding.btnClearToInfo.setOnClickListener { resolvedAccountNumber = "" resolvedRecipientName = "" + selectedFahipayService = null binding.cardToInfo.visibility = View.GONE + binding.layoutServiceSelector.visibility = View.INVISIBLE binding.tilTo.visibility = View.VISIBLE binding.btnPickContact.visibility = View.VISIBLE binding.btnScanQr.visibility = View.VISIBLE @@ -265,6 +273,16 @@ class TransferFragment : Fragment() { return } + // Fahipay source: only phone numbers are supported + if (selectedAccount?.profileType == "FAHIPAY") { + if (AccountInputParser.detect(accountNumber) == AccountInputParser.InputType.PHONE) { + lookupFahipayTarget(accountNumber) + } else { + binding.tilTo.error = getString(R.string.transfer_fahipay_phone_only) + } + return + } + val mibSess = session val bmlSess = bmlSession if (mibSess == null && bmlSess == null) { @@ -352,6 +370,99 @@ class TransferFragment : Fragment() { } } + private fun lookupFahipayTarget(number: String) { + binding.tilTo.isEnabled = false + viewLifecycleOwner.lifecycleScope.launch { + data class LookupResult( + val dhiraagu: DhiraaguClient.Result, + val ooredoo: OoredooClient.CustType + ) + val result = withContext(Dispatchers.IO) { + if (number.startsWith("7")) { + // Dhiraagu first, fall back to Ooredoo + val d = try { DhiraaguClient().validateNumber(number) } + catch (_: Exception) { DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED) } + val o = if (d.type == DhiraaguClient.CustType.UNSUPPORTED) + try { OoredooClient().validateNumber(number) } + catch (_: Exception) { OoredooClient.CustType.UNSUPPORTED } + else OoredooClient.CustType.UNSUPPORTED + LookupResult(d, o) + } else { + // Ooredoo first, fall back to Dhiraagu + val o = try { OoredooClient().validateNumber(number) } + catch (_: Exception) { OoredooClient.CustType.UNSUPPORTED } + val d = if (o == OoredooClient.CustType.UNSUPPORTED) + try { DhiraaguClient().validateNumber(number) } + catch (_: Exception) { DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED) } + else DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED) + LookupResult(d, o) + } + } + binding.tilTo.isEnabled = true + + val dhiraaguName = result.dhiraagu.ownerName.takeIf { it.isNotBlank() } + + // Collect all applicable services + val services = buildList { + if (result.dhiraagu.type == DhiraaguClient.CustType.RELOAD) add("DHIRAAGU_RELOAD") + if (result.dhiraagu.type == DhiraaguClient.CustType.BILL_PAY) add("DHIRAAGU_BILL") + if (result.ooredoo == OoredooClient.CustType.PRE || result.ooredoo == OoredooClient.CustType.HYBRID) add("RAASTAS") + if (result.ooredoo == OoredooClient.CustType.POST || result.ooredoo == OoredooClient.CustType.HYBRID) add("OOREDOO_BILL") + } + + if (services.isEmpty()) return@launch + + // Only one option — auto-select, no chip UI needed + if (services.size == 1) { + selectFahipayService(services[0], number, dhiraaguName) + return@launch + } + + // Multiple options (Ooredoo HYBRID) — show chips + binding.chipDhiraaguReload.visibility = if ("DHIRAAGU_RELOAD" in services) View.VISIBLE else View.GONE + binding.chipDhiraaguBill.visibility = if ("DHIRAAGU_BILL" in services) View.VISIBLE else View.GONE + binding.chipRaastas.visibility = if ("RAASTAS" in services) View.VISIBLE else View.GONE + binding.chipOoredooBill.visibility = if ("OOREDOO_BILL" in services) View.VISIBLE else View.GONE + binding.layoutServiceSelector.visibility = View.VISIBLE + binding.chipGroupService.clearCheck() + + binding.chipDhiraaguReload.setOnCheckedChangeListener { _, checked -> + if (checked) { selectFahipayService("DHIRAAGU_RELOAD", number, dhiraaguName); binding.layoutServiceSelector.visibility = View.INVISIBLE } + } + binding.chipDhiraaguBill.setOnCheckedChangeListener { _, checked -> + if (checked) { selectFahipayService("DHIRAAGU_BILL", number, dhiraaguName); binding.layoutServiceSelector.visibility = View.INVISIBLE } + } + binding.chipRaastas.setOnCheckedChangeListener { _, checked -> + if (checked) { selectFahipayService("RAASTAS", number); binding.layoutServiceSelector.visibility = View.INVISIBLE } + } + binding.chipOoredooBill.setOnCheckedChangeListener { _, checked -> + if (checked) { selectFahipayService("OOREDOO_BILL", number); binding.layoutServiceSelector.visibility = View.INVISIBLE } + } + } + } + + private fun selectFahipayService(service: String, number: String, ownerName: String? = null) { + selectedFahipayService = service + val contacts = viewModel.contacts.value ?: emptyList() + val displayName = ownerName + ?: contacts.firstOrNull { it.benefAccount == number }?.benefNickName + ?: number + val serviceLabel = when (service) { + "RAASTAS" -> "Raastas" + "OOREDOO_BILL" -> "Ooredoo Bill Pay" + "DHIRAAGU_RELOAD" -> "Dhiraagu Reload" + "DHIRAAGU_BILL" -> "Dhiraagu Bill Pay" + else -> service + } + prefillToDirectly( + accountNumber = number, + displayName = displayName, + subtitle = "$serviceLabel · $number", + colorHex = "#FF6B00", + imageHash = null + ) + } + private fun prefillToDirectly( accountNumber: String, displayName: String, @@ -661,7 +772,9 @@ class TransferFragment : Fragment() { resolvedAccountNumber = "" resolvedRecipientName = "" resolvedBankName = "" + selectedFahipayService = null binding.cardToInfo.visibility = View.GONE + binding.chipGroupService.visibility = View.GONE binding.tilTo.visibility = View.VISIBLE binding.btnPickContact.visibility = View.VISIBLE binding.btnScanQr.visibility = View.VISIBLE diff --git a/app/src/main/res/layout/fragment_transfer.xml b/app/src/main/res/layout/fragment_transfer.xml index 476c85f..23c3774 100644 --- a/app/src/main/res/layout/fragment_transfer.xml +++ b/app/src/main/res/layout/fragment_transfer.xml @@ -26,14 +26,22 @@ android:orientation="vertical" android:padding="20dp"> - + + + + android:layout_marginBottom="8dp"> - + + + + + + + + + + + + + + + + + + + + + + Contacts From account Account Number or Favara ID + From + To + Select Service + Fahipay transfers require a 7-digit phone number My Accounts This is the same account as the sender Look up account diff --git a/docs/dhiraaguapi/01-number-lookup.md b/docs/dhiraaguapi/01-number-lookup.md new file mode 100644 index 0000000..e14021d --- /dev/null +++ b/docs/dhiraaguapi/01-number-lookup.md @@ -0,0 +1,172 @@ +# Number Lookup + +Validate a Dhiraagu mobile number and determine whether it is prepaid (reload) or postpaid (bill pay). + +This is a two-step process: first fetch the Easy Pay page to extract the nonce, then POST the number to the IO API. + +--- + +## Step 1 — Fetch Nonce + +### Endpoint + +``` +GET https://www.dhiraagu.com.mv/services/easy-pay +``` + +### Request Headers + +``` +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0 +``` + +### curl Example + +```bash +curl --request GET \ + --url 'https://www.dhiraagu.com.mv/services/easy-pay' \ + --header 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0' +``` + +### Response + +The server returns an HTML page. The nonce is embedded in a ` +``` + +**Extraction regex:** + +``` +var nonce = "([^"]+)" +``` + +The nonce is a UUID (36-char string). It is tied to this page load and must be used immediately in Step 2. + +--- + +## Step 2 — Number Lookup + +### Endpoint + +``` +POST https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-3A22-485B-A518-DA6B6DE653A5&sub=dhiraaguIO&act=infoUnlisted +``` + +### Request Headers + +| Header | Value | +|---|---| +| `Content-Type` | `application/json` | +| `User-Agent` | `Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0` | +| `nonce` | UUID extracted from Step 1 | +| `Origin` | `https://dhiraagu.com.mv` | + +### Request Body + +```json +{ + "number": "7654321" +} +``` + +| Field | Type | Notes | +|---|---|---| +| `number` | `string` | 7-digit Dhiraagu mobile number (local format, no country code) | + +### curl Example + +```bash +curl --request POST \ + --url 'https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-3A22-485B-A518-DA6B6DE653A5&sub=dhiraaguIO&act=infoUnlisted' \ + --header 'Content-Type: application/json' \ + --header 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0' \ + --header 'nonce: feb6acb9-a532-428e-a4d6-c4e5515b4bc3' \ + --header 'Origin: https://dhiraagu.com.mv' \ + --data '{"number":"7654321"}' +``` + +--- + +## Responses + +### Success — Prepaid (Reload) + +```json +{ + "respStatus": "OK", + "serviceDetails": [ + { + "prepaidIndicator": "Y", + "serviceType": "GSM" + } + ], + "accountOwnerInfo": { + "name": "Ahmed Mohamed", + "accountNo": "7654321" + } +} +``` + +| Field | Type | Description | +|---|---|---| +| `respStatus` | `string` | `"OK"` on success | +| `serviceDetails[0].prepaidIndicator` | `string` | `"Y"` = prepaid (reload), `"N"` = postpaid (bill pay) | +| `accountOwnerInfo.name` | `string` | Account holder's full name | + +### Success — Postpaid (Bill Pay) + +Same structure as above, with `prepaidIndicator: "N"`. + +```json +{ + "respStatus": "OK", + "serviceDetails": [ + { + "prepaidIndicator": "N", + "serviceType": "GSM" + } + ], + "accountOwnerInfo": { + "name": "Fatima Ali", + "accountNo": "7123456" + } +} +``` + +### Failure — Number Not Found + +```json +{ + "respStatus": "FAILED", + "respMessage": "Number not found" +} +``` + +`respStatus` is not `"OK"`. Treat any non-OK status as unsupported. + +### Failure — Invalid / Expired Nonce + +If the nonce is missing, invalid, or has already been used, the server returns an error response (non-OK status or HTTP 4xx). Re-fetch the Easy Pay page to get a fresh nonce. + +--- + +## Result Mapping + +| `prepaidIndicator` | Service | +|---|---| +| `"Y"` | Dhiraagu Reload | +| `"N"` | Dhiraagu Bill Pay | +| absent / other | Unsupported — fall back to Ooredoo lookup | + +--- + +  + +--- + +[← README](README.md) diff --git a/docs/dhiraaguapi/README.md b/docs/dhiraaguapi/README.md new file mode 100644 index 0000000..f85e1d8 --- /dev/null +++ b/docs/dhiraaguapi/README.md @@ -0,0 +1,100 @@ +# Dhiraagu API Documentation + +Reverse-engineered from traffic captures of the Dhiraagu Easy Pay web service (`dhiraagu.com.mv`). + +--- + +## Overview + +Dhiraagu exposes a number-lookup API used by their Easy Pay web page to validate mobile numbers before payment. The API uses a two-step flow: + +1. **Nonce extraction** — fetch the Easy Pay HTML page and extract a one-time nonce token embedded in the page JavaScript. +2. **Number lookup** — POST the mobile number with the nonce to the IO API endpoint, which returns the account type and owner name. + +The nonce is single-use and page-session-specific; it must be re-fetched for each lookup. + +--- + +## Base URL + +``` +https://www.dhiraagu.com.mv +``` + +--- + +## Authentication Model + +| Value | How obtained | How used | +|---|---|---| +| `nonce` | Extracted from Easy Pay page HTML | Sent as `nonce` request header on the lookup POST | + +No persistent session or login is required. The nonce is valid for a single lookup request. + +--- + +## Common Request Headers + +``` +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0 +Origin: https://dhiraagu.com.mv +``` + +The `User-Agent` header is required; requests without it are rejected by the server. + +--- + +## Number Lookup Flow + +``` +Client Server + | | + | GET /services/easy-pay | ← fetch page to get nonce + |----------------------------------------->| + | 200 OK (HTML page) | + | var nonce = "feb6acb9-a532-428e-..." | + |<-----------------------------------------| + | | + | POST /api/sdk-dhr-webapi.ashx | + | ?website_id=CA2BB809... | + | &sub=dhiraaguIO&act=infoUnlisted | + | nonce: feb6acb9-a532-428e-... | + | { "number": "7654321" } | + |----------------------------------------->| + | { respStatus: "OK", | + | serviceDetails: [{prepaidIndicator}], | + | accountOwnerInfo: { name } } | + |<-----------------------------------------| +``` + +--- + +## Result Interpretation + +| `prepaidIndicator` | Meaning | +|---|---| +| `"Y"` | Prepaid — use **Dhiraagu Reload** | +| `"N"` | Postpaid — use **Dhiraagu Bill Pay** | +| absent / other | Number not found or unsupported | + +--- + +## Applicable Numbers + +Only Dhiraagu mobile numbers are returned by this API. Dhiraagu numbers in the Maldives start with **7** (7-digit local format, e.g. `7654321`). + +--- + +## Documents + +| # | File | Description | +|---|---|---| +| 1 | [Number Lookup](01-number-lookup.md) | Validate a Dhiraagu number and determine account type | + +--- + +  + +--- + +> **Next →** [Number Lookup](01-number-lookup.md) diff --git a/docs/ooredooapi/01-number-validation.md b/docs/ooredooapi/01-number-validation.md new file mode 100644 index 0000000..ca16785 --- /dev/null +++ b/docs/ooredooapi/01-number-validation.md @@ -0,0 +1,113 @@ +# Number Validation + +Validate an Ooredoo mobile number and determine its account type (prepaid, postpaid, or hybrid). + +--- + +## Endpoint + +``` +GET https://www.ooredoo.mv/ooredoo-prod/QuickPayPackage/v1/numberTypeValidation +``` + +--- + +## Request + +### Query Parameters + +| Parameter | Value | Notes | +|---|---|---| +| `action` | `cust_details` | Always `cust_details` | +| `msisdn` | `9607654321` | Full MSISDN — country code `960` + 7-digit local number | + +### curl Example + +```bash +curl --request GET \ + --url 'https://www.ooredoo.mv/ooredoo-prod/QuickPayPackage/v1/numberTypeValidation?action=cust_details&msisdn=9609654321' +``` + +--- + +## Responses + +### Success — Prepaid + +```json +{ + "custType": "PRE", + "msisdn": "9609654321" +} +``` + +| Field | Type | Description | +|---|---|---| +| `custType` | `string` | `"PRE"` = prepaid customer | +| `msisdn` | `string` | The MSISDN that was queried | + +→ Offer **Raastas** top-up only. + +--- + +### Success — Postpaid + +```json +{ + "custType": "POST", + "msisdn": "9609123456" +} +``` + +→ Offer **Ooredoo Bill Pay** only. + +--- + +### Success — Hybrid + +```json +{ + "custType": "HYBRID", + "msisdn": "9609789012" +} +``` + +→ Offer both **Raastas** and **Ooredoo Bill Pay**. The user must select which service to use. + +--- + +### Failure — Number Not Found + +```json +{ + "custType": null, + "errorMessage": "Data Not Found", + "msisdn": "9609000000" +} +``` + +| Field | Type | Description | +|---|---|---| +| `custType` | `null` | Number is not an Ooredoo subscriber | +| `errorMessage` | `string` | `"Data Not Found"` | + +Treat `custType: null` as unsupported — fall back to Dhiraagu lookup. + +--- + +## Result Mapping + +| `custType` | Service(s) | +|---|---| +| `"PRE"` | Raastas only | +| `"POST"` | Ooredoo Bill Pay only | +| `"HYBRID"` | Raastas + Ooredoo Bill Pay (show chip selector) | +| `null` / absent | Unsupported — fall back to Dhiraagu lookup | + +--- + +  + +--- + +[← README](README.md) diff --git a/docs/ooredooapi/README.md b/docs/ooredooapi/README.md new file mode 100644 index 0000000..344c6be --- /dev/null +++ b/docs/ooredooapi/README.md @@ -0,0 +1,85 @@ +# Ooredoo API Documentation + +Reverse-engineered from traffic captures of the Ooredoo Quick Pay web service (`ooredoo.mv`). + +--- + +## Overview + +Ooredoo exposes a number-validation API used by their Quick Pay service to check mobile numbers before payment. It is a single unauthenticated GET request that returns the customer type for a given MSISDN. + +The response determines which payment service to offer: + +- **PRE** (prepaid) → Raastas top-up only +- **POST** (postpaid) → Ooredoo Bill Pay only +- **HYBRID** → both Raastas and Ooredoo Bill Pay (user must choose) +- No match → number is not an Ooredoo number + +--- + +## Base URL + +``` +https://www.ooredoo.mv +``` + +--- + +## Authentication Model + +No authentication required. The endpoint is publicly accessible. + +--- + +## Common Request Headers + +No special headers are required. Standard HTTP/HTTPS headers apply. + +--- + +## Number Lookup Flow + +``` +Client Server + | | + | GET /ooredoo-prod/QuickPayPackage/v1/ | + | numberTypeValidation | + | ?action=cust_details | + | &msisdn=9607654321 | + |------------------------------------------->| + | { custType: "PRE" } | + |<-------------------------------------------| +``` + +--- + +## Result Interpretation + +| `custType` | Meaning | +|---|---| +| `"PRE"` | Prepaid — offer **Raastas** top-up | +| `"POST"` | Postpaid — offer **Ooredoo Bill Pay** | +| `"HYBRID"` | Both prepaid and postpaid — offer both (user selects) | +| `null` / absent | Not an Ooredoo number | + +--- + +## Applicable Numbers + +Only Ooredoo mobile numbers are returned by this API. Ooredoo numbers in the Maldives start with **9** (7-digit local format, e.g. `9654321`). The API expects the full MSISDN including country code `960`. + +--- + +## Documents + +| # | File | Description | +|---|---|---| +| 1 | [Number Validation](01-number-validation.md) | Validate an Ooredoo number and determine account type | + +--- + +  + +--- + +> **Next →** [Number Validation](01-number-validation.md)