Write-Response Contract: new_balance
Every Invo endpoint that credits or debits a player's balance returns the post-write balance at one canonical location: response.new_balance. Read from there.
Why this exists
Wallet UIs need to display the player's new balance immediately after every write. Without a single agreed location, partners had to special-case each endpoint (purchase_details.new_balance, send_details.new_balance, transfer_details.new_balance, etc.). One missed branch = a stranded wallet showing a stale number. The canonical location eliminates that class of bug: same field name, same place, every endpoint.
Where to read it
| Endpoint | Side affected | new_balance reflects |
|---|---|---|
| /api/currency-purchases/purchase-currency | Player credit | Player's balance after the purchase credit |
| /api/item-purchases/purchase-item | Player debit | Player's balance after the item debit |
| /api/transfers/initiate-transfer | Source player reservation | Source's available_balance post-reservation |
| /api/transfers/verify-sms (deprecated) | no funds movement | Source's available_balance (re-confirmation) |
| /api/transfers/claim-transfer | Target player credit | Target's available_balance post-credit |
| /api/currency-sends/initiate-send | Sender reservation | Sender's available_balance post-reservation |
| /api/currency-sends/verify-sms (deprecated) | no funds movement | Sender's available_balance (re-confirmation) |
| /api/currency-sends/claim-currency | Receiver credit | Receiver's available_balance post-credit |
Field shape
Type
Decimal-as-string. Always parse defensively:
// JS / TS
const newBalance = response.new_balance != null
? new Decimal(response.new_balance)
: null;
# Python
from decimal import Decimal
new_balance = (
Decimal(response["new_balance"])
if response.get("new_balance") is not None
else None
)Units
The destination tenant's branded currency. The currency_name field in the same response tells you which one.
Not USD. If you display in USD on your wallet UI, convert using the destination tenant's exchange rate.
When new_balance is null
Whenever nothing has been credited yet, so there is no new balance to report. The field is explicitly null (not missing) so your code can branch on status:
status: "requires_action"(3D Secure / SCA) onPOST /api/currency-purchases/purchase-currencyand when completing a hosted checkout card payment: the buyer must authenticate first.status: "pending_payment": the player is sent topayment_urlto pay (a storefront or regional payment route); the balance is credited when that payment completes.- A Steam purchase init: the response carries
steam_checkout_urland the coins are credited after finalize. - A
confirm-paymentretry for an order that was already processed (already_processed: true): read the balance instead.
if (response.status === 'success' && response.new_balance != null) {
// safe to update wallet UI
updateWalletBalance(new Decimal(response.new_balance));
} else if (response.status === 'requires_action') {
// 3DS challenge: pass response.client_secret to your card processor's
// SDK. INVO's auth challenge uses the automatic-confirmation flow, so
// call the SDK's "confirm payment" variant (which confirms AND runs the
// challenge), not a manual-confirm-only handler.
await confirmWithYourCardElement(response.client_secret);
}On a success path that credited or debited the balance, new_balance is non-null. The field is always present in these responses, never undefined, so treat null as "not yet, read it later".
Belt-and-suspenders: trust GET /player-balances
Recommended pattern
Use the response's new_balance for fast wallet updates, but treat GET /api/player-balances/player/by-email/<email> as the source of truth. After any successful write, kick off a background read to reconfirm. Mid-deploy, network hiccup, or a stale write-response shape will never strand a user as long as you re-read.
async function purchaseAndUpdate(usdAmount) {
const response = await api.purchaseCurrency({ usd_amount: usdAmount });
if (response.status === 'success' && response.new_balance != null) {
// fast path: optimistically update from write response
setWalletBalance(new Decimal(response.new_balance));
}
// belt: always reconcile against authoritative read endpoint
const fresh = await api.getPlayerBalance({ player_email });
setWalletBalance(new Decimal(fresh.available_balance));
}Backwards-compatible aliases
Partners that integrated before the canonical location was nailed down can still read from the legacy nested fields. Both are populated to the same value; new integrations should use the top-level field.
| Endpoint | Legacy location |
|---|---|
| /currency-purchases/purchase-currency | purchase_details.new_balance |
| /item-purchases/purchase-item | balance_info.new_balance |
| /transfers/claim-transfer | transfer_details.new_balance |
| /currency-sends/claim-currency | send_details.new_balance |