API ReferenceTopups

Charges

Calculate the exact payable amount for a top-up order before placing it

POST /api/v1/topups/charges

Resolves the best variant for the given product_id + amount and returns the full price breakdown — same shape as the voucher charges endpoint, so a single client implementation works for both.

Recharges are always one-at-a-time. There is no quantity in the request and no max_quantity in the response — if a customer wants two top-ups, place two orders.

Cross-currency wallets are supported. Pass wallet_id if you want a specific wallet to be billed; when its currency differs from the variant's, Octopus Cards looks up an admin-managed forex rate and the response carries net_amount / handling_fee_amount / forex_rate / conversion_fee in the wallet currency. If wallet_id is omitted, Octopus Cards picks a wallet in the variant currency, falling back to your default-currency wallet.

Request

curl -X POST "https://api.octopuscards.io/api/v1/topups/charges" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": 4218,
    "amount": 4.99
  }'

Request Parameters

KeyTypeRequiredDescription
product_idintegerYesProduct to price. Must be > 0.
amountnumberYesOrder amount in the product's currency. Must be > 0. For fixed-denomination products, must match one of the variant's fixed_amounts. For ranges, must fall within [min_amount, max_amount].
wallet_idintegerNoWallet to bill. When supplied and its currency differs from the variant's, the response is FX-converted into the wallet currency. Omit to use the variant-currency wallet (or your default-currency wallet as fallback).
categorystringNoOptional sub-category filter (Airtime, Data, Bundle). Use when a single product carries multiple variant categories and you want to constrain the variant selection.

There is no quantity field — recharges are always 1 per order.

Response

Same-currency wallet (no FX — wallet_id omitted or already in variant currency):

{
  "non_discounted_total": 4.99,
  "discount_amount": 0.2495,
  "total_amount": 4.7405,
  "discount": 5.0,
  "total_payable": 4.7405,
  "charges_details": {
    "source_currency": "USD",
    "destination_currency": "USD"
  }
}

Cross-currency wallet (FX applied — e.g. USD variant billed to an INR wallet):

{
  "non_discounted_total": 4.99,
  "discount_amount": 0.2495,
  "total_amount": 4.7405,
  "discount": 5.0,
  "net_amount": 395.87,
  "handling_fee_amount": 0.0,
  "total_payable": 395.87,
  "charges_details": {
    "source_currency": "USD",
    "destination_currency": "INR",
    "forex_rate": 83.51,
    "conversion_fee": 0.0
  }
}

Response Fields

KeyTypeDescription
non_discounted_totalnumberGross amount before discount, in source_currency.
discount_amountnumberAbsolute discount applied, in source_currency.
total_amountnumberPost-discount net in source_currency (non_discounted_total − discount_amount).
discountnumberClient discount percentage (e.g. 5.0 means 5%). Configured per client/product on client_topup_products; defaults to 0 when no mapping exists.
net_amountnumber, omitemptyPresent only when source ≠ destination currency. total_amount × forex_rate — the post-FX amount before the conversion fee.
handling_fee_amountnumber, omitemptyPresent only when source ≠ destination currency. net_amount × conversion_fee%. Currently always 0 (conversion fee is a placeholder).
total_payablenumberFinal amount you'll be charged in destination_currency. Equals total_amount when no FX, or net_amount + handling_fee_amount when cross-currency.
charges_details.source_currencystringISO 4217 currency of the variant.
charges_details.destination_currencystringISO 4217 currency you'll be billed in. Equals source_currency when no FX.
charges_details.forex_ratenumber, omitemptyAdmin-managed rate used for the conversion (source → destination). Present only when source ≠ destination.
charges_details.conversion_feenumber, omitemptyConversion-fee percentage applied on top of FX. Present only when source ≠ destination. Currently 0.

No max_quantity — top-up orders are 1-at-a-time. Variant-routing internals are intentionally not surfaced.

Cross-currency FX requires an admin-managed rate in the forex_values table for the source → destination pair. If none exists, the request rejects with 400 "Exchange rate not available for the wallet currency".

Errors

400 Bad Requestproduct_id or amount missing or invalid.

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

Other validation messages: "Amount is required".

On this page