add support to detect dhiraagu and ooredoo packages
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 2s

This commit is contained in:
2026-05-17 00:25:48 +05:00
parent 93405aade2
commit ccc9e11d55
9 changed files with 781 additions and 3 deletions

View File

@@ -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) }
}
}

View File

@@ -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 }
}
}

View File

@@ -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

View File

@@ -26,14 +26,22 @@
android:orientation="vertical"
android:padding="20dp">
<!-- From account dropdown -->
<!-- From label + account dropdown -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:text="@string/transfer_label_from"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilFrom"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/transfer_from"
android:layout_marginBottom="16dp">
android:layout_marginBottom="8dp">
<AutoCompleteTextView
android:id="@+id/actvFrom"
@@ -118,7 +126,15 @@
</com.google.android.material.card.MaterialCardView>
<!-- To field row: input + pick contact button -->
<!-- To label + field row: input + pick contact button -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:text="@string/transfer_label_to"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -171,6 +187,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="8dp"
app:cardCornerRadius="4dp"
app:cardElevation="0dp"
app:strokeWidth="1dp"
@@ -228,6 +245,66 @@
</com.google.android.material.card.MaterialCardView>
<!-- Service selector: invisible to reserve space, prevents amount/remarks from shifting -->
<LinearLayout
android:id="@+id/layoutServiceSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="8dp"
android:visibility="invisible">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/transfer_select_service"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroupService"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:singleSelection="true"
app:selectionRequired="true">
<com.google.android.material.chip.Chip
android:id="@+id/chipDhiraaguReload"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dhiraagu Reload"
android:visibility="gone" />
<com.google.android.material.chip.Chip
android:id="@+id/chipDhiraaguBill"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dhiraagu Bill Pay"
android:visibility="gone" />
<com.google.android.material.chip.Chip
android:id="@+id/chipRaastas"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Raastas"
android:visibility="gone" />
<com.google.android.material.chip.Chip
android:id="@+id/chipOoredooBill"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ooredoo Bill Pay"
android:visibility="gone" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
<View
android:id="@+id/spacerTo"
android:layout_width="match_parent"

View File

@@ -147,6 +147,10 @@
<string name="transfer_tab_contacts">Contacts</string>
<string name="transfer_from">From account</string>
<string name="transfer_to">Account Number or Favara ID</string>
<string name="transfer_label_from">From</string>
<string name="transfer_label_to">To</string>
<string name="transfer_select_service">Select Service</string>
<string name="transfer_fahipay_phone_only">Fahipay transfers require a 7-digit phone number</string>
<string name="transfer_my_accounts">My Accounts</string>
<string name="transfer_same_as_from">This is the same account as the sender</string>
<string name="transfer_lookup_account">Look up account</string>

View File

@@ -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 `<script>` block:
```html
<script>
var nonce = "feb6acb9-a532-428e-a4d6-c4e5515b4bc3";
...
</script>
```
**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 |
---
&nbsp;
---
[← README](README.md)

100
docs/dhiraaguapi/README.md Normal file
View File

@@ -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 |
---
&nbsp;
---
> **Next →** [Number Lookup](01-number-lookup.md)

View File

@@ -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 |
---
&nbsp;
---
[← README](README.md)

85
docs/ooredooapi/README.md Normal file
View File

@@ -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 |
---
&nbsp;
---
> **Next →** [Number Validation](01-number-validation.md)