Python SDK (server)
invonetwork is the official first-party server-side SDK for integrating Invo from a Python backend (Django, Flask, FastAPI, or plain Python). It's the Python counterpart to the Web / TypeScript SDK: same endpoints, field names, and webhook signing scheme, so both stay in lockstep.
Server-side only: holds your game secret
This SDK authenticates with your game secret and is meant for your backend. For the browser half (passkey enroll/approve, self-claim, balances/destinations for a logged-in player), pair it with the Web SDK. The game secret never reaches the browser.
This applies to a game or a platform alike: the SDK calls the tenant a game and names its credential game_secret, but a platform tenant uses the same field and the same flow.
Not in this SDK (by design): the player-token WebAuthn ceremonies: passkey enroll, approve/step-up, confirm-receipt/claim, the enrollment OTP grant, and device link. Those run in the browser (navigator.credentials) via the Web SDK'sInvoClient, or you relay them through your backend. Everything game-secret (mint, initiate, claim, status, guardian, phone-share, checkout, purchase, item purchase, balances, inbound-pending, destinations, linked-identities, webhook verification) is here.
Currency & Item Purchase
Hosted checkout, direct rail, and spend-currency item purchases
Sends & Transfers
Initiate on the server; the browser approves by passkey
Balances & Reads
Player balances, inbound-pending, linked identities, order details
Webhook Verification
Constant-time HMAC verification with typed events
Install
Requires Python 3.9+. The exact command differs slightly by OS. Use the one for your platform:
macOS / Linux
python3 -m pip install invonetworkWindows
py -m pip install invonetworkRecommended: inside a virtual environment
python3 -m venv .venv
source .venv/bin/activate
pip install invonetworkpy -m venv .venv
.venv\Scripts\Activate.ps1
pip install invonetworkFull stack? Install the server SDK here and the browser SDK on your web front-end:
# Python backend (this package)
pip install invonetwork
# Web / browser front-end (Node ≥ 18)
npm install @invonetwork/web-sdkQuickstart
Construct one InvoServer with your game secret and reuse it. Get your secret and base URLs from the Invo console (https://console.invo.network for production, https://console.invo.network/test for testing).
from invonetwork import InvoServer
invo = InvoServer(
game_secret="ivsdk_test_...", # server-side ONLY
base_url="https://sandbox.invo.network/sandbox", # prod: https://invo.network
)
# Mint a short-lived, game-scoped token for the browser SDK to use
token = invo.mint_player_token(player_email="player@example.com", player_phone="+15555550111")
# Spend game currency on an in-game item (balance debit; grant off the item.purchased webhook)
order = invo.purchase_item(
client_request_id="req-123", # idempotency key
player_email="player@example.com",
player_name="Ada",
item_id="sword_001",
item_name="Flaming Sword",
item_quantity=1,
unit_price="150.00",
total_price="150.00",
)
# Read a player's balances
balance = invo.get_player_balance(player_email="player@example.com")Key prefixes (3.30.0)
New game secrets name their environment: ivsdk_test_... is test mode (sandbox) and ivsdk_live_... is production, including a key you get by rotating. Leave base_url out and the SDK picks the server from the prefix, so going live is swapping the key. Keys you already have (ivsdk_... with no prefix) keep working exactly as before. environment_from_key_prefix(key) returns "production", "sandbox" or None (an older unprefixed key).
import os
from invonetwork import InvoServer, KeyEnvironmentMismatchError, environment_from_key_prefix
secret = os.environ["INVO_GAME_SECRET"]
environment_from_key_prefix(secret) # "production" | "sandbox" | None
invo = InvoServer(secret) # test key -> sandbox, live key -> productionIf a test or live key is given a base_url on the other INVO server, InvoServer(...) raises KeyEnvironmentMismatchError (a ValueError) before any request, with a message naming the server to use. A base_url pointing at your own proxy is never checked.
If a request still reaches the wrong environment, INVO answers 401 KEY_FOR_OTHER_ENVIRONMENT; check it with err.is_key_for_other_environment. It is not retried: send test keys to the test server and live keys to the live server.
Verify webhooks
Pass the raw request bytes (never a re-serialized object) and your signing secret. verify_webhook checks the HMAC in constant time, rejects stale timestamps, supports secret rotation, and returns a typed event.
from flask import Flask, request
from invonetwork import verify_webhook, InvoError
app = Flask(__name__)
@app.post("/invo/webhooks")
def invo_webhook():
try:
event = verify_webhook(
request.get_data(), # RAW bytes
request.headers.get("X-Invo-Signature"),
"whsec_...", # your signing secret (or a list during rotation)
)
except InvoError:
return "", 400 # bad signature / stale / malformed
if event.event_type == "purchase.completed":
grant_currency(event.data) # idempotent on event.idempotency_key
return "", 200from fastapi import FastAPI, Request, Response
from invonetwork import verify_webhook, InvoError
app = FastAPI()
@app.post("/invo/webhooks")
async def invo_webhook(request: Request):
raw = await request.body()
try:
event = verify_webhook(raw, request.headers.get("X-Invo-Signature"), "whsec_...")
except InvoError:
return Response(status_code=400)
# handle event.event_type ...
return Response(status_code=200)Chargeback dispute fee fields: from 2026-10-21 the closing purchase.disputed on a card dispute, on either tier, carries dispute_fee_usd (string or None) and dispute_fee_charged_to_partner (bool or None). They are typed, optional fields from invonetwork 3.21.0; on earlier versions read them from the raw payload.
Inbound "to collect" (incoming from other titles)
get_inbound_pending lists a player's incoming, unclaimed sends/transfers, including value sent from other titles to a player on your platform. It's the source of truth behind a "you have X to collect" badge, and pairs with the transfer.claim_pending webhook.
pending = server.get_inbound_pending(player_email="p@example.com") # or player_phone=...
for row in pending.inbound_pending:
# row.source_game = where it came from (another game). Match row.to_phone to the player;
# row.to_identity_id is None when the phone maps to >1 of your players (don't require it).
print(row.transaction_id, row.net_amount, row.to_phone, row.source_game)
# Lists only pending/unclaimed; a claimed transfer drops off.This is the server/platform view (game-secret). The browser player-token equivalent is the JS SDK's client.getPendingCollect(), where incoming rows come back as kind="receiving_confirm". Render those, not just player-initiated ones.
Errors
Every failure raises InvoError with .code, .status,.message, .body, and .request_id, plus classifier helpers so you don't string-match:
from invonetwork import InvoError
try:
invo.purchase_item(...)
except InvoError as e:
if e.is_insufficient_balance:
... # not enough currency
elif e.is_duplicate_request:
... # replayed client_request_id
elif e.retry_after is not None:
... # throttled; back off e.retry_after seconds
else:
raiseWhat's included
- Tokens & flows:
mint_player_token,initiate_send,initiate_transfer,create_checkout,purchase_currency,confirm_payment,purchase_item - Steam (3.5.0+):
steam_packs,steam_init_purchase(overlay or browser checkout viausersession="web"+player_ip),steam_finalize_purchase, plus poller-taxonomy error properties (is_steam_authorization_pending/is_steam_authorization_dead) and pre-charge refusal helpers (is_partner_credit_unavailableetc.). Packs, never prices;metadataechoes on the webhook. See Steam Payments. - Completion & recovery (v0.2.0+):
claim_transfer/claim_currency,get_transfer_status/get_send_status,get_guardian_approval_status. The legacyverify_sms_transfer/verify_sms_sendmethods still exist for older integrations and are deprecated; do not build on them. - Phone-share & destinations (v0.3.0+):
phone_share_initiate/phone_share_approve/phone_share_status(resolve a409 PHONE_SHARE_APPROVAL_REQUIRED), and server-sideget_destinations(source_game_id=..., direction=...) - Reads:
get_order_details,get_item_purchase_history(+iterate_item_purchase_history),get_player_activity(+iterate_player_activity, from 3.20.0),platform_commerce.get_purchase_history(+iterate_purchase_history, from 3.20.0),get_item_order_details,get_player_balance,get_inbound_pending,get_linked_identities - Webhooks:
verify_webhookwith a typedWebhookEvent - Resilience: automatic retries (network / 429 / 5xx, idempotent-only, honoring
retry_after), timeouts, and observability hooks - Types: full type hints, ships
py.typed(mypy --strictclean)