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
| Key | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | Product to price. Must be > 0. |
amount | number | Yes | Order 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_id | integer | No | Wallet 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). |
category | string | No | Optional 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
| Key | Type | Description |
|---|---|---|
non_discounted_total | number | Gross amount before discount, in source_currency. |
discount_amount | number | Absolute discount applied, in source_currency. |
total_amount | number | Post-discount net in source_currency (non_discounted_total − discount_amount). |
discount | number | Client discount percentage (e.g. 5.0 means 5%). Configured per client/product on client_topup_products; defaults to 0 when no mapping exists. |
net_amount | number, omitempty | Present only when source ≠ destination currency. total_amount × forex_rate — the post-FX amount before the conversion fee. |
handling_fee_amount | number, omitempty | Present only when source ≠ destination currency. net_amount × conversion_fee%. Currently always 0 (conversion fee is a placeholder). |
total_payable | number | Final 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_currency | string | ISO 4217 currency of the variant. |
charges_details.destination_currency | string | ISO 4217 currency you'll be billed in. Equals source_currency when no FX. |
charges_details.forex_rate | number, omitempty | Admin-managed rate used for the conversion (source → destination). Present only when source ≠ destination. |
charges_details.conversion_fee | number, omitempty | Conversion-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 Request — product_id or amount missing or invalid.
{
"error": {
"name": "ValidationException",
"code": "VALIDATION_FAILURE",
"message": "Product ID is required"
}
}Other validation messages: "Amount is required".