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

EndpointSide affectednew_balance reflects
/api/currency-purchases/purchase-currencyPlayer creditPlayer's balance after the purchase credit
/api/item-purchases/purchase-itemPlayer debitPlayer's balance after the item debit
/api/transfers/initiate-transferSource player reservationSource's available_balance post-reservation
/api/transfers/verify-sms (deprecated)no funds movementSource's available_balance (re-confirmation)
/api/transfers/claim-transferTarget player creditTarget's available_balance post-credit
/api/currency-sends/initiate-sendSender reservationSender's available_balance post-reservation
/api/currency-sends/verify-sms (deprecated)no funds movementSender's available_balance (re-confirmation)
/api/currency-sends/claim-currencyReceiver creditReceiver'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) on POST /api/currency-purchases/purchase-currency and when completing a hosted checkout card payment: the buyer must authenticate first.
  • status: "pending_payment": the player is sent to payment_url to pay (a storefront or regional payment route); the balance is credited when that payment completes.
  • A Steam purchase init: the response carries steam_checkout_url and the coins are credited after finalize.
  • A confirm-payment retry 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.

EndpointLegacy location
/currency-purchases/purchase-currencypurchase_details.new_balance
/item-purchases/purchase-itembalance_info.new_balance
/transfers/claim-transfertransfer_details.new_balance
/currency-sends/claim-currencysend_details.new_balance