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.

  1. Keys and sandbox. In the sandbox console (https://dev.console.invo.network) take the title’s key (ivsdk_...) and its game_id, set the title to live and give it a currency; reveal the sandbox clock key (ivclk_...) under Game Settings. Point your base URL at https://sandbox.invo.network/sandbox. This applies to a game or a platform alike: the API calls the tenant a game and names its key X-Game-Secret-Key, but a platform tenant uses the same field and the same flow. A title that is not live answers 403 GAME_NOT_LIVE on every create; setting it live is the same console action in production (phase 11). Overview: prerequisites; Sandbox, section 1.
    ValueWhere it comes fromSide
    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_id on 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.network in production, https://sandbox.invo.network/sandbox in sandbox.Server
  2. Webhooks. PUT /api/dev/webhooks/games/<game_id> with your target URL and the ten subscription.* events (plus purchase.completed, to skip a renewal’s mint leg, and purchase.disputed, for chargebacks); store signing_secret from that response now, it cannot be retrieved later. Implement verification over the raw bytes, dedupe on X-Invo-Idempotency-Key, fire POST .../test, and read GET .../deliveries until the webhook.test row reads succeeded. Register BEFORE the first subscription exists. Webhooks, sections 1 and 2.
  3. Player. Know how a brand-new member is created. Any call that carries player_name creates them: the hosted card page mint, /subscribe itself, a purchase, a send, a transfer. Without it the card page answers 404 PLAYER_NOT_FOUND for a member your title has never seen, and /setup-intent never creates one. Decide the order for brand-new members now. Card road, “New and existing members”.
  4. Card capture. Mint POST /api/checkout/card-setup-sessions, send the member to card_setup_url (never framed; use the URL as given; send player_name for a new member), and on their return read the card id from GET /api/currency-purchases/player-cards. In sandbox, enter 4242 4242 4242 4242 on the page. Skip this phase on the Steam rail. Card road, section 1.
  5. Subscribe. POST /api/subscriptions/subscribe with a client_request_id you persisted before the call, consent, and the member’s card; branch on all five first_charge.status values; on any timeout or 5xx replay the same body. On Steam, /steam/init, store steam_order_id, then /steam/finalize. Card road, sections 3 and 4; Steam road. Then reconcile the first renewal: receive subscription.renewed for period 1 and match it to the create response on subscription_id and first_charge.paid_period_seq = event period_seq; extend access to current_period_start on the event (the new paid_through); book split.partner_revenue_usd. Webhooks, “Reconciling period 1”.
  6. Renewals and dunning. advance-clock then force-renewal for a second paid period; force-failure four times for the whole ladder to expired; one real authentication challenge with the step-up test card. Confirm your handlers keep access on payment_failed, past_due and authentication_required, and revoke only on expired (at final_period_end) and canceled (at access_until). Renewals; Sandbox, sections 2 to 5.
  7. Manage. GET one and list by player; cancel at period end and immediately (read terminated_immediately and access_until); stage a price change; replace the card with /payment-method. Manage.
  8. Refunds. Refund a card period in part, then in full, always with a reason; read the receipts, receive subscription.refunded, reverse refund.partner_debited_usd (revenue plus Invo’s card fee: the whole refunded price) in your books, handle the subscription.canceled a full refund sends, replay the same key, and handle a 202 pending_approval and the three refund request events. Refunds, “Prove it in sandbox”.
  9. Reporting. If you nominate revenue-share recipients, run the aggregate report for the window you just filled and check net_attributed_usd against what your handler accrued. Reporting.
  10. Errors. Map every code your integration branches on to the error reference; match on error_code, never on message.
  11. 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 with webhook.test; a real card on the production card page charges nothing). Then set the production title live in 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

  1. Replay before retry. On any timeout or 5xx from /subscribe, send the same body again. A new client_request_id in a retry loop lands on 409 ACTIVE_SUBSCRIPTION_EXISTS at best and a second subscription at worst.
  2. requires_action is not paid. Nothing has been charged. Do not grant paid access until subscription.renewed.
  3. pending is not failed. Wait for the event; Invo resolves it.
  4. current_period_end is not entitlement. Use paid_through.
  5. payment_failed and past_due do not revoke access. Only expired (at final_period_end) and canceled (at access_until) do.
  6. authentication_required is not a failure. Do not send the member a “payment failed” message; send them the link.
  7. Do not poll for renewals. Subscribe to subscription.renewed.
  8. 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.
  9. At-period-end cancel on an unpaid subscription is immediate. Read terminated_immediately on the response.
  10. Price changes apply next period, and big jumps do not apply at all. More than double, or above the 25000.00 per-charge ceiling, stays pending forever.
  11. A card saved without off-session consent cannot back a subscription. Cards saved through saved_card_id purchases, or before the consent change, need to be saved again through the hosted card page, /setup-intent, or a save_card: true purchase of a new card. The platform-commerce checkout page never saves one.
  12. The card page creates a new member only when you send player_name. Without it the hosted card page answers 404 PLAYER_NOT_FOUND for a member your title has never seen; /setup-intent never creates one. For a new signup, mint the card page with player_name, then /subscribe. That is also the order a paid trial needs, since it needs the card before /subscribe.
  13. 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 with no_payment_method.
  14. wallet_only is deprecated, and absolute. A card attached later is ignored until you pass wallet_only: false on /payment-method, and a wallet_only card subscription can never be paid. Do not send it.
  15. A renewal’s purchase.completed is not a purchase. Every paid card renewal also sends one (data.metadata.source == "subscription_renewal"). Grant nothing from it; grant on subscription.renewed.
  16. A 202 from /refund is not a refund. status: "pending_approval" means nothing moved; wait for subscription.refund_approved or subscription.refund_rejected.
  17. A full refund ends the subscription. Refund a partial amount if the member keeps the membership.
  18. 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.
  19. A paid trial needs the card first. Without a saved card /subscribe refuses it with 400 PAYMENT_METHOD_REQUIRED; a free trial would have been created card-less.
  20. 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 new client_request_id. A temporary error, by contrast, goes to past_due and is retried.
  21. skipped_trial means a free trial. A paid trial reports paid (or another first-charge outcome) at the trial price, and its period 1 sends subscription.renewed with is_trial: true.
  22. A late trial charge means two charges back to back. If a paid trial is still awaiting_authentication or past_due at trial_end and then succeeds, the full price follows immediately. Cancel it at trial_end if that is not what you want.
  23. sub_ is reserved as a prefix for every idempotency key on the platform, not only here.
  24. Metadata is yours except _invo. Invo writes cancellation reasons under that key.
  25. Send consent. It is what a disputed charge is argued from, trial terms included.
  26. Do not store or log confirmation_url beside anything you publish. It is a bearer link.
  27. subscription.period_seq is the next period, not the paid one. After a paid first charge it already reads 2. Key entitlement on paid_period_seq and on the period_seq of subscription.renewed.

Steam traps

  1. Finalize is your call. Steam does not tell Invo the member authorised; your game must call /steam/finalize after the overlay or checkout closes, and again if it returned not_authorized.
  2. Match the client callback on steam_order_id, never on steam_transid. Store it against the subscription_id before you return from init.
  3. 24 hours to authorise. An unfinalised subscription expires and must be re-created with a new key.
  4. web sessions need player_ip and the checkout URL must open top-level; it cannot be framed.
  5. Only the primary key or the steam channel key may start a Steam subscription.
  6. No trials, no interval_count other than 1, no wallet_only, no card fields.
  7. The price is a ceiling. Increases are refused; start a new Steam subscription. Decreases stage.
  8. /payment-method is refused on a Steam subscription.
  9. Coins per period depend on the member’s country and are fixed at init. Show amount_coins from the init response, not price times 10.
  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.
  11. A member can cancel from Steam. You learn it from subscription.canceled with canceled_by: "steam"; access runs to access_until.
  12. 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.
  13. Replaying /steam/init returns the row’s current status, which may already be active, canceled or expired.
  14. A second init while one is pending reuses it. Same terms, same Steam account and same usersession: you get the pending row back with pending_reuse: true and the same steam_order_id, and no second agreement is opened. Anything else: 409 STEAM_AUTHORIZATION_PENDING carrying the pending subscription_id.
  15. 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.
  16. Same item_id as the card version of the membership. 409 ACTIVE_SUBSCRIPTION_EXISTS across rails is the guard against double-billing, not a bug.
  17. Refunds of Steam-charged periods are not supported yet. /refund answers 409 STEAM_REFUND_NOT_SUPPORTED; wallet-covered periods refund in coins.
  18. Sandbox does not exercise the storefront. sandbox_auto_approved: true means the charge was simulated; do one real Steam sandbox authorisation before launch.
  19. split.total_usd on Steam is not the price. It is what the coins are worth after the storefront’s share and VAT; the price is the top-level amount_usd.
  20. One expiry arrives with no payment_failed before it: steam_authorization_abandoned. Handle subscription.expired on its own. (steam_containment is 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.