Launch checklist and traps
What must be true before you create a real subscription, followed by the traps that account for nearly every support question we have seen. Each one links to the page that explains it.
The phases, in order
The same eleven phases as the map on the overview, same numbers and names, in the order to do them, each naming the page that covers it. Do the whole list in sandbox first, then repeat phases 1, 2 and 4 against production keys and finish with phase 11. Every phase is self-serve in both environments. Two steps are one way and are called out: the webhook signing secret is shown once, and events fired before a target is registered are not delivered to it later.
- Keys and sandbox. In the sandbox console (
https://dev.console.invo.network) take the title’s key (ivsdk_...) and itsgame_id, set the title toliveand give it a currency; reveal the sandbox clock key (ivclk_...) under Game Settings. Point your base URL athttps://sandbox.invo.network/sandbox. This applies to a game or a platform alike: the API calls the tenant a game and names its keyX-Game-Secret-Key, but a platform tenant uses the same field and the same flow. A title that is not live answers403 GAME_NOT_LIVEon every create; setting it live is the same console action in production (phase 11). Overview: prerequisites; Sandbox, section 1.Value Where it comes from Side INVO_GAME_SECRETConsole, on the title. Rotatable with a 7-day grace. One per environment. Server only, never the browser INVO_GAME_IDConsole, on the title (also subscription.game_idon any read).Server INVO_WEBHOOK_SECRETPhase 2, shown once at registration. Server only INVO_SANDBOX_CLOCK_KEYSandbox console, Game Settings. Test harness only; no effect outside sandbox. Test harness INVO_BASE_URLhttps://invo.networkin production,https://sandbox.invo.network/sandboxin sandbox.Server - Webhooks.
PUT /api/dev/webhooks/games/<game_id>with your target URL and the tensubscription.*events (pluspurchase.completed, to skip a renewal’s mint leg, andpurchase.disputed, for chargebacks); storesigning_secretfrom that response now, it cannot be retrieved later. Implement verification over the raw bytes, dedupe onX-Invo-Idempotency-Key, firePOST .../test, and readGET .../deliveriesuntil thewebhook.testrow readssucceeded. Register BEFORE the first subscription exists. Webhooks, sections 1 and 2. - Player. Know how a brand-new member is created. Any call that carries
player_namecreates them: the hosted card page mint,/subscribeitself, a purchase, a send, a transfer. Without it the card page answers404 PLAYER_NOT_FOUNDfor a member your title has never seen, and/setup-intentnever creates one. Decide the order for brand-new members now. Card road, “New and existing members”. - Card capture. Mint
POST /api/checkout/card-setup-sessions, send the member tocard_setup_url(never framed; use the URL as given; sendplayer_namefor a new member), and on their return read the cardidfromGET /api/currency-purchases/player-cards. In sandbox, enter4242 4242 4242 4242on the page. Skip this phase on the Steam rail. Card road, section 1. - Subscribe.
POST /api/subscriptions/subscribewith aclient_request_idyou persisted before the call,consent, and the member’s card; branch on all fivefirst_charge.statusvalues; on any timeout or 5xx replay the same body. On Steam,/steam/init, storesteam_order_id, then/steam/finalize. Card road, sections 3 and 4; Steam road. Then reconcile the first renewal: receivesubscription.renewedfor period 1 and match it to the create response onsubscription_idandfirst_charge.paid_period_seq= eventperiod_seq; extend access tocurrent_period_starton the event (the newpaid_through); booksplit.partner_revenue_usd. Webhooks, “Reconciling period 1”. - Renewals and dunning.
advance-clockthenforce-renewalfor a second paid period;force-failurefour times for the whole ladder toexpired; one real authentication challenge with the step-up test card. Confirm your handlers keep access onpayment_failed,past_dueandauthentication_required, and revoke only onexpired(atfinal_period_end) andcanceled(ataccess_until). Renewals; Sandbox, sections 2 to 5. - Manage.
GETone and list by player; cancel at period end and immediately (readterminated_immediatelyandaccess_until); stage a price change; replace the card with/payment-method. Manage. - Refunds. Refund a card period in part, then in full, always with a
reason; read the receipts, receivesubscription.refunded, reverserefund.partner_debited_usd(revenue plus Invo’s card fee: the whole refunded price) in your books, handle thesubscription.canceleda full refund sends, replay the same key, and handle a202pending_approvaland the three refund request events. Refunds, “Prove it in sandbox”. - Reporting. If you nominate revenue-share recipients, run the aggregate report for the window you just filled and check
net_attributed_usdagainst what your handler accrued. Reporting. - Errors. Map every code your integration branches on to the error reference; match on
error_code, never onmessage. - Go-live gate. Walk the checklist below. Repeat phases 1, 2 and 4 against production keys (a production title, its own key and
game_id, its own webhook registration proven withwebhook.test; a real card on the production card page charges nothing). Then set the production titlelivein the production console, the same action you took in sandbox; a live title is what makes subscriptions sellable and transfers receivable, and nothing else gates it. Record the sandbox run somewhere your team can see it: “has the drill been run” is a question people ask months later, and a script that exists is not evidence that it passed. Then create the first real subscription. Overview: prerequisites.
Before launch
The sandbox clock tools are not under /api
https://sandbox.invo.network/sandbox/subscriptions/<id>/advance-clock, not .../sandbox/api/subscriptions/.... The wrong path is a plain 404 that looks like a missing feature. Every other subscription call is under /api.
Card traps
- Replay before retry. On any timeout or 5xx from
/subscribe, send the same body again. A newclient_request_idin a retry loop lands on409 ACTIVE_SUBSCRIPTION_EXISTSat best and a second subscription at worst. requires_actionis notpaid. Nothing has been charged. Do not grant paid access untilsubscription.renewed.pendingis notfailed. Wait for the event; Invo resolves it.current_period_endis not entitlement. Usepaid_through.payment_failedandpast_duedo not revoke access. Onlyexpired(atfinal_period_end) andcanceled(ataccess_until) do.authentication_requiredis not a failure. Do not send the member a “payment failed” message; send them the link.- Do not poll for renewals. Subscribe to
subscription.renewed. - One live subscription per item, on either rail. Cancel the old one before creating a new one for the same
item_id, or change the price with/amount. - At-period-end cancel on an unpaid subscription is immediate. Read
terminated_immediatelyon the response. - Price changes apply next period, and big jumps do not apply at all. More than double, or above the
25000.00per-charge ceiling, stays pending forever. - A card saved without off-session consent cannot back a subscription. Cards saved through
saved_card_idpurchases, or before the consent change, need to be saved again through the hosted card page,/setup-intent, or asave_card: truepurchase of a new card. The platform-commerce checkout page never saves one. - The card page creates a new member only when you send
player_name. Without it the hosted card page answers404 PLAYER_NOT_FOUNDfor a member your title has never seen;/setup-intentnever creates one. For a new signup, mint the card page withplayer_name, then/subscribe. That is also the order a paid trial needs, since it needs the card before/subscribe. - Coins never pay a card subscription. Since 2026-09-17 the card is charged the full price every period. Save a card before
/subscribe; a member with coins but no card fails withno_payment_method. wallet_onlyis deprecated, and absolute. A card attached later is ignored until you passwallet_only: falseon/payment-method, and awallet_onlycard subscription can never be paid. Do not send it.- A renewal’s
purchase.completedis not a purchase. Every paid card renewal also sends one (data.metadata.source == "subscription_renewal"). Grant nothing from it; grant onsubscription.renewed. - A
202from/refundis not a refund.status: "pending_approval"means nothing moved; wait forsubscription.refund_approvedorsubscription.refund_rejected. - A full refund ends the subscription. Refund a partial amount if the member keeps the membership.
- A lost chargeback does not end the subscription. It costs you what a refund of that period does: your share is reversed and Invo’s card fee is charged to you (
purchase.disputed,partner_debited_usd), unless the chargeback guarantee covers it (guarantee_covered: true), and the next renewal still charges. Cancel it yourself if you do not want to keep billing that member. - A paid trial needs the card first. Without a saved card
/subscriberefuses it with400 PAYMENT_METHOD_REQUIRED; a free trial would have been created card-less. - A refused trial charge ends the subscription at once.
first_charge.status: "failed",subscription.status: "expired", no retries. Replaying the key will not revive it; subscribe again with a newclient_request_id. A temporary error, by contrast, goes topast_dueand is retried. skipped_trialmeans a free trial. A paid trial reportspaid(or another first-charge outcome) at the trial price, and its period 1 sendssubscription.renewedwithis_trial: true.- A late trial charge means two charges back to back. If a paid trial is still
awaiting_authenticationorpast_dueattrial_endand then succeeds, the full price follows immediately. Cancel it attrial_endif that is not what you want. sub_is reserved as a prefix for every idempotency key on the platform, not only here.- Metadata is yours except
_invo. Invo writes cancellation reasons under that key. - Send
consent. It is what a disputed charge is argued from, trial terms included. - Do not store or log
confirmation_urlbeside anything you publish. It is a bearer link. subscription.period_seqis the next period, not the paid one. After a paid first charge it already reads2. Key entitlement onpaid_period_seqand on theperiod_seqofsubscription.renewed.
Steam traps
- Finalize is your call. Steam does not tell Invo the member authorised; your game must call
/steam/finalizeafter the overlay or checkout closes, and again if it returnednot_authorized. - Match the client callback on
steam_order_id, never onsteam_transid. Store it against thesubscription_idbefore you return from init. - 24 hours to authorise. An unfinalised subscription expires and must be re-created with a new key.
websessions needplayer_ipand the checkout URL must open top-level; it cannot be framed.- Only the primary key or the
steamchannel key may start a Steam subscription. - No trials, no
interval_countother than 1, nowallet_only, no card fields. - The price is a ceiling. Increases are refused; start a new Steam subscription. Decreases stage.
/payment-methodis refused on a Steam subscription.- Coins per period depend on the member’s country and are fixed at init. Show
amount_coinsfrom the init response, not price times 10. - An empty wallet pays the full price; a partial wallet pays only for the missing coins. Both are correct and both appear as
funding.steam_charged_usd. - A member can cancel from Steam. You learn it from
subscription.canceledwithcanceled_by: "steam"; access runs toaccess_until. - Some Steam renewals are deferred for reasons on your side (billing setup, credit, Steam configuration) with no event. Keep your Invo billing setup healthy and watch
next_charge_at. - Replaying
/steam/initreturns the row’s current status, which may already beactive,canceledorexpired. - A second init while one is pending reuses it. Same terms, same Steam account and same
usersession: you get the pending row back withpending_reuse: trueand the samesteam_order_id, and no second agreement is opened. Anything else:409 STEAM_AUTHORIZATION_PENDINGcarrying the pendingsubscription_id. - Reuse is deliberately narrow, and narrower is not the same as safer. It would be easy to read the rule as “reuse whenever the price matches, because a duplicate agreement is the danger”. It is not. An authorisation belongs to one Steam account and one session type: hand a member an agreement her current Steam account cannot see, or a web checkout link when she is in the overlay, and she is stuck for a full day with no way to act. A rival agreement she can answer is the lesser harm, so the rule fails toward “she can act” rather than toward “nothing is duplicated”. When you get the 409, show her the pending one and let her finalize or cancel it.
- Same
item_idas the card version of the membership.409 ACTIVE_SUBSCRIPTION_EXISTSacross rails is the guard against double-billing, not a bug. - Refunds of Steam-charged periods are not supported yet.
/refundanswers409 STEAM_REFUND_NOT_SUPPORTED; wallet-covered periods refund in coins. - Sandbox does not exercise the storefront.
sandbox_auto_approved: truemeans the charge was simulated; do one real Steam sandbox authorisation before launch. split.total_usdon Steam is not the price. It is what the coins are worth after the storefront’s share and VAT; the price is the top-levelamount_usd.- One expiry arrives with no
payment_failedbefore it:steam_authorization_abandoned. Handlesubscription.expiredon its own. (steam_containmentis a second such code; it is not sent at present, and currency bought through a platform store is subject to that store's rules, so keep any handling you have for it.)
Every code behind these traps is on the error reference.