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)