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, free and paid trials, consent evidence and attribution.
Packages
The thing you sell, managed in your dashboard: name, price, interval and the item_id it owns, with its subscriber counts. Your subscribe call does not change.
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, funding with worked examples on both rails (card: the full price; Steam: wallet first), 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 refund request events, the envelope, the exact signature scheme, retries, dedupe, and which event to trust for what.
Refunds
Refund a billed period, full or partial: the limits (30 days, a daily limit, refund requests), what goes back to the card, the whole refunded amount you bear (Invo’s card fee included), cancellation on a full refund, the receipt and the events. Also what a lost chargeback on a renewal does.
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 card and 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.
| Phase | What you do | Page |
|---|---|---|
| 1. Keys and sandbox | Get 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. Webhooks | Register a target, store the signing secret, verify signatures, fire webhook.test, dedupe on the idempotency key. | Webhooks |
| 3. Player | Know how a new member is created: any call carrying player_name creates them, including the hosted card page mint (/setup-intent does not). | Card road, “New and existing members” |
| 4. Card capture | Mint 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. Subscribe | POST /api/subscriptions/subscribe (card) or /steam/init then /steam/finalize (Steam); branch on first_charge.status. | Card road / Steam road |
| 6. Renewals and dunning | Extend 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. Manage | Get, list, cancel, reprice, change the card. | Manage |
| 8. Refunds | POST .../refund with a reason, branch on status (refunded or a 202 pending_approval), reverse your revenue on subscription.refunded, handle the refund request events. | Refunds |
| 9. Reporting | Windowed attribution statements for your own payout run. | Reporting |
| 10. Errors | Every code every route returns, with the remedy. | Errors |
| 11. Go-live gate | The 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
- 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.99USD period is worth99.90coins. On the Steam rail it is what that price buys through Steam for the member’s country, floored to whole coins (for example69coins for9.99USD in the US). - Invo charges the rail behind the subscription. On the card rail the saved card is charged the full price every period (card only since 2026-09-17); the coins it buys are minted and spent in the same step, so the member’s wallet never pays for a card subscription and never moves. On the Steam rail Invo spends the member’s existing wallet balance first and tops it up from the member’s Steam agreement for the shortfall only.
- The period’s coins are split between Invo’s platform fee and your revenue. The fee on a card charge is your title’s subscription rate plus a fixed amount per charge, set by your tier: 4.5 percent plus
0.30USD on the Open tier (the default) and 5.5 percent plus0.50USD on the Merchant of Record tier (see Tiers). That is a different rate from the fee on item purchases, which is 10 percent, and the two move independently. The fee is charged on the listed price (before tax) of every card charge: on the Open tier a9.99USD period pays Invo0.75and you9.24, and a100.00USD period pays Invo4.80and you95.20; on the Merchant of Record tier the same9.99period pays Invo1.05and you8.94. On a card issued outside the United States, 1.5 percent of the listed price (before tax) is charged to you on top of the fee on either tier (0.15on a9.99period), reported on each renewal assplit.international_card_fee_usd. Fraud screening and the chargeback guarantee are included. These card terms apply to INVO card charges only; purchases through a third-party storefront or payment rail are unaffected. A negotiated subscription rate on your agreement replaces the percentage; the fixed amount stays your tier’s. On the Steam rail the fee is 7 percent of what the coins are worth, with no fixed amount. The exact figures are reported on every renewal event in thesplitblock, whereinvo_fee_percentis the EFFECTIVE percentage including that fixed component, so on small periods it reads higher than the tier rate. Read the amounts rather than the percentage. - 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).
A subscription is cash. A card subscription is paid only by card: a member who holds coins but has no saved card is not charged from the coins, and the charge fails into dunning with failure_code: "no_payment_method". Save a card before you subscribe the member. A paid trial goes further: without a saved card it is refused outright with 400 PAYMENT_METHOD_REQUIRED. wallet_only is deprecated: a wallet_only card subscription can never be paid.
A card renewal also sends purchase.completed for the coins its charge minted, with data.metadata.source == "subscription_renewal". Do not grant currency from it; subscription.renewed is the only entitlement signal.
Who is the seller, and who handles tax
Every developer account is on one of two tiers. The tier decides who is the seller of your subscriptions to the member, and so who is responsible for tax on them. The subscription API, the events and the billing clock are the same on both. The full comparison is on Tiers.
| Where the member pays | Who is the seller | Tax on the subscription | Subscription fee |
|---|---|---|---|
| Card rail, Open tier (the default for every account) | You, the developer. Invo runs the billing and pays you out. | Yours. Invo adds no tax to the charge and collects none. You are responsible for any tax on your subscription sales. | 4.5 percent plus 0.30 USD per card charge, plus 1.5 percent on a card issued outside the US |
| Card rail, Merchant of Record tier (by application) | Invo. | Invo handles consumption tax on the sale. | 5.5 percent plus 0.50 USD per card charge, plus 1.5 percent on a card issued outside the US (the full schedule is on Tiers) |
| Steam rail (either tier) | Steam, the storefront. | Steam’s. Steam prices include VAT where it applies, which is why a Steam period is worth fewer coins in some countries. | 7 percent, on what the coins are worth, with no fixed component |
Your tier is set per developer account, not per title. Whatever the tier, the split block on each renewal carries the fee that was actually taken; read it rather than recomputing it.
On the Merchant of Record tier Invo also issues a receipt for every card charge and a refund receipt for every card refund, emailed to the member when their email is known. The number and link reach you as receipt_number and receipt_url on subscription.renewed, subscription.refunded (the refund receipt) and a paid first_charge; they are absent on the Open tier and on Steam. And a chargeback reserve holds part of your share of each card charge (10 percent for 90 days by default) before it becomes available to withdraw; the split on the event is unchanged. See receipts and the reserve.
The two clocks
Two different dates live on every subscription and must not be confused.
| Field | Meaning | Exists when |
|---|---|---|
current_period_start / current_period_end | The 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_through | The 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
status | Live? | Meaning |
|---|---|---|
trialing | yes | In a trial. On a free trial nothing has been charged and the first charge runs when the trial ends. On a paid trial (trial_amount_usd) the trial price was charged at creation and the regular price is charged when the trial ends. |
active | yes | Billing normally. The most recent charge succeeded. |
past_due | yes | A charge failed and Invo is retrying on the dunning schedule. Access is retained through the grace window. |
awaiting_authentication | yes | A card charge needs the cardholder to authenticate. Nothing has been charged. Not a failure. |
pending_steam_authorization | no | Steam only. Created, waiting for the member to authorise the agreement in Steam. Not billable, and does not occupy the one-live-per-item slot. |
canceled | no | Ended by you, by the member through Steam, or retired at period end after an at-period-end cancel. Terminal. |
expired | no | Ended by Invo after the retry budget ran out, after a paid trial’s own charge was refused, 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
| From | Event | To |
|---|---|---|
| (create, no trial) | first charge succeeds inline | active |
| (create, no trial) | first card charge needs authentication | awaiting_authentication |
| (create, no trial) | first charge fails | past_due |
| (create, free trial) | nothing is charged | trialing |
| (create, paid trial) | trial charge succeeds inline | trialing (period 1 paid) |
| (create, paid trial) | trial charge needs authentication | awaiting_authentication (back to trialing when completed before trial_end) |
| (create, paid trial) | trial charge refused (declined, or no chargeable card) | expired immediately |
| (create, paid trial) | trial charge hits a temporary error | past_due (retried on the normal schedule) |
trialing | trial ends, first paid period charges | active (or past_due / awaiting_authentication on failure) |
active | renewal succeeds | active (period advanced) |
active | renewal fails | past_due |
active | card needs authentication | awaiting_authentication |
past_due | retry succeeds | active |
past_due | retry budget exhausted | expired |
awaiting_authentication | member completes the challenge | active |
awaiting_authentication | challenge lapses | back to ordinary retries (past_due on the next failure) |
| any live | POST /cancel with at_period_end: false, or nothing paid yet | canceled immediately |
| any live | POST /cancel with at_period_end: true and a paid period | stays live with cancel_at_period_end: true, then canceled at paid_through |
| Steam, any live | member cancels the agreement from their Steam account | canceled (event carries canceled_by: "steam") |
pending_steam_authorization | POST /steam/finalize after the member authorises | active |
pending_steam_authorization | not authorised within 24 hours | expired |
A paid renewal lands in active, whatever the subscription was before, with one exception: the trial charge of a paid trial keeps the subscription trialing while the trial window is still running. active arrives when period 2, the first regular-price period, is paid.
Which rail
funding_rail | Who pays each period | Created by | Page |
|---|---|---|---|
card | The member’s saved card on file with Invo, charged the full price every period. | POST /api/subscriptions/subscribe | Card road |
steam | The member’s Steam wallet, through a recurring agreement they authorise once. The Invo wallet is spent first; Steam tops up the shortfall. | POST /api/subscriptions/steam/init then POST /api/subscriptions/steam/finalize | Steam 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”.
| Capability | Card | Steam |
|---|---|---|
| Trials | yes, free or paid (trial_amount_usd) | no |
| Who is the seller | you on the Open tier; Invo on the Merchant of Record tier (Tiers) | Steam |
| Intervals | month or year, interval_count 1 to 36 | month or year, interval_count 1 only |
| Price increases | staged for the next period (with limits) | refused; start a new subscription |
| Price decreases | staged | staged |
| Wallet pays the period | never (card only; wallet_only is deprecated and can never be paid) | yes, first; Steam tops up the shortfall |
| Change the payment method | yes | no |
| Refunds | yes, in cash (partial included), within 30 days and a daily limit | not yet for Steam-charged periods |
| Coins per period | price times 10 | depends on the member’s country, fixed at init |
| Member can cancel from outside your title | no | yes, 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:
| Key | What it is | Use it for |
|---|---|---|
Primary game key (ivsdk_...) | The key issued with the title. Names no storefront. | Everything in this section. |
| Channel key | A 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.
| HTTP | Body | Meaning |
|---|---|---|
| 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
| Environment | API base | Console |
|---|---|---|
| Sandbox | https://sandbox.invo.network/sandbox | https://dev.console.invo.network |
| Production | https://invo.network | https://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, refunds and refund requests 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, the hosted card page mint (/api/checkout/card-setup-sessions), a currency purchase, a send or a transfer. Without player_name the hosted card page answers 404 PLAYER_NOT_FOUND for an unknown member; /setup-intent never creates 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
| Surface | Limit | On 429 |
|---|---|---|
POST /subscribe, POST /steam/init | 30 per hour per (game, player_email); 2000 per minute per game | Body {"error": "rate_limit_exceeded", "message": "...", "retry_after": <seconds>} plus a Retry-After header. |
GET /player | 120 per minute per (game, player_email); 6000 per minute per game | Same. |
GET /<subscription_id> | 6000 per minute per game | Same. |
POST .../cancel, .../amount, .../payment-method | 30 per minute per (game, subscription); 2000 per minute per game | Same. |
POST .../refund | 20 per minute per (game, subscription) | {"message": "Too many refund requests. Please slow down.", "error_code": "RATE_LIMITED"} |
| Sandbox clock tools | 10 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-sessions | 2000 per minute per game key; 120 per minute per IP | Body {"error": "rate_limit_exceeded", "message": "...", "retry_after": <seconds>, "limit_type": "rate_limit"} plus a Retry-After header. |
POST /api/currency-purchases/setup-intent | 20 per hour per player_email | Message 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", not9.99), so a parser cannot silently round them. - Match on
error_code(and onstatuswhere the body carries one), never on the human-readablemessage. 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.
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
});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 };
}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
)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.