FAQ / SDKs and Checkout

SDKs and Checkout: Questions and Answers

Hosted checkout, the Unity and Unreal plugins, and the Web and Python SDKs. 83 questions, grouped by the documentation page each one is about. Where a response or webhook carries a figure, read it there rather than recomputing it.

Game Developer Integration (hosted checkout)

Read the Game Developer Integration (hosted checkout) page

Why is the recommended pattern a signed checkout session instead of putting the SDK key in a URL?

Your SDK key authorizes every API call as your title, and anything in a URL ends up in browser history, server logs, referrer headers and crash reports. A checkout session is minted by your backend with X-Game-Secret-Key, and the returned checkout_url carries only a short-lived, single-use signed token bound to that one player and amount, so intercepting it gives an attacker nothing reusable.

How long is a checkout_url valid, and is it single use?

15 minutes (expires_in_seconds is 900, and expires_at is returned with the session). It is single use: once the purchase completes, opening it again returns 409 SESSION_ALREADY_CONSUMED, and after the TTL it returns 410 SESSION_EXPIRED.

What does POST /api/checkout/sessions accept?

A JSON body with player_email (required), usd_amount (required, a decimal string, quantized to cents), and optional rail, success_url, cancel_url and metadata. success_url/cancel_url may be https, http or an app deep-link scheme (script schemes such as javascript: are refused), and metadata must be a JSON object under 8 KB. It returns 201 with session_id, checkout_url, session_token, expires_at and expires_in_seconds; a bad field returns 400 INVALID_INPUT and a bad key returns 401 INVALID_GAME_SECRET.

What are the rail options, and what is the default?

rail accepts platform (INVO's card checkout), game (the regional game-store rail) and steam (the game-platform rail). When you omit it, the rail is chosen from the channel your SDK key belongs to, then your game's configured default payment rail, and only then platform. If the game rail is not ready for your title, the session is minted on the card rail instead and the response says so.

Where do I read the current card maximum instead of hard-coding it?

Read max_amount (and min_amount) from GET /api/checkout/validate-game; it is the same platform setting that session creation enforces, so the two cannot drift. A usd_amount of 0 or above the maximum is refused with 400 INVALID_INPUT, and a card amount below the card minimum is refused with BELOW_CARD_MINIMUM once that minimum is enforced.

If I list a $10.00 pack, what does the player pay, how many coins are credited, and what fee am I charged?

The player pays $10.00 (plus consumption tax only where INVO collects it on Merchant of Record) and is credited 100 coins at the standard 10 coins = $1 rate, or your per-game rate if you have one. On the Open tier INVO's card fee of 3.5% + $0.15 ($0.50 on this pack) is charged to you, not the player, and a non-US card adds 1.5%. On Merchant of Record the fee is 5.0% + $0.15 ($0.65 on this pack), also plus 1.5% for a non-US card; the webhook reports what you were charged in partner_fee_usd and international_card_fee_usd.

Should I open checkout in a full-page redirect, a WebView, or an iframe, and can any https site embed it?

A full-page redirect or WebView works everywhere with no setup and returns the player to your success_url; it is the recommended default. An iframe also works out of the box: the checkout page is served with frame-ancestors https:, so any https site may embed it (give the iframe allow="payment"). When embedded, the page does not navigate your top window; listen for the INVO_CHECKOUT_COMPLETE postMessage instead.

Can I restrict framing to my own origins?

Not today. A per-partner allow-list of framing origins can be stored on your account, but the checkout page currently served does not apply it, so any https site can still embed your checkout. The session binding is what protects you: a framing page cannot change the amount or the recipient, and card entry happens inside the card processor's own secure frame.

What postMessage does the page send on success, and why is it only a UX hint?

On success the page posts { type: "INVO_CHECKOUT_COMPLETE", data: { status: "completed", new_balance, currency_name, transaction_id } } to its parent window. It is not signed and is sent with target origin *, so anything could imitate it; use it only to refresh your UI optimistically and grant value from the purchase.completed webhook or a server-side balance read.

Is there a postMessage for cancellation?

There is no cancellation event. When the page is embedded and the player presses "Go back", it posts { type: "invo_checkout", action: "redirect", url: <your cancel_url> } so your parent page can navigate (only if you passed a cancel_url); otherwise handle cancellation with your own close control on the overlay, and the unused session simply expires.

Which origin should my postMessage listener check in production and in sandbox?

Check event.origin === "https://invo.network" in production and "https://sandbox.invo.network" in sandbox. That is the checkout page's own origin, not the developer console host, and your check must match the environment the session was minted in.

What PCI scope does hosted checkout keep me in?

SAQ-A, the lightest tier. Card data is entered only on the INVO-hosted page inside the card processor's own frame and never touches your servers or your page.

Can the hosted checkout page show our logo instead of INVO's?

Yes. Upload your game's logo in the developer console under Game settings, Checkout branding, and it replaces the INVO logo on the hosted checkout page for that game (coin purchases, Platform Commerce item purchases, the platform store hand-off screen and the game storefront payment screen), on the hosted card page for subscriptions and on the subscription payment confirmation page. INVO's logo is the default when none is set, and a small "Secured by INVO" line always stays at the bottom. The image must be PNG, JPEG or WebP, up to 1 MB; SVG and GIF are not accepted. A transparent, wide PNG of about 4:1 (for example 640 x 160) works best, and it is shown at up to 40 px tall and 200 px wide. It is set in the console only; there is no API or SDK call, and your checkout_url does not change.

Why do the approval and claim pages not show our logo?

Approval, device approval, claim and collect, consent, identity, phone change and guardian pages, and receipts, always look like INVO. On those pages the player confirms something about their identity or money with INVO, and keeping them identical everywhere teaches players what a genuine INVO approval looks like, so a copy dressed up as a game stands out. On a Merchant of Record account INVO is also the seller named on the receipt.

Is our checkout logo shared between sandbox and production?

It is copied once, when a game is copied or linked between sandbox and production, the same way the game icon is. After that the two are separate: changing or removing the logo in one environment does not change the other.

How do I integrate in a Unity WebGL build with a .jslib plugin?

Have your backend mint the session and return checkout_url, then put the browser iframe code in a .jslib (for example Assets/Plugins/InvoCheckout.jslib) that creates the overlay and iframe and listens for INVO_CHECKOUT_COMPLETE. Call it from C# through [DllImport("__Internal")], and have the .jslib call back into your game with SendMessage on success. Treat that callback as a UI refresh only; credit from your webhook handler.

How should a Unity mobile, Unreal, iOS or Android build open checkout, and why must it never call /api/checkout/sessions directly?

The client asks your own backend (or dedicated server) for a session, receives checkout_url over a trusted call, and opens it in a WebView, browser widget or the system browser; on success the page redirects to your success_url. Calling /api/checkout/sessions from the build would require shipping your SDK key inside it, where it can be extracted and used to act as your title. The hosted page does not call native WebView bridges, so on mobile detect completion by the success_url navigation and confirm with your webhook.

What app store billing rules should I review before shipping a WebView checkout?

Apple App Store and Google Play rules generally require their own in-app billing for digital goods sold inside a mobile app, with exceptions that vary by region and app category. Review the current guidelines for each store you ship to before enabling a WebView checkout in a store build.

Which webhook is the authoritative signal that a purchase completed?

purchase.completed. It fires after the player is credited on every rail, even if the WebView was closed or the postMessage never arrived, and it carries transaction_id, order_id and the player's new_balance. Dedupe on X-Invo-Idempotency-Key.

What fields are in purchase.completed?

data carries transaction_id, order_id, player_email, identity_id, usd_amount, currency_amount, currency_name, new_balance, rail, channel and your metadata, plus charge_total_usd, subtotal_usd, tax_usd, tax_jurisdiction, tax_rate_pct and tax_treatment. Card coin purchases add partner_fee_usd and international_card_fee_usd, and Merchant of Record sales add receipt_number and receipt_url. Amounts are decimal strings; tax fields are 0.00 or null where no tax is collected.

Is my create-session metadata echoed back unchanged on all rails?

Yes, the metadata object you pass is echoed in purchase.completed on every rail. The one exception is keys beginning with _invo_, which are reserved for INVO and stripped when the session is created.

What do 409 SESSION_ALREADY_CONSUMED and 410 SESSION_EXPIRED mean?

409 SESSION_ALREADY_CONSUMED means that session URL was already used for a purchase; 410 SESSION_EXPIRED means its 15-minute lifetime has passed. In both cases mint a new session; never reuse a URL.

Why does my webhook signature check fail when I use a JSON body parser?

The HMAC is computed over "<t>." + the raw request bytes. A JSON parser re-serializes the body (whitespace, key order, number formatting), so the bytes you hash no longer match. Use a raw body parser such as express.raw({ type: "application/json" }), verify, then parse.

What is the webhook retry schedule?

After a failed delivery (non-2xx or timeout) INVO retries after about 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours, with small jitter. With the default of 6 attempts the delivery is marked dead after the sixth failure; the 24-hour step is used only if you raise max_attempts (1 to 12) in your webhook retry policy. Dead deliveries can be redriven from the console.

Unity SDK

Read the Unity SDK page

Which Unity versions and dependencies are required?

Unity 6 (6000.0 or later), .NET Standard 2.1, Newtonsoft.Json (com.unity.nuget.newtonsoft-json) and TextMeshPro, plus an INVO developer account.

How do I install the SDK from GitHub, and which files go into Assets?

Clone https://github.com/Invo-Technologies/Invo-unity-sdk.git and copy Assets/InvoSDK/ and Assets/APIManager.cs into your project's Assets folder. Then install Newtonsoft.Json through Window > Package Manager and run InvoSDK > Setup Wizard.

What does the Setup Wizard configure, and where is InvoSDKConfig.asset stored?

The wizard writes your settings (key for sandbox, game ID, display values, player defaults, environment, gameServerUrl and checkoutSessionEndpoint) into Assets/InvoSDK/Resources/InvoSDKConfig.asset. If a second copy exists at Assets/Resources/InvoSDKConfig.asset, delete it; the wizard detects the duplicate. The asset is gitignored, so each developer creates their own.

What do the config fields (sdkKey, gameId, gameName, playerEmail, playerPhone, gameCurrencyName, useProduction, and so on) do?

sdkKey is your game secret and is used only in sandbox builds; gameId is your console game ID (not secret); gameName, gameCurrencyName, gameIconUrl, gameCurrencyUrl and gameVersion are display values; playerEmail and playerPhone are the default player for testing (the phone is part of the player identity); useProduction switches between sandbox and production. gameServerUrl (required for production) and checkoutSessionEndpoint point at your own server. There is no playerPassword field in the current SDK.

If the SDK key must never ship in a client build, how should I use the Unity SDK safely in production?

Since 3.0.0 the SDK refuses to send the game secret from a production build: set gameServerUrl, and every game-secret call goes to <gameServerUrl>/api/<path>, where your server authenticates the player, checks the path against an allow-list, adds X-Game-Secret-Key and forwards the call unchanged. Your server must take the player's email from its own session (never the request body) and set item prices from its own catalog. Without gameServerUrl, production calls fail with SDK_GAME_SERVER_REQUIRED; approval calls use the player token and go to INVO directly.

How do I get a player balance, purchase an item, and fetch the item catalog with APIManager?

Use APIManager.Instance.GetPlayerBalanceAsync(email, onSuccess, onError), GetGameItemsAsync(onSuccess, onError) and PurchaseItemAsync(clientRequestId, playerEmail, playerName, itemId, itemName, quantity, unitPrice, totalPrice, ...). Prices are decimal, and clientRequestId is the idempotency key: create it once per tap with NewClientRequestId() and reuse it on every retry. A 2xx purchase response can still carry a non-success status, so check it before granting the item.

Which endpoints do the APIManager methods call?

Balance: GET /api/player-balances/player/by-email/{email}; catalog: POST /api/v1/game-items/list; item purchase: POST /api/item-purchases/purchase-item; destinations: POST /api/currency-sends/available-destinations and /api/transfers/available-destinations; sends: /api/currency-sends/initiate-send, /claim-currency, /{id}/status; transfers: /api/transfers/initiate-transfer, /claim-transfer, /{id}/status; player token: POST /api/sdk/player-token. Approvals use /api/sdk/approvals/device/* and /api/sdk/{transfers|send}/{id}/approve|confirm-receipt, and the pending list uses GET /api/sdk/transfers/pending.

Is VerifySmsAsync still supported, and what replaces it?

No. SMS verification is retired: the old methods are now VerifySendSmsAsync, VerifyTransferSmsAsync, ResendSendPinAsync and ResendTransferPinAsync, all marked [Obsolete] and no longer used by the panels. Approve sends and transfers with InvoDeviceApproval.RunAsync(transactionId, flow), which runs the QR or system-browser phone approval and the settle call.

What pre-built UI components are included?

Under Assets/InvoSDK/Scripts/UI/: item purchase (ItemPurchasePanel, ItemPurchaseConfirmPanel, ItemCardView), the transfer wizard (TransferCurrencyPanel, step 3 is the phone approval), the send panel (SenderCurrencyPanel plus the receiver's Collect panel), phone approval views (InvoDeviceApprovalPanelView, InvoDeviceApprovalOverlay, InvoApprovalStep), featured items (InvoSDKFeaturedPanel and its daily/weekly cards) and InvoSDKWindowManager. Treat them as working references, not a finished storefront; the 3.0.0 panels need new fields wired.

What do the editor tools do?

Under the InvoSDK menu: Setup Wizard writes your config, Item Catalog manages items, prices, discounts and categories, Test Purchase runs sandbox purchases (a requires_action status means 3-D Secure is needed and nothing was charged), and Check Player Balance reads a player's balance during development.

How do I fix "Config not found!", "Player email not set", JSON parse errors and 401 or 403 errors?

"Config not found! Please run Setup Wizard." means no InvoSDKConfig.asset exists under a Resources folder; run the wizard. With no player email the current SDK does not error, it logs "No active player email" and leaves balance polling idle until you set one. JSON parse errors usually mean Newtonsoft.Json is missing from the build; a 401 is a missing or wrong key or a sandbox key against production, and a 403 "Game is not active" means the game is not live or testing.

Why isn't my balance updating, and what does balancePollInterval control?

balancePollInterval (default 10 seconds) is how often APIManager re-reads the balance; after failures it backs off by doubling up to maxBalancePollInterval (300 seconds) and honors any Retry-After. Polling is idle when no player email is set, and the label shows available_balance, so funds reserved by a pending send are not shown. In production the read goes through your gameServerUrl, so check that path is on your allow-list.

Unreal Engine SDK

Read the Unreal Engine SDK page

Which engine versions and platforms are supported?

Unreal Engine 4.27+ and 5.x on a Windows 64-bit development environment, with Visual Studio 2019 or later for C++ work.

How do I install and enable the plugin?

Clone https://github.com/Invo-Technologies/invo-unreal-sdk.git and place the InvoSDK folder under your project's Plugins/ directory. Open the editor, go to Edit > Plugins, search for "InvoSDK", enable it and restart.

What fields does InvoSDKConfig need, and which are required?

Required: sdkKey, gameId, gameName, playerEmail and useProduction. Optional: gameIconUrl, gameCurrencyName, gameCurrencyUrl and gameVersion. Keep sdkKey out of shipped client builds; use it only on a dedicated server or in sandbox testing.

How do I make an API request from Blueprints with the Invo Request node, and what do OnSuccess and OnFail return?

Add the "Invo Request" node (InvoSDK | API), set Base URL, Endpoint, Headers, Body and Method, and wire the OnSuccess and OnFail pins. OnSuccess fires on any 2xx response and OnFail on a non-2xx or a transport failure; both pass ResponseContent (raw body), StatusCode, bWasSuccessful and JsonObject (parsed JSON). A request that could not start reports StatusCode -1.

Which JSON helper functions are available in Blueprints?

In the InvoSDK | JSON category: GetStringField, GetIntegerField, GetBoolField and GetObjectsArray, each reading a named field from the JsonObject wrapper.

How do I fetch a balance and purchase an item from C++, and why must those calls run on a dedicated server or authority?

Include InvoSDKApiManager.h and InvoSDKJsonHelpers.h, create a request with UInvoSDKApiManager::InvoRequest(...) for GET /api/player-balances/player/by-email/{email} or POST /api/item-purchases/purchase-item, bind OnSuccess/OnFail and call Activate(). Both calls need X-Game-Secret-Key, which authenticates as your whole game, so they must run where the key is safe: a dedicated server or backend, never a shipped client.

Which UMG widgets come with the plugin?

Under Content/UI/ and Content/Widgets/: daily deals, featured items and bundles, item cards and lists, item purchase, navigation bar, quick send, recent purchases, send currency, transfer currency and weekly featured bundle widgets. Sender approval is not a UMG widget: the plugin opens INVO's hosted approval page in the system browser and shows a built-in match-code prompt on a first-time phone. A legacy SMS verification widget is still in the content folder but SMS is retired; do not use it.

How do I fix "Config not found!", 401, 403 and request timeouts?

"Config not found!" means the InvoSDKConfig asset is missing or not referenced; create and configure it. A 401 means a missing or wrong key or token for that environment; a 403 means the game is not live or testing or the key belongs to the other environment (sandbox versus production). Timeouts are usually connectivity or a wrong base URL: sandbox calls must use https://sandbox.invo.network with the /sandbox/api/ prefix.

How do I enable verbose debug logging?

Add [Core.Log] with LogTemp=Verbose to your project's DefaultEngine.ini.

Web / TypeScript SDK

Read the Web / TypeScript SDK page

What is the difference between @invonetwork/web-sdk and @invonetwork/web-sdk/server, and which one holds the game secret?

@invonetwork/web-sdk/server (InvoServer) runs on your backend and holds the game secret: it mints player tokens, initiates sends and transfers, creates checkouts and runs item purchases. @invonetwork/web-sdk (InvoClient) runs in the browser with only a short-lived player token, for passkey enrollment, approvals, self-claim, device link, balances and destinations. Never import /server into browser code.

What Node version does the server entry need, and does the package ship ESM, CJS and types?

Node 18 or later (it uses the global fetch). The package ships ESM, CommonJS and TypeScript declarations for both entry points.

In browser-direct mode, what CORS setup does Invo need for my origin, and why does proxy mode not need it?

In browser-direct mode the browser calls INVO's API itself, so INVO must add your origin to its CORS allow-list; ask INVO to do that for each environment. In proxy mode the browser only talks to your own backend, which forwards to INVO server-to-server, so no cross-origin browser call to INVO happens and no CORS change is needed.

In proxy mode, what do I pass to InvoClient as token, and where does the real player token get swapped in?

Set baseUrl to your proxy and pass your own short-lived session or CSRF token as token (or use cookie auth with a custom fetch). Your proxy validates that token, then injects the real INVO player token as the Authorization header before forwarding, so the INVO token never reaches the browser.

When should I use the session actor resolver versus the trusted first-party resolver in examples/proxy-server.ts?

Use the session resolver (the default) for real player-facing integrations: the acting player is whoever is logged in to your session, so one login can only act as one player. The trusted first-party mode, enabled by setting INVO_PROXY_TRUSTED_SECRET, lets the caller name playerEmail and is guarded by a shared secret header; it is only for an internal dashboard or test rig that must act as arbitrary players.

How do I create a hosted checkout with createCheckout, and how long is checkoutUrl valid?

On the server call invo.createCheckout({ playerEmail, usdAmount, rail?, metadata?, successUrl?, cancelUrl? }) and send the browser to the returned checkoutUrl. It is single use and valid for 15 minutes; grant currency from the purchase.completed webhook.

Which item purchase checks run locally before the request?

purchaseItem throws INVALID_INPUT before any network call unless itemQuantity is an integer from 1 to 1000, unitPrice is above 0 and at most 999999.99, and totalPrice equals unitPrice times itemQuantity within 0.01 (compared in integer cents). Required string fields must also be present.

Should I grant an item from the purchaseItem response or from the item.purchased webhook?

Grant from the item.purchased webhook, which carries transaction_id, order_id, the item fields, new_balance and the fee breakdown. Use the synchronous response for immediate UI, but the webhook is delivered even if your request timed out or the client went away.

How do I tell a duplicate purchase from an insufficient balance?

err.isDuplicateRequest is true on a 409 for a reused clientRequestId (DUPLICATE_REQUEST or DUPLICATE_CLIENT_REQUEST_ID), which means the original purchase already went through; do not show it as a failure. err.isInsufficientBalance is true only on a 400 whose body carries required_amount and/or current_balance; a 429 insufficient_balance_blocked throttle is deliberately not classified as insufficient balance.

How do initiateSend field names map to initiateTransfer field names?

Same shape, different prefixes: senderPlayerName/Email/Phone map to sourcePlayerName/Email/Phone, receiverPlayerEmail/Phone map to targetPlayerEmail/Phone, and receivingGameId maps to targetGameId. clientRequestId and amount are the same on both, and the receiver/target email is optional.

How do I branch on verificationMethod when it is in_app, sms, or undefined on a 202 guardian response?

in_app means the sender approves with a passkey or device approval. sms is a legacy value; still approve with a passkey (or hosted/QR device approval), never a PIN. undefined comes with an HTTP 202 guardian hold: the transaction waits for the guardian, so poll getGuardianApprovalStatus(txnId) or wait for the outcome before asking the sender to approve.

What is the difference between identity_gate and receiving_confirm items in getPendingCollect()?

identity_gate rows are the player's own outgoing sends or transfers still waiting for their approval: call approveSend/approveTransfer. receiving_confirm rows are inbound value waiting for this player to collect, including value from other titles: call confirmReceiptSend/confirmReceiptTransfer. Render both kinds, or inbound value will never show.

When should I use the server's getInboundPending instead of getPendingCollect?

Use getPendingCollect() in the browser for the logged-in player's own list (player token, no PII). Use the server's getInboundPending({ playerEmail | playerPhone }) (game secret) when your backend needs a richer view, for example to route a "you have currency to collect" notification by toPhone/toIdentityId or to badge a player who is not currently in a session. Both pair with the transfer.claim_pending webhook.

What should the UI show on a 202 with holdReason RISK_HOLD, GUARDIAN_APPROVAL_PENDING or RECIPIENT_IDENTITY_PENDING?

All three mean "accepted but not finished yet", not failure. RISK_HOLD: tell the player it is under review and will complete or be returned automatically; GUARDIAN_APPROVAL_PENDING: tell them it is waiting for their guardian's approval; RECIPIENT_IDENTITY_PENDING (on confirm-receipt): the recipient must finish confirming who they are before the funds are released. Poll status or wait for the webhook rather than retrying.

How do I resubmit when claimTransfer or claimCurrency returns needsAccountSelection with candidates?

The phone matches more than one of your players, so show the candidates and let the player choose. Re-submit the same claim with the chosen account's id in targetPlayerId (claimTransfer) or receiverPlayerId (claimCurrency).

How do I clear 409 PHONE_SHARE_APPROVAL_REQUIRED with phoneShareInitiate, phoneShareApprove and phoneShareStatus?

The 409 means the phone already belongs to a player with another email, and its owner must consent. The 409 body carries approval_id and whether a code was already sent; call phoneShareInitiate({ phone, email }) only if no code went out, then phoneShareApprove({ approvalId, otp }) with the code the phone's owner received, and phoneShareStatus to check. Once approved, re-issue the original request unchanged; the grant is consumed automatically.

When does enrollPasskey() throw isEnrollmentAuthorizationRequired versus isEnrollmentProofRequired?

isEnrollmentAuthorizationRequired (ENROLLMENT_REQUIRES_AUTHORIZATION) means your tenant requires a one-time code before a first enrollment: run enrollmentBegin(), enrollmentVerify(code), then retry enrollPasskey(). isEnrollmentProofRequired (ENROLLMENT_REQUIRES_PROOF, 409) means the identity already has an approval method: if the player still has it, run linkDevice() then enroll; if they lost it, use the passkey recovery flow.

Since 3.10.1 linkDevice() takes no arguments. What happens if older code passes an id?

Nothing breaks. The SDK still sends a passed id, but the server ignores it and mints its own link id, which the SDK echoes on the complete call.

Why must approveHosted be called from a click handler, and what do its results mean?

It opens INVO's approval popup synchronously before any network call, and browsers only allow popups opened during a user gesture. Results: approved (the approval settled; see approval), denied (the player declined), expired (the grant timed out; start again), closed (the player closed the popup or you aborted), blocked (the popup could not be used; message says why and nothing was started if the browser blocked it) and redirected (popups were blocked and the redirect fallback sent this tab to the hosted page).

How do steamPacks, steamInitPurchase and steamFinalizePurchase fit together, and why must purchaseReference be reused on retries?

On the game-platform rail you sell INVO-defined packs, never prices: steamPacks({ steamid }) lists packs priced for that player, steamInitPurchase({ playerEmail, steamid, packId, purchaseReference, metadata }) opens the authorization (in the overlay, or a browser checkout with usersession: "web" plus playerIp), and steamFinalizePurchase({ orderId }) credits once the player approves. purchaseReference is the idempotency key: reusing it on a retry returns the same order instead of opening a second charge. A finalize replay answers alreadyProcessed: true, never a double credit.

Which webhook verifier should I use on Node, Cloudflare Workers or the Next.js App Router?

On Node use verifyWebhook(rawBody, signatureHeader, secret) (it uses node:crypto). On Workers, Deno, edge runtimes or Bun use verifyWebhookAsync (Web Crypto, same arguments, awaited), or createWebhookHandler({ secret, onEvent }), which returns a Fetch-API handler suitable for a Next.js App Router route.

Which calls does the SDK retry automatically, and why are passkey POSTs never retried?

Only idempotent requests retry: GETs and POSTs that carry an idempotency key, on network errors, 429 (honoring retry_after) and 500/502/503/504, with exponential backoff (maxRetries default 2, 0 disables). Passkey POSTs carry single-use challenges and assertions, so replaying one would fail or double-count; they are never retried, and neither is anything you abort.

What does an InvoError with code ABORTED mean?

The call was cancelled through the AbortSignal you passed ({ signal }). It has status 0, is never retried, and says nothing about whether the server processed the request, so re-check state before acting on it.

Python SDK

Read the Python SDK page

Which Python versions does invonetwork support, and does it have third-party runtime dependencies?

Python 3.9 or later, with no third-party runtime dependencies.

What base_url do I pass to InvoServer for sandbox and production, and should I reuse one client?

https://sandbox.invo.network/sandbox for sandbox and https://invo.network for production, each with that environment's own game secret. Construct one InvoServer per environment and reuse it across requests.

How do I mint a player token with mint_player_token and hand it to the browser SDK?

Call invo.mint_player_token(player_email=..., player_phone=..., player_name=...) on your server and return result.token to your front end, which passes it to new InvoClient({ token, baseUrl }). Always pass player_phone: an unknown player is then created (created=True) instead of answering 404, and phone_mismatch=True tells you INVO holds a different phone.

What are the required arguments of purchase_item, and must prices be strings?

Required keyword arguments: client_request_id, player_email, player_name, item_id, item_name, item_quantity (int 1 to 1000), unit_price and total_price; player_phone, item_description and item_category are optional. Prices may be str, int or float, but strings such as "150.00" are recommended because they are sent as given and avoid float rounding; total_price must equal unit_price times quantity within 0.01.

Which version adds get_player_activity and platform_commerce.get_purchase_history?

Version 3.20.0, along with iterate_player_activity and iterate_purchase_history for walking every page.

How do steam_packs, steam_init_purchase and steam_finalize_purchase fit together, and when do I pass usersession="web" with player_ip?

steam_packs lists the packs priced for the player, steam_init_purchase(player_email=, steamid=, pack_id=, purchase_reference=, metadata=) opens the authorization, and steam_finalize_purchase(order_id=) credits after the player approves. By default authorization pops up in the in-game overlay; pass usersession="web" when the overlay cannot render (a browser build) to get a browser checkout URL, and then player_ip is required and must be the player's own address, not your server's. Reuse purchase_reference on every retry.

Why are passkey enroll, approve, confirm-receipt and device link not in the Python SDK, and how do I support them from a Python backend?

They are WebAuthn ceremonies that must run in the player's browser through navigator.credentials, with a player token, so a server SDK cannot perform them. Mint the player token in Python and run those steps with the Web SDK's InvoClient in the browser (or relay them through your backend); everything that uses the game secret is in the Python SDK.

Do the deprecated verify_sms_transfer and verify_sms_send still work, and do they raise DeprecationWarning?

They still work and call /api/transfers/verify-sms and /api/currency-sends/verify-sms, but they are deprecated (since 3.1.0) and a future major version will remove them. They emit no runtime warning. Do not build on them: approve with a passkey or device approval in the browser.

What does each row from get_inbound_pending contain, and why is to_identity_id sometimes None?

Each InboundPendingItem has transaction_id, flow (transfer or send), amount, net_amount (what the recipient receives after fees), source_game_id, source_game, to_phone, to_identity_id, created_at and claim_code_expires_at. to_identity_id is None when the destination phone maps to more than one of your players, so match on to_phone and do not require the identity id.

How do I verify webhooks with verify_webhook in Flask and FastAPI?

Pass the raw bytes, the X-Invo-Signature header and your signing secret: verify_webhook(request.get_data(), request.headers.get("X-Invo-Signature"), secret) in Flask, or verify_webhook(await request.body(), ...) in FastAPI. It raises InvoError on a bad signature, stale timestamp or malformed body (answer 400) and returns a typed WebhookEvent; dedupe on the idempotency key before acting.

How do I rotate a webhook secret without dropping deliveries?

Pass a list of secrets, for example verify_webhook(raw, sig, [old_secret, new_secret]); a signature matching any of them is accepted. Remove the old secret once rotation is complete.

Which attributes does InvoError expose, and which one do I quote to support?

code, status, message, body and request_id, plus retry_after and classifier properties such as is_insufficient_balance and is_duplicate_request. Quote request_id (the backend request id from the response headers) in support tickets.

Which failures are retried automatically, and why only idempotent requests?

Network errors and timeouts, 429 (honoring retry_after) and 500/502/503/504, but only for GETs and POSTs marked idempotent (those carrying an idempotency key). A timeout can fire while the server is still processing, so retrying a non-idempotent POST could perform the action twice.

What do is_steam_authorization_pending, is_steam_authorization_dead and is_partner_credit_unavailable tell a poller?

is_steam_authorization_pending means the player has not approved yet: keep waiting and call finalize again. is_steam_authorization_dead means the authorization ended without approval (cancelled, failed, refunded and so on): stop polling. is_partner_credit_unavailable (PARTNER_CREDIT_UNAVAILABLE, 409) is a refusal before the player is charged because the studio's funding is not available right now; show a plain "try again later" and reuse the same purchase_reference on a manual retry.

Does the package ship type information?

Yes, it ships py.typed with full type hints.

Back to the FAQ hub. Something not answered here? Email partners@invo.network.