Subscriptions

A subscription is a standing agreement, created by your server, under which Invo bills one of your players a fixed USD price on a fixed interval and grants them your title’s currency each period. You keep the entitlement (what the member gets in your title). Invo keeps the clock, the charges, the retries, the notifications and the revenue split. This section walks the whole thing, on both rails, with the exact call at every step in curl, Node and Python.

The pages in this section

The card road, end to end

Send the member to the Invo-hosted card page (no browser card code on your side), read the card id, create the subscription, handle every first_charge outcome including the authentication step, trials, wallet-only, consent evidence and attribution.

The Steam road, end to end

Init, the overlay or web-session authorisation, finalize, what a period is worth in each country, what Steam cannot do, and the traps.

Reading and changing a subscription

Get, list, the full subscription object, cancel (now or at period end), change the price, change the card.

Renewals, funding and dunning

The billing clock, the wallet-first funding order with worked examples on both rails, the 2 / 3 / 2 retry ladder, grace, expiry, card authentication and the deferrals you are not told about.

Webhooks

Every event with a card example and a Steam example, the envelope, the exact signature scheme, retries, dedupe, and which event to trust for what.

Refunds

Refund a billed period, full or partial, what goes back to the card and what goes back to the wallet, the receipt and the event.

Sandbox recipe

The four clock tools and their second credential, then a step-by-step recipe on each rail that takes a subscription from creation through renewals, dunning, expiry, cancel and refund in minutes.

Error reference

Every code every subscription route can return, grouped by route, with the remedy.

Launch checklist and traps

What must be true before you go live, and the sixteen card traps and sixteen Steam traps that account for nearly every support question.

Reporting and attribution

Three read-only endpoints that tell you what each nominated recipient is owed for a window, net of reversals. A report, not a remittance.

From a game secret to a reconciled renewal, in order

Every phase a partner goes through, the call that does it, and the page that covers it. Nothing on this list needs a conversation with Invo, in sandbox or in production; where a step needs the console it says so.

PhaseWhat you doPage
1. Keys and sandboxGet the sandbox key (ivsdk_...) and game_id from the sandbox console, set the sandbox base URL, make the title live, reveal the sandbox clock key.This page (prerequisites); Sandbox
2. WebhooksRegister a target, store the signing secret, verify signatures, fire webhook.test, dedupe on the idempotency key.Webhooks
3. PlayerMake sure the member exists in your title (any call carrying player_name creates them; the card endpoints do not).Card road, “The member must exist first”
4. Card captureMint POST /api/checkout/card-setup-sessions, send the member to the Invo-hosted card page, read the card id from /player-cards.Card road, section 1
5. SubscribePOST /api/subscriptions/subscribe (card) or /steam/init then /steam/finalize (Steam); branch on first_charge.status.Card road / Steam road
6. Renewals and dunningExtend access on subscription.renewed; message on payment_failed and authentication_required; revoke only on expired and canceled. Drive it in minutes with the sandbox clock tools.Renewals; Sandbox
7. ManageGet, list, cancel, reprice, change the card.Manage
8. RefundsPOST .../refund, read the receipt, reverse your revenue on subscription.refunded.Refunds
9. ReportingWindowed attribution statements for your own payout run.Reporting
10. ErrorsEvery code every route returns, with the remedy.Errors
11. Go-live gateThe checklist, walked in order; phases 1, 2 and 4 repeated against production keys; then set the production title live in the production console, the same action as in sandbox.Checklist

What happens each billing period

  1. Invo works out what the period is worth in coins. On the card rail that is the price times the platform peg of 10 coins per USD: a 9.99 USD period is worth 99.90 coins. On the Steam rail it is what that price buys through Steam for the member’s country, floored to whole coins (for example 69 coins for 9.99 USD in the US).
  2. Invo spends the member’s existing wallet balance first.
  3. If the wallet is short, Invo tops it up from the rail behind the subscription (the saved card, or the member’s Steam agreement) for the shortfall only, mints the coins, and then spends the full period amount from the wallet.
  4. The coins spent are split between Invo’s platform fee and your revenue. The fee percentage is your title’s platform fee (10 percent by default; the remainder, 90 percent, is yours). On the card rail a fixed component of 0.30 USD is added to the fee on the card-funded portion of a renewal only; the wallet-funded portion carries the percentage alone. The exact figures are reported on every renewal event in the split block.
  5. Invo settles your revenue to you through your normal partner payout. Invo never pays anyone else on your behalf (see revenue_share, which is attribution only).

The member’s ledger always reads “coins in, coins out”. The wallet is the unit of account; the rail is only ever used for the shortfall. A member who already holds enough balance renews without any card or Steam call at all.

The two clocks

Two different dates live on every subscription and must not be confused.

FieldMeaningExists when
current_period_start / current_period_endThe billing window Invo is currently billing or about to bill. A projection.From the instant the subscription is created, before any money has moved.
paid_throughThe entitlement boundary: the furthest date the member has actually paid for.Only after a period has been charged successfully. null before that.

Grant access from paid_through, never from current_period_end

A subscription created a minute ago has a current_period_end a month out and a paid_through of null until its first charge lands. Code that reads the window as entitlement hands out a month of access for nothing.

Statuses

statusLive?Meaning
trialingyesIn a free trial. Nothing charged yet. The first charge runs when the trial ends.
activeyesBilling normally. The most recent charge succeeded.
past_dueyesA charge failed and Invo is retrying on the dunning schedule. Access is retained through the grace window.
awaiting_authenticationyesA card charge needs the cardholder to authenticate. Nothing has been charged. Not a failure.
pending_steam_authorizationnoSteam only. Created, waiting for the member to authorise the agreement in Steam. Not billable, and does not occupy the one-live-per-item slot.
cancelednoEnded by you, by the member through Steam, or retired at period end after an at-period-end cancel. Terminal.
expirednoEnded by Invo after the retry budget ran out (or, on Steam, after the member never authorised). Terminal.

“Live” means the subscription occupies the member’s one-live-subscription-per-item slot and will be touched by the renewal engine.

State transitions

FromEventTo
(create, no trial)first charge succeeds inlineactive
(create, no trial)first card charge needs authenticationawaiting_authentication
(create, no trial)first charge failspast_due
(create, trial)trialing
trialingtrial ends, first paid period chargesactive (or past_due / awaiting_authentication on failure)
activerenewal succeedsactive (period advanced)
activerenewal failspast_due
activecard needs authenticationawaiting_authentication
past_dueretry succeedsactive
past_dueretry budget exhaustedexpired
awaiting_authenticationmember completes the challengeactive
awaiting_authenticationchallenge lapsesback to ordinary retries (past_due on the next failure)
any livePOST /cancel with at_period_end: false, or nothing paid yetcanceled immediately
any livePOST /cancel with at_period_end: true and a paid periodstays live with cancel_at_period_end: true, then canceled at paid_through
Steam, any livemember cancels the agreement from their Steam accountcanceled (event carries canceled_by: "steam")
pending_steam_authorizationPOST /steam/finalize after the member authorisesactive
pending_steam_authorizationnot authorised within 24 hoursexpired

A paid renewal always lands in active, whatever the subscription was before.

Which rail

funding_railHow the shortfall is fundedCreated byPage
cardThe member’s saved card on file with Invo.POST /api/subscriptions/subscribeCard road
steamThe member’s Steam wallet, through a recurring agreement they authorise once.POST /api/subscriptions/steam/init then POST /api/subscriptions/steam/finalizeSteam road

A subscription’s rail is fixed at creation. A card cannot be attached to a Steam subscription and a Steam agreement cannot be attached to a card subscription.

One item_id per membership, shared across both rails

The one-live-subscription rule is per game, per player, per item_id, and it does not care which rail the subscription is on. So the recurring membership’s item_id must be the same value whether the member subscribes by card or through Steam, and it must be different from the id of any one-off pack you sell (a one-off purchase of “guild-42-membership” and a subscription to it are not the same product).

The guard is 409 ACTIVE_SUBSCRIPTION_EXISTS: a Steam init for an item the member already subscribes to by card (or the reverse) is refused with the live subscription_id in the body. Cancel the old one first, or treat the 409 as “already a member”.

CapabilityCardSteam
Trialsyesno
Intervalsmonth or year, interval_count 1 to 36month or year, interval_count 1 only
Price increasesstaged for the next period (with limits)refused; start a new subscription
Price decreasesstagedstaged
Wallet-only modeyesno
Change the payment methodyesno
Refundsyesnot yet for Steam-charged periods
Coins per periodprice times 10depends on the member’s country, fixed at init
Member can cancel from outside your titlenoyes, from their Steam account

Prerequisites

Your title must be live

Creating a subscription (/subscribe and /steam/init) requires the title to be in live status. A title in testing status is refused with 403 GAME_NOT_LIVE. A testing title may still read, list, cancel, reprice and change the card on existing subscriptions, and may save cards. You set the title to live yourself, from the developer console, in sandbox and in production alike; it is the same action in both, and no review is involved. A live title is what makes subscriptions sellable and transfers receivable, so do it before your first subscription in each environment.

Keys, and which to use where

Every request carries your server-side secret in the X-Game-Secret-Key header. Two kinds of key are accepted in that header:

KeyWhat it isUse it for
Primary game key (ivsdk_...)The key issued with the title. Names no storefront.Everything in this section.
Channel keyA per-storefront key you create for the title (steam, ios, android, web, console).Everything on the card rail. On the Steam rail only the steam channel key (or the primary key) may start a Steam subscription; a key for any other channel is refused 409 STEAM_CHANNEL_REQUIRED.

Both kinds carry a 7-day rotation grace: after you rotate, the previous key keeps working for 7 days. Keys are server-side credentials. Never ship them in a client application, a browser or a mobile app.

HTTPBodyMeaning
401{"message": "X-Game-Secret-Key header is required"}Header missing.
401{"message": "Invalid game secret key"}Unknown, disabled or out-of-grace key.
403{"message": "Game '<name>' is not active"}The title is suspended or otherwise not live or testing.

Base URLs

EnvironmentAPI baseConsole
Sandboxhttps://sandbox.invo.network/sandboxhttps://dev.console.invo.network
Productionhttps://invo.networkhttps://console.invo.network
Production   BASE = https://invo.network
Sandbox      BASE = https://sandbox.invo.network/sandbox

# every path in this section is written against BASE:
POST $BASE/api/subscriptions/subscribe
#   sandbox:     https://sandbox.invo.network/sandbox/api/subscriptions/subscribe
#   production:  https://invo.network/api/subscriptions/subscribe

The sandbox /sandbox prefix is part of the base. A request without it returns a 404 that looks exactly like a wrong path. Each environment has its own keys; never mix them. The sandbox clock tools have one more twist: they live at $BASE/subscriptions/<id>/... with no /api segment. See the sandbox recipe.

Subscribe to webhooks before you go live

Renewals, failures, authentication challenges, cancellations, expiries and refunds are all reported by webhook, and several of them carry data you cannot fetch any other way (for example the confirmation link on subscription.authentication_required). Register a target URL before you create a real subscription. See Webhooks.

Identity: what a member needs

A member is identified by player_email. player_name is required on create and is used only if the player does not exist yet in your title. player_phone is optional. If the phone you supply already belongs to a different identity on the platform, the create is refused with 409 PHONE_SHARE_APPROVAL_REQUIRED and a one-time code is sent to the phone’s current owner; the body carries approval_id, expires_at and next_endpoint for the approval flow.

Where a member comes from

A member exists in your title once any call that carries player_name has run for their email: /subscribe and /steam/init themselves, a currency purchase, a send or a transfer. The two card-capture endpoints (the hosted card page and /setup-intent) identify an existing member and answer 404 PLAYER_NOT_FOUND for an unknown one; they never create players. The order of operations for a brand-new member on the card road is spelled out on the card road.

Your game_id

The webhook registration call is addressed by game_id. Read it from the console on the title, or from any subscription object (subscription.game_id, a string). Once you know it, GET /api/dev/webhooks/games/<game_id> with your key returns the title’s current webhook subscription; the unscoped list GET /api/dev/webhooks/games takes a console session, not the key. Each environment has its own title and therefore its own id.

Rate limits

SurfaceLimitOn 429
POST /subscribe, POST /steam/init30 per hour per (game, player_email); 2000 per minute per gameBody {"error": "rate_limit_exceeded", "message": "...", "retry_after": <seconds>} plus a Retry-After header.
GET /player120 per minute per (game, player_email); 6000 per minute per gameSame.
GET /<subscription_id>6000 per minute per gameSame.
POST .../cancel, .../amount, .../payment-method30 per minute per (game, subscription); 2000 per minute per gameSame.
POST .../refund20 per minute per (game, subscription){"message": "Too many refund requests. Please slow down.", "error_code": "RATE_LIMITED"}
Sandbox clock tools10 per minute per (game, subscription); 60 per minute per IP{"message": "Sandbox clock rate limit exceeded", "error_code": "RATE_LIMITED"}
POST /api/checkout/card-setup-sessions2000 per minute per game key; 120 per minute per IPBody {"error": "rate_limit_exceeded", "message": "...", "retry_after": <seconds>, "limit_type": "rate_limit"} plus a Retry-After header.
POST /api/currency-purchases/setup-intent20 per hour per player_emailMessage only.

Conventions used throughout

  • All timestamps are ISO 8601 with an explicit offset, for example 2026-11-05T14:02:11.482913+00:00.
  • All money and coin figures are JSON strings, never numbers ("9.99", not 9.99), so a parser cannot silently round them.
  • Match on error_code (and on status where the body carries one), never on the human-readable message. Messages are illustrative and may be reworded.
  • “Member” and “player” mean the same person: the end user your title bills. “Partner” and “developer” mean you.
  • Where a shape on these pages disagrees with the API, the API wins and the page has a bug. Tell us.

Clients used by the samples

Every page in this section stacks the same five samples per call: curl, Node with the @invonetwork/web-sdk server client, Node over raw HTTP, Python with the invonetwork package, and Python over raw HTTP. The SDK samples assume the clients below; the raw HTTP samples assume the two helpers below and nothing else. The SDKs are optional: everything on these pages is plain HTTPS with one header, and the raw samples are complete on their own.

npm install @invonetwork/web-sdk     # subscriptions, cards and the clock tools from 3.8.0;
                                     # cards.createSetupSession (the hosted card page) from 3.9.0. Node 18+.
pip install invonetwork              # subscriptions, cards and the clock tools from 3.7.0;
                                     # cards.create_setup_session from 3.8.0. Python 3.9+.

Typed results in the Node package are camelCase (result.firstCharge.paidPeriodSeq); the Python package uses snake_case attributes (result.first_charge.paid_period_seq). Webhook events parsed by either package keep the wire names (event.event_type, event.data.period_seq), because the body is the signed artefact.

Node, @invonetwork/web-sdk
import { InvoServer } from "@invonetwork/web-sdk/server";

const invo = new InvoServer({
  gameSecret: process.env.INVO_GAME_SECRET!,   // server-side only
  baseUrl: process.env.INVO_BASE_URL!,         // https://invo.network  |  https://sandbox.invo.network/sandbox
});
Node, raw HTTP helper
const BASE = process.env.INVO_BASE_URL;            // https://invo.network  |  https://sandbox.invo.network/sandbox
const GAME_SECRET = process.env.INVO_GAME_SECRET;  // server-side only

async function invo(method, path, body) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "Content-Type": "application/json", "X-Game-Secret-Key": GAME_SECRET },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json().catch(() => ({}));
  return { status: res.status, json };
}
Python, invonetwork
import os
from invonetwork import InvoServer

invo = InvoServer(
    game_secret=os.environ["INVO_GAME_SECRET"],   # server-side only
    base_url=os.environ["INVO_BASE_URL"],         # https://invo.network  |  https://sandbox.invo.network/sandbox
)
Python, raw HTTP helper
import os, requests

BASE = os.environ["INVO_BASE_URL"]            # https://invo.network  |  https://sandbox.invo.network/sandbox
GAME_SECRET = os.environ["INVO_GAME_SECRET"]  # server-side only

def invo(method, path, body=None, params=None):
    r = requests.request(method, BASE + path,
                         headers={"Content-Type": "application/json",
                                  "X-Game-Secret-Key": GAME_SECRET},
                         json=body, params=params, timeout=30)
    try:
        return r.status_code, r.json()
    except ValueError:
        return r.status_code, {}

Where to start

Read the card road or the Steam road for your rail, then Webhooks, then run the sandbox recipe end to end. Finish with the checklist before you flip the title live in production.