Mobile recharge flow
End-to-end pattern for a mobile recharge - lookup → product → variants → charges → order → poll terminal status
Mobile recharges differ from gaming top-ups in one important way: in many countries, the MSISDN (mobile number) does not unambiguously identify the operator. A number that starts with +91 88... could be Airtel India, Vi (Vodafone Idea), Reliance Jio, or BSNL. The /topups/lookup endpoint resolves the number against the catalogue and returns matching MOBILE products.
For countries where the operator is unambiguous (or when it's already known), skip the lookup and start from step 2.
The map
┌──────────────────┐
│ 1. Find operator │ POST /api/v1/topups/lookup
└─────────┬────────┘
▼
┌──────────────────┐ (optional)
│ 2. Show variants │ GET /api/v1/topups/products/:id/variants
└─────────┬────────┘
▼
┌──────────────────┐
│ 3. Quote charges │ POST /api/v1/topups/charges
└─────────┬────────┘
▼
┌──────────────────┐
│ 4. Place order │ POST /api/v1/topups/orders
└─────────┬────────┘
▼
┌──────────────────┐
│ 5. Poll status │ GET /api/v1/topups/orders/:id (loop)
└──────────────────┘Step 1 — Detect the operator
curl -X POST "$HOST/api/v1/topups/lookup" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mobile_number": "+918879685100"
}'The endpoint returns a bare JSON array of operators, ordered with Octopus Cards's primary guess first:
[
{ "product_id": 4219, "name": "Airtel India", "country_code": "IND", "identified": true },
{ "product_id": 4231, "name": "Vi (Vodafone Idea) India", "country_code": "IND", "identified": false }
]identified: trueis Octopus Cards's primary guess. For confident customers, just use thisproduct_id.identified: falseentries are alternatives. Show them as "wrong operator? Pick another" disambiguation in the UI.- Empty array means the number could not be resolved. Most likely causes: the number isn't in a supported country, or the catalogue has no active
MOBILEproducts for that operator. Fall back to manual operator selection.
The number must be in E.164 format (+ followed by country code and digits — no spaces, no dashes).
Step 2 — (Optional) Show recharge variants
curl "$HOST/api/v1/topups/products/4219/variants" \
-H "Authorization: Bearer $TOKEN"Mobile recharge variants come in three flavours:
| Category | Shape | Example |
|---|---|---|
| Airtime | Open range, is_fixed_amount: false, min_amount / max_amount set | Top up any amount between ₹10 and ₹10,000 |
| Data | Fixed denominations | 1 GB / 28 days for ₹179, 2 GB / 28 days for ₹239 |
| Bundle | Fixed denominations | Talk + data combos |
The category query parameter on charges/orders constrains the variant selection — pass "Airtime" to force the open-range variant, "Data" for fixed plans.
Step 3 — Calculate charges
curl -X POST "$HOST/api/v1/topups/charges" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": 4219,
"amount": 200,
"category": "Airtime"
}'For fixed plans, pass the exact denomination from fixed_amounts[]. For airtime, pass any amount within [min_amount, max_amount] and Octopus Cards picks the open-range variant.
Step 4 — Place the order
curl -X POST "$HOST/api/v1/topups/orders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": 4219,
"amount": 200,
"category": "Airtime",
"input_data": {
"phone_number": "+918879685100"
},
"client_reference": "RECHARGE_REQ_4391"
}'Notes specific to mobile:
- The
phone_numberfield is typedphonein the product'sinput_fields— the server normalises it to E.164 if local format is sent (8879685100→+918879685100when paired with country context). To be safe, always send E.164 from the client side. - Same number as the lookup. Don't mix: a recharge for product
4219(Airtel India) with a phone number on Vi will be rejected as user-fixable. client_referenceis the dedup + idempotency key. Use it to make retries safe — duplicate(client_id, client_reference)pairs are rejected with400 "Duplicate client_reference".
Step 5 — Poll until terminal
Same loop as gaming — see the Gaming top-up flow polling step for the curl/Go/Node/Python implementations. Mobile recharges typically terminate faster than gaming (5–15 seconds) since the operator's prepaid platform processes them directly.
Handling failure modes
| Failure | Likely cause | Next |
|---|---|---|
is_user_fixable: true, failure_code: INVALID_RECIPIENT | Customer typed the wrong MSISDN | Show failure_reason. Wallet refunded. Let them re-enter. |
is_user_fixable: true, failure_code: RECIPIENT_INELIGIBLE | Lookup picked the wrong operator, or the customer's number can't accept the chosen plan | Show operator alternatives or a different plan. |
is_user_fixable: true, failure_code: RECIPIENT_BARRED | The MSISDN is suspended/deactivated on the operator's side | Surface to customer; nothing Octopus Cards can do. |
is_user_fixable: false, failure_code: UNKNOWN | Octopus Cards exhausted its automatic recovery without classifying the failure. Rare. | Surface "we couldn't complete this order". Wallet refunded. |
Transient operator outages and connectivity issues do not appear in this table — they keep the order in PENDING and Octopus Cards retries until the operator comes back. From a foreground perspective, those look like a slower-than-usual order, not a failure.
Country-specific gotchas
- India: lookup is highly reliable;
identified: truerate is >99%. - Brazil, US, Canada: number portability means the number alone doesn't identify the operator. Lookup is best-effort — surface alternatives.
- Nigeria, Kenya, Ghana: lookup works but is sensitive to the country prefix. Reject MSISDNs without the leading
+. - Smaller markets: lookup may return empty for valid numbers if the catalogue can't resolve operators in that country. Fall back to a country → operator dropdown.
Why no "amount validation" before order?
Because the variant selector is fuzzy. For airtime variants with open ranges, almost any positive amount in the operator's currency is valid. Octopus Cards pushes amount validation server-side in /topups/charges and /topups/orders — fail fast on 400 "No variant available" and surface the message to the customer.
Summary
- E.164 numbers, always. The lookup endpoint is strict; the order endpoint is permissive but stricter is safer.
lookup → charges → order → pollis the canonical flow.- Two
MOBILEvariant shapes: open-range Airtime, fixed Data/Bundle. Thecategoryfilter disambiguates. - Most recharges terminate within 10 seconds. Anything past 5 minutes is an internal incident, not a customer issue.