GuidesWalkthroughs

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: true is Octopus Cards's primary guess. For confident customers, just use this product_id.
  • identified: false entries 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 MOBILE products 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:

CategoryShapeExample
AirtimeOpen range, is_fixed_amount: false, min_amount / max_amount setTop up any amount between ₹10 and ₹10,000
DataFixed denominations1 GB / 28 days for ₹179, 2 GB / 28 days for ₹239
BundleFixed denominationsTalk + 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_number field is typed phone in the product's input_fields — the server normalises it to E.164 if local format is sent (8879685100+918879685100 when 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_reference is the dedup + idempotency key. Use it to make retries safe — duplicate (client_id, client_reference) pairs are rejected with 400 "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

FailureLikely causeNext
is_user_fixable: true, failure_code: INVALID_RECIPIENTCustomer typed the wrong MSISDNShow failure_reason. Wallet refunded. Let them re-enter.
is_user_fixable: true, failure_code: RECIPIENT_INELIGIBLELookup picked the wrong operator, or the customer's number can't accept the chosen planShow operator alternatives or a different plan.
is_user_fixable: true, failure_code: RECIPIENT_BARREDThe MSISDN is suspended/deactivated on the operator's sideSurface to customer; nothing Octopus Cards can do.
is_user_fixable: false, failure_code: UNKNOWNOctopus 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: true rate 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 → poll is the canonical flow.
  • Two MOBILE variant shapes: open-range Airtime, fixed Data/Bundle. The category filter disambiguates.
  • Most recharges terminate within 10 seconds. Anything past 5 minutes is an internal incident, not a customer issue.

On this page