API ReferenceTopups

Create Order

Place a gaming top-up or mobile recharge order

POST /api/v1/topups/orders

Creates a top-up order. Octopus Cards validates your input, picks the best variant for the given product_id + amount, debits your wallet (or redeems a voucher code if provided), and submits the order for fulfilment.

Unlike vouchers, top-ups are typically fulfilled asynchronously: most fulfilment confirms via back-channel, and your order status moves from PENDINGDELIVERED or FAILED over seconds to a few minutes. Poll GET /topups/orders/:id until terminal.

We strive to eventually deliver every order

Transient fulfilment issues — timeouts, rate limits, transient declines — do not surface as FAILED. They keep the order in PENDING and the retry pipeline keeps working until the order either delivers or hits a clearly customer-actionable failure. An order can sit in PENDING for minutes (typical), hours (slow day), or, rarely, longer — that's the system working, not stuck. See failure_code semantics below.

Request

curl -X POST "https://api.octopuscards.io/api/v1/topups/orders" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": 4218,
    "amount": 4.99,
    "input_data": {
      "player_id": "5123456789",
      "server": "asia"
    },
    "client_reference": "CAMPAIGN_DEC_24"
  }'

Request Parameters

KeyTypeRequiredDescription
product_idintegerYesProduct to recharge. Must be > 0. Use /topups/lookup for mobile, or render the catalogue for gaming.
amountnumberYesOrder amount in the product's currency. Must be > 0 and match an available denomination/range.
input_dataobjectConditionalMap of field_name → value. Required keys are defined by the product's input_fields (see Products). Extra keys are rejected; missing required keys return a validation error.
client_referencestringNoYour dedup + lookup key (printable ASCII, max 255 characters). Unique per client — a duplicate (client_id, client_reference) returns 400 "Duplicate client_reference". Optional, but required for retry-safe order creation. See Idempotency.
wallet_idintegerNoOptional. The wallet to debit. When supplied and its currency differs from the variant's, Octopus Cards looks up an admin-managed forex rate and bills the wallet in its own currency. If no rate exists for the pair, the request rejects with 400 "Exchange rate not available for the wallet currency". Omit to use the variant-currency wallet, falling back to your default-currency wallet.
categorystringNoConstrain variant selection (Airtime, Data, Bundle).
redeem_voucher_codestringNoFund this order by redeeming a previously-delivered voucher code instead of debiting the wallet. The voucher must be in (DELIVERED, COMPLETED) and unredeemed.

Input field validation is strict. Send only the keys returned by /topups/products/:id under input_fields. Unknown keys are rejected — fulfilment paths sometimes fold them into request signing and an unrecognised key would surface later as a confusing "Invalid Signature" error.

Response (typical — order accepted, awaiting fulfilment)

200 OK

{
  "id": 9123456,
  "client_reference": "CAMPAIGN_DEC_24",
  "product_id": 4218,
  "product_name": "PUBG Mobile UC",
  "variant_name": "325 UC",
  "country_code": "GLO",
  "amount": 4.7405,
  "currency": "USD",
  "status": "PENDING",
  "status_text": "Order submitted for fulfilment",
  "input_data": {
    "player_id": "5123456789",
    "server": "asia"
  },
  "created_at": "2026-05-19T14:30:00Z"
}

Response (synchronous fulfilment)

A handful of products fulfil inside the request. In that case the order is already terminal in the response:

{
  "id": 9123457,
  "product_id": 4218,
  "product_name": "PUBG Mobile UC",
  "variant_name": "325 UC",
  "country_code": "GLO",
  "amount": 4.7405,
  "currency": "USD",
  "status": "DELIVERED",
  "status_text": "Top-up delivered",
  "transaction_id": 7401123,
  "input_data": { "player_id": "5123456789", "server": "asia" },
  "created_at": "2026-05-19T14:30:00Z",
  "completed_at": "2026-05-19T14:30:02Z"
}

Response (rejected with user-fixable failure)

When the order is rejected synchronously and the user can correct their input (e.g. wrong player ID), the response carries terminal failure details:

{
  "id": 9123458,
  "product_id": 4218,
  "product_name": "PUBG Mobile UC",
  "variant_name": "325 UC",
  "country_code": "GLO",
  "amount": 4.7405,
  "currency": "USD",
  "status": "FAILED",
  "status_text": "Invalid recipient",
  "failure_code": "INVALID_RECIPIENT",
  "failure_reason": "The player ID you entered could not be found. Please double-check and try again.",
  "is_user_fixable": true,
  "input_data": { "player_id": "5123456789", "server": "asia" },
  "created_at": "2026-05-19T14:30:00Z",
  "completed_at": "2026-05-19T14:30:02Z"
}

Surface failure_reason to the customer when is_user_fixable is true. The wallet is automatically refunded for failed orders — no client action needed.

Enumerations

status:

ValueMeaning
PENDINGOrder accepted; fulfilment in progress. This is the normal not-yet-terminal state and includes any automatic retries Octopus Cards is running on transient conditions. Poll the order endpoint or wait for the notification email.
DELIVEREDTop-up successfully completed. Terminal.
FAILEDOrder has been concluded as not deliverable — almost always because the customer's input or chosen plan was clearly invalid (is_user_fixable: true). The wallet has been refunded. Terminal. Inspect failure_code / failure_reason / is_user_fixable.
CANCELLEDOrder was cancelled before fulfilment. Wallet refunded. Terminal.
RECHARGEDLegacy/internal status. Not normally returned to clients.

failure_code (only present when status == FAILED):

The enum is grouped by the part of the system that originated the failure.

Recipient-level (the player ID / mobile number / account)

CodeMeaningis_user_fixable
INVALID_RECIPIENTPlayer ID / mobile number / account ID is malformed, or the operator does not recognise it.true
RECIPIENT_BARREDOperator has blocked this recipient from receiving top-ups.false
RECIPIENT_INELIGIBLERecipient exists but cannot receive this specific plan (wrong network tier, wrong region, plan restrictions).true (pick a different plan)
RECIPIENT_LIMIT_EXCEEDEDPer-recipient amount or count cap hit for the period.false (try later)

Operator-level (the mobile operator or game platform)

CodeMeaningis_user_fixable
OPERATOR_UNAVAILABLEOperator's prepaid platform is temporarily down.false (try later)
OPERATOR_LIMIT_EXCEEDEDOperator-wide amount or count cap hit.false (try later)

Client-level (your account)

CodeMeaningis_user_fixable
CLIENT_LIMIT_EXCEEDEDYour client account hit a transaction count or value cap.false (contact your account manager)

Product / catalogue

CodeMeaningis_user_fixable
PRODUCT_UNAVAILABLESelected plan is not currently for sale (delisted, region restricted, not enabled for client).true (pick a different plan)
PRODUCT_OUT_OF_STOCKProduct is configured but inventory is depleted.true (pick another, or try later)
AMOUNT_OUT_OF_RANGEAmount is not in the variant's allowed denominations or range.true

Catch-all

CodeMeaningis_user_fixable
UNKNOWNRare. Octopus Cards exhausted its automatic recovery without classifying the failure. Wallet has been refunded. If you intend to retry, place a fresh order; if it persists, contact support.false

Response Fields

KeyTypeDescription
idintegerServer-issued order ID. Pass to /topups/orders/:id.
client_referencestringEcho of the value you supplied on create (omitted if you didn't supply one).
product_idintegerThe product the order was placed against.
product_namestringDisplay name of the product (e.g. "PUBG Mobile UC", "Airtel India").
variant_namestringDisplay name of the variant Octopus Cards matched (e.g. "325 UC"). Internal variant routing IDs are not surfaced.
country_codestringISO 3166-1 alpha-3 country code of the product (e.g. IND, GLO).
amountnumberPer-unit charge debited from your wallet (after client discount). Note: this is the charged amount, not the face value.
currencystringWallet/product currency.
statusstringSee enumeration above.
status_textstringHuman-readable status detail.
failure_codestringStable machine code when status == FAILED. See the Failure Codes enum for the canonical list. Use it for routing and metrics; make customer-facing copy decisions on is_user_fixable and failure_reason.
failure_reasonstringUser-facing failure message when status == FAILED. Safe to render directly.
is_user_fixablebooleanWhen true, the customer can correct their input and retry. Skip automatic retries in this case.
input_dataobjectEchoed from the request (after server-side normalisation, e.g. phone numbers re-formatted to E.164).
transaction_idintegerWallet transaction ID. Present once the wallet has been debited.
created_atstringRFC 3339 timestamp.
completed_atstringRFC 3339 timestamp. Present only when status is terminal.

What Happens Step by Step

  1. Validate request fields and the product's input field schema.
  2. Pick variant — find the best variant for (product_id, amount, client_id, category).
  3. Calculate charges — apply your client discount.
  4. Wallet check — find a wallet in the variant's currency; verify the balance covers the charge.
  5. Debit wallet — atomically, in the same transaction as the recharge insert.
  6. Submit for fulfilment.
  7. Respond with either a PENDING order (typical) or a terminal status when fulfilment completed inside the request.
  8. Background — the retry cron polls PENDING orders, applies fulfilment results, and triggers email notifications on terminal transitions.

Errors

400 Bad Request — required fields missing or invalid.

{
  "error": {
    "name": "ValidationException",
    "code": "VALIDATION_FAILURE",
    "message": "Product ID is required"
  }
}

Other messages: "Amount is required", "Invalid request body".

On this page