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 PENDING → DELIVERED 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
| Key | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | Product to recharge. Must be > 0. Use /topups/lookup for mobile, or render the catalogue for gaming. |
amount | number | Yes | Order amount in the product's currency. Must be > 0 and match an available denomination/range. |
input_data | object | Conditional | Map 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_reference | string | No | Your 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_id | integer | No | Optional. 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. |
category | string | No | Constrain variant selection (Airtime, Data, Bundle). |
redeem_voucher_code | string | No | Fund 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:
| Value | Meaning |
|---|---|
PENDING | Order 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. |
DELIVERED | Top-up successfully completed. Terminal. |
FAILED | Order 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. |
CANCELLED | Order was cancelled before fulfilment. Wallet refunded. Terminal. |
RECHARGED | Legacy/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)
| Code | Meaning | is_user_fixable |
|---|---|---|
INVALID_RECIPIENT | Player ID / mobile number / account ID is malformed, or the operator does not recognise it. | true |
RECIPIENT_BARRED | Operator has blocked this recipient from receiving top-ups. | false |
RECIPIENT_INELIGIBLE | Recipient exists but cannot receive this specific plan (wrong network tier, wrong region, plan restrictions). | true (pick a different plan) |
RECIPIENT_LIMIT_EXCEEDED | Per-recipient amount or count cap hit for the period. | false (try later) |
Operator-level (the mobile operator or game platform)
| Code | Meaning | is_user_fixable |
|---|---|---|
OPERATOR_UNAVAILABLE | Operator's prepaid platform is temporarily down. | false (try later) |
OPERATOR_LIMIT_EXCEEDED | Operator-wide amount or count cap hit. | false (try later) |
Client-level (your account)
| Code | Meaning | is_user_fixable |
|---|---|---|
CLIENT_LIMIT_EXCEEDED | Your client account hit a transaction count or value cap. | false (contact your account manager) |
Product / catalogue
| Code | Meaning | is_user_fixable |
|---|---|---|
PRODUCT_UNAVAILABLE | Selected plan is not currently for sale (delisted, region restricted, not enabled for client). | true (pick a different plan) |
PRODUCT_OUT_OF_STOCK | Product is configured but inventory is depleted. | true (pick another, or try later) |
AMOUNT_OUT_OF_RANGE | Amount is not in the variant's allowed denominations or range. | true |
Catch-all
| Code | Meaning | is_user_fixable |
|---|---|---|
UNKNOWN | Rare. 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
| Key | Type | Description |
|---|---|---|
id | integer | Server-issued order ID. Pass to /topups/orders/:id. |
client_reference | string | Echo of the value you supplied on create (omitted if you didn't supply one). |
product_id | integer | The product the order was placed against. |
product_name | string | Display name of the product (e.g. "PUBG Mobile UC", "Airtel India"). |
variant_name | string | Display name of the variant Octopus Cards matched (e.g. "325 UC"). Internal variant routing IDs are not surfaced. |
country_code | string | ISO 3166-1 alpha-3 country code of the product (e.g. IND, GLO). |
amount | number | Per-unit charge debited from your wallet (after client discount). Note: this is the charged amount, not the face value. |
currency | string | Wallet/product currency. |
status | string | See enumeration above. |
status_text | string | Human-readable status detail. |
failure_code | string | Stable 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_reason | string | User-facing failure message when status == FAILED. Safe to render directly. |
is_user_fixable | boolean | When true, the customer can correct their input and retry. Skip automatic retries in this case. |
input_data | object | Echoed from the request (after server-side normalisation, e.g. phone numbers re-formatted to E.164). |
transaction_id | integer | Wallet transaction ID. Present once the wallet has been debited. |
created_at | string | RFC 3339 timestamp. |
completed_at | string | RFC 3339 timestamp. Present only when status is terminal. |
What Happens Step by Step
- Validate request fields and the product's input field schema.
- Pick variant — find the best variant for
(product_id, amount, client_id, category). - Calculate charges — apply your client discount.
- Wallet check — find a wallet in the variant's currency; verify the balance covers the charge.
- Debit wallet — atomically, in the same transaction as the recharge insert.
- Submit for fulfilment.
- Respond with either a
PENDINGorder (typical) or a terminal status when fulfilment completed inside the request. - Background — the retry cron polls
PENDINGorders, 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".