FAQ / Subscriptions

Subscriptions: Questions and Answers

Packages, the card road and the platform store road, managing, renewals, webhooks, refunds, sandbox and reporting. 129 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.

Subscriptions overview

Read the Subscriptions overview page

How does a card subscription period differ from a platform store rail period, and which wallet pays first on the platform store rail?

On the card rail the saved card is charged the full price every period and the period is worth price times 10 coins (9.99 USD = 99.90 coins); the coins are minted and spent in the same step, so the member's wallet never pays and never moves. On the platform store rail a period is worth what the price buys through the storefront for the member's country (fixed at init), and from period 2 the member's existing INVO wallet balance is spent first, with the storefront agreement topping up only the shortfall. Period 1 on the platform store rail always charges the full price, because that charge creates the agreement.

Why should access be granted from paid_through rather than current_period_end, and when is paid_through null?

current_period_start/current_period_end are a projection of the window Invo is billing or about to bill and exist from the moment the subscription is created, before any money moves; paid_through is the furthest date the member has actually paid for. paid_through is null until a period has been charged successfully: a free trial, a first charge that is requires_action, failed or pending, a pending platform store rail authorisation, or a paid trial whose trial charge was refused. Granting from current_period_end would hand out a month of access for nothing.

Which statuses count as live for the one-live-subscription-per-item rule?

trialing, active, past_due and awaiting_authentication. pending_steam_authorization, canceled and expired are not live, so a pending platform store rail row never occupies the slot.

How does a paid trial differ from a free trial, and what happens if the trial charge is refused?

A free trial (trial_days or trial_end) charges nothing for period 1 and returns first_charge.status: "skipped_trial"; a paid trial adds trial_amount_usd, which is charged inside /subscribe as period 1, so the member must already have a saved card (otherwise 400 PAYMENT_METHOD_REQUIRED and nothing is created). In both cases amount_usd is charged as period 2 at trial_end. If the card refuses the trial charge (declined, or no chargeable card), the subscription ends at once as expired with no retries: you get subscription.payment_failed (retries_remaining: 0) then subscription.expired with expire_cause: "trial_payment_failed"; a temporary processing error instead goes to past_due and is retried on the normal ladder.

What does 409 ACTIVE_SUBSCRIPTION_EXISTS guard against, and why must item_id match across the card and platform store rails?

It stops a member being billed twice for one membership: the rule is one live subscription per game, per player, per item_id, regardless of rail, and the 409 body carries the live subscription_id. If the card and platform store rail versions of a membership used different item_ids, the guard could not see the duplicate and the member could hold and pay for both. The membership item_id must also differ from any one-off pack id you sell.

What fees does Invo take on an Open card charge versus a Merchant of Record charge?

On the card rail the subscription fee is 3.5% plus 0.15 USD per charge on the Open tier and 5.0% plus 0.15 USD per charge on the Merchant of Record tier, measured on the listed price before tax; a card issued outside the US adds 1.5% of the listed price on either tier. On Open a 9.99 USD period pays Invo 0.50 and you 9.49; on Merchant of Record it pays Invo 0.65 and you 9.34. Fraud screening is included on both tiers; the chargeback guarantee is Merchant of Record only (Open-tier charges made before 2026-10-21 keep it). A negotiated rate on your agreement replaces the percentage; the fixed amount stays your tier's.

Why does a 9.99 USD platform store rail period give 69 coins in the US?

On the platform store rail the period is worth what the price actually yields after the storefront's share and any VAT for the member's country, converted at 10 coins per USD and floored to whole coins: 9.99 USD nets about 6.90 USD in the US, which is 69 coins (58 for a member in Germany). The figure is fixed at init and returned as amount_coins; show it to the member and never compute it as price times 10.

Which webhooks should extend access, trigger dunning messages, or revoke access?

Extend access only on subscription.renewed. Message the member (fix your card, or here is your confirmation link) on subscription.payment_failed, subscription.past_due and subscription.authentication_required, without revoking. Revoke only on subscription.expired (at final_period_end) and subscription.canceled (at access_until).

How does the 2, 3, 2 day retry ladder work, and when does past_due become expired?

After the first failed charge the subscription goes past_due and is retried 2 days later, then 3 days after that, then 2 days after that: three retries over seven days, with access retained throughout (grace_period_end on the event). If the third retry also fails, the subscription becomes expired and you receive a final subscription.payment_failed (retries_remaining: 0) followed by subscription.expired. Any successful retry returns it to active. 2, 3, 2 is the default; Invo can set a different schedule for a title.

How do cancel-now and cancel-at-period-end differ, and what happens when a member cancels from their platform store account?

POST /api/subscriptions/{id}/cancel with at_period_end: true (the default) keeps the subscription live with cancel_at_period_end: true and retires it at paid_through without another charge; if nothing has been paid yet it is downgraded to immediate (terminated_immediately: true, downgrade_reason: "NO_PAID_PERIOD"). at_period_end: false ends it now, with access_until set to paid_through if that is in the future. A member who ends the agreement from their platform store account produces subscription.canceled with canceled_by: "steam", cancel_at_period_end: false, and access running to access_until; no further charge is made.

Why does /subscribe refuse a title in testing, and what can a testing title still do?

Creating a subscription (/subscribe and /steam/init) requires the title to be live, and a testing title gets 403 GAME_NOT_LIVE, so no one can be enrolled in recurring billing on a title you have not released. A testing title can still read, list, cancel, reprice and change the card on existing subscriptions, and can save cards. You set the title live yourself in the developer console, in sandbox and production alike, with no review.

What are the rate limits on /subscribe and /steam/init?

30 requests per hour per (game, player_email) and 2000 per minute per game. A 429 returns {"error": "rate_limit_exceeded", "message": "...", "retry_after": <seconds>} plus a Retry-After header.

What can and cannot be changed on a platform store rail subscription?

You can cancel it (now or at period end) and stage a price decrease through /amount. You cannot raise the price (409 STEAM_REAUTHORIZATION_REQUIRED; start a new subscription at the new price and cancel the old one), attach or change a card (409 NOT_A_CARD_SUBSCRIPTION), set wallet_only, or add a trial, and the interval and interval_count (always 1) are fixed. The rail itself is fixed at creation.

Subscription Packages

Read the Subscription Packages page

Can my server create packages through the API, or only in the dashboard?

Only through the developer dashboard. Package management (/api/dev/subscriptions/plans) is authenticated by your console sign-in, not by X-Game-Secret-Key, and the server SDKs do not wrap it, so a leaked game key cannot reprice what members pay. Your server's subscribe call is unchanged.

Why does a package need its own item_id, and who supplies it?

You supply it; Invo never mints it. The item_id is your own entitlement handle, the one your server already sends on /subscribe and receives on every lifecycle webhook, and a package is the record that describes it. Members are counted by title and item_id, which is why a package must own one.

What happens if I create a package on an item_id a live package already uses?

It is refused with ITEM_ID_IN_USE, and the refusal names the package that holds it. Two live packages on one handle would be mutually exclusive for a member, because a member can hold at most one live subscription per title, player and item_id. The rule is per title; two titles may use the same handle. (If a closed package on that handle still has live members, creation is refused with ITEM_ID_HAS_SUBSCRIBERS instead.)

If I create a package over an item_id with 400 existing subscribers, how many will it report?

It adopts them immediately with no linking step: live_subscriber_count reports all 400 (every live member, including trials, past due, awaiting authentication and those cancelling at period end). subscriber_count reports only those of the 400 who are active and not set to cancel, so it can be lower. Nothing about those subscriptions changes; they keep their own price, interval and renewal date.

What is the difference between subscriber_count and live_subscriber_count?

subscriber_count (basis active) counts members being charged: active and not cancelling at period end; it is the revenue basis and matches the monthly figure beside it. live_subscriber_count (basis live) counts every member the billing system still holds: trialing, active, past_due, awaiting_authentication, including those cancelling at period end. The live count is always at least the paying count, so a package whose members are all on trial reads subscriber_count: 0 and a non-zero live_subscriber_count.

What happens if I change a package price without saying whom it applies to?

The change is refused with 400 APPLIES_TO_REQUIRED; there is no default. Send applies_to: "new_only" (new subscribers only; existing members keep their price). Renaming or re-describing a package, or re-saving it with the same price, is not a price change and needs no applies_to.

Why is the "existing subscribers too" price option refused right now?

Moving existing members to a new price at their next renewal (applies_to: "existing_next_period") is not built yet, so it is refused with 409 APPLIES_TO_NOT_YET_SUPPORTED rather than half-applied. Only new_only runs today. To move an individual existing member, stage the price on that subscription with POST /api/subscriptions/{id}/amount.

Does closing a package cancel or reprice existing subscribers?

No. Closing (retiring) stops new subscriptions only; every current member keeps renewing at the price they are on, because each subscription carries its own price, interval and item and billing never reads the package. A package with members can be closed but never deleted, closing is idempotent (already_retired), and the response reports how many members were left untouched. A closed package cannot be repriced (409 PLAN_RETIRED) until you reopen it.

What happens if I reopen a closed package after creating a replacement on the same item_id?

Reopening is refused with ITEM_ID_IN_USE, naming the package that now holds the handle, because two live packages cannot share one item_id. Close the replacement first if you want the original back.

Card Subscriptions

Read the Card Subscriptions page

Which endpoint mints a card-setup link, and how long is card_setup_url valid?

POST /api/checkout/card-setup-sessions (server to server, X-Game-Secret-Key) returns session_id, card_setup_url and expires_at. The session lives 10 minutes, so mint it when the member is ready to enter a card; an expired link tells the member to ask for a new one. Use the URL exactly as returned and open it top-level, never framed.

Can the hosted card page and the payment confirmation page show our logo?

Yes. The logo you upload in the console under Game settings, Checkout branding replaces the INVO logo on the hosted card page (card_setup_url) and on the subscription payment confirmation page, as it does on hosted checkout. INVO's logo is the default, and a small "Secured by INVO" line always stays at the bottom. There is no API call for it.

Why does the hosted card page need player_name for a new member?

Every card belongs to a member of your title, and the hosted card page only creates a member it has never seen when you send player_name (and optionally player_phone), exactly as /subscribe would. Without it an unknown member gets 404 PLAYER_NOT_FOUND and nothing is created. An existing member is used as is and never renamed.

How do /setup-intent and the hosted card page differ, and why can /setup-intent never create a player?

The hosted card page needs no card code on your side: two server calls and a redirect, and it is not tied to how Invo processes cards. POST /api/currency-purchases/setup-intent is for partners who already run their own card form; it returns a client_secret your client-side form must confirm, so it is bound to the current card processor. /setup-intent only identifies an existing member and saves a card (idempotent on setup_reference); it takes no player_name, phone, item or amount, so it has nothing to create a player from and answers 404 PLAYER_NOT_FOUND for an unknown member.

When player_card_id is omitted on /subscribe, which card is used?

Invo attaches the member's newest unexpired saved card for this title. If they have none, the subscription is still created without a card (card: null, has_payment_method: false) and its first charge fails with failure_code: "no_payment_method", whatever coins the member holds; a card saved later is adopted automatically at the next charge attempt. The exception is a paid trial, which is refused with 400 PAYMENT_METHOD_REQUIRED.

What does /subscribe return for a paid trial when the member has no usable card?

400 PAYMENT_METHOD_REQUIRED, and nothing is created. Save the card first (for a brand-new member, the hosted card page with player_name), then subscribe, optionally naming it with player_card_id.

What does first_charge.status: "requires_action" mean, and what do I do with confirmation_url?

The card issuer wants the cardholder to authenticate; nothing has been charged and the subscription is awaiting_authentication, so do not grant paid access. Relay confirmation_url (an Invo-hosted page) to the member by email or in-app before expires_at; when they complete it you receive subscription.renewed. The link is a bearer link: do not frame it or log it next to identifiers you publish, and note that a replay of /subscribe cannot rebuild it (it is also on subscription.authentication_required).

How should I handle first_charge failed when the subscription is expired versus past_due?

past_due means the subscription is in dunning: tell the member to fix their card, keep access, and Invo retries at next_retry_at (you also get subscription.payment_failed and subscription.past_due). expired with next_retry_at: null only happens when a paid trial's own charge was refused: the subscription never started, will not be retried, and the item is free again, so subscribe the member again with a new client_request_id once they have a working card.

Why should entitlement be keyed on first_charge.paid_period_seq?

After a paid first charge subscription.period_seq already points at the next window (it reads 2), while first_charge.paid_period_seq (1, or 2 when a replay lands after a free trial converted) names the period actually paid. The matching subscription.renewed reports that same number as period_seq, so keying on it lets you join the response and the event without double-granting or skipping a period.

What does a replayed client_request_id return, and what can a replay not rebuild?

HTTP 200 with the same body shape as the 201 and idempotent_replay: true; nothing is charged again and first_charge is re-derived from what actually happened (so a replay after settlement reports paid). A replay cannot rebuild a step-up confirmation_url (replay the subscription.authentication_required webhook delivery instead) and cannot change terms.

What do mismatched_fields on IDEMPOTENT_REPLAY_MISMATCH tell me?

You reused a client_request_id for the same player and item with different material terms, and mismatched_fields lists which: any of amount_usd, interval, interval_count, wallet_only, funding_rail, trial_amount_usd (an omitted trial price counts as a difference). To change terms on the existing subscription use /amount or /payment-method; to create a new one, use a new key.

What happens at trial_end on a paid trial?

Invo charges the regular amount_usd as period 2; on success you get subscription.renewed with period_seq: 2 and is_trial: false, and the subscription moves to active. Dunning and card authentication apply from there as for any renewal. The step from trial price to regular price is not a price change, so nothing is staged and the more-than-double rule does not apply.

How does a card saved without off-session consent affect a subscription?

It cannot back a subscription. Cards saved through saved_card_id purchases, or saved before the consent change, lack that consent; the member must save the card again through the hosted card page, /setup-intent, or a currency purchase of a new card with save_card: true.

Platform Store Subscriptions

Read the Platform Store Subscriptions page

What status does POST /api/subscriptions/steam/init give a new subscription, and is anything charged?

The subscription is created in pending_steam_authorization (HTTP 201) and nothing is charged: first_charge.status is pending_authorization. The charge for period 1 happens only at /steam/finalize, after the member authorises the agreement.

Which fields are refused on the platform store rail init?

wallet_only (409 WALLET_ONLY_SUBSCRIPTION), player_card_id/card_id (400 CARD_FIELDS_NOT_ALLOWED), trial_amount_usd (400 TRIAL_AMOUNT_NOT_SUPPORTED_FOR_FUNDING, checked first), trial_days/trial_end (400 TRIAL_NOT_SUPPORTED_ON_STEAM), and any interval_count other than 1 (400 STEAM_INTERVAL_COUNT_UNSUPPORTED). steamid is required (400 STEAMID_REQUIRED).

How do usersession client and web differ?

client (the default) presents the authorisation in the in-game overlay, so there is nothing for you to open; call finalize when the overlay closes. web returns a steam_checkout_url you open in the member's browser as a top-level window (it cannot be framed), and requires player_ip (400 MISSING_PLAYER_IP otherwise).

What should I store from the init response, and how does it link to the client callback?

Store steam_order_id (a string holding an unsigned 64-bit integer) against subscription_id before you return. The client callback your game receives when the overlay closes carries an order_id equal to that value, which is how you find the subscription to finalize. steam_transid is a reference for support tooling only; never match on it. Also show the member amount_coins.

When does a second init return pending_reuse: true, and when STEAM_AUTHORIZATION_PENDING?

While a row for the same player and item is still pending (under 24 hours old), a second init with the same amount_usd, interval, interval_count, steamid and usersession, and an open authorisation, returns that row as HTTP 200 with pending_reuse: true and the same steam_order_id; no new agreement is opened, even under a new client_request_id. Different terms, a different account, the other usersession, or an authorisation that has not finished opening returns 409 STEAM_AUTHORIZATION_PENDING with the pending subscription_id; finalize, cancel or wait out that row first.

When should I call /steam/finalize, and what do not_authorized and NO_STEAM_AUTHORIZATION mean?

Call POST /api/subscriptions/steam/finalize with {"subscription_id": "..."} after the overlay or web checkout closes; the storefront does not tell Invo the member said yes, so this call is yours. 409 with status: "not_authorized" means the member has not finished authorising yet: retry after they have. 409 NO_STEAM_AUTHORIZATION means there is no open authorisation left to finalize: start a new subscription.

Why does period_seq read 2 after finalize while subscription.renewed reports 1?

subscription.period_seq always points at the next window, so once period 1 is paid it reads 2; the subscription.renewed event reports the period just paid (period_seq: 1), matching first_charge.paid_period_seq on the finalize response. Key entitlement on the paid period, never on subscription.period_seq; the card rail behaves the same way.

How long does a member have to authorise?

24 hours from init. After that the row expires with subscription.expired, failure_code: "steam_authorization_abandoned" and final_period_end: null, and you must start again with a new client_request_id.

How is a later platform store rail period charged when the wallet is empty, partly funded or full?

Empty wallet: the storefront is charged the full advertised price. Partly funded: the storefront is charged the smallest amount that buys the missing coins, never above the price and never below the 1.00 USD minimum top-up, and any coins above the shortfall stay in the wallet. Full: no storefront charge at all that period. Each appears as funding.steam_charged_usd on subscription.renewed.

What happens on a platform store rail price increase (STEAM_REAUTHORIZATION_REQUIRED)?

POST /api/subscriptions/{id}/amount with a higher price is refused with 409 STEAM_REAUTHORIZATION_REQUIRED, carrying current_amount_usd and requested_amount_usd, because the member authorised the current price as a ceiling. Start a new platform store rail subscription at the new price and cancel this one; a decrease stages normally.

Which features are unavailable on the platform store rail (trials, interval_count other than 1, wallet_only, card fields, refunds)?

No trials, free or paid; monthly or yearly with interval_count 1 only; no wallet_only; no card fields and no /payment-method (409 NOT_A_CARD_SUBSCRIPTION); no price increases. Refunds of periods charged through the storefront are not supported yet (409 STEAM_REFUND_NOT_SUPPORTED, refused before anything moves); a period the wallet covered entirely can be refunded in coins.

Does sandbox exercise the storefront's side of the platform store rail?

No. In sandbox /steam/finalize auto-approves, seeds a stand-in agreement and runs the real Invo settlement, and later renewals treat the storefront charge as captured; the response carries sandbox_auto_approved: true. That proves Invo's currency path (coins, split, events, dunning) but not the storefront charge, so do at least one real authorisation against your title's storefront sandbox before launch.

Manage a Subscription

Read the Manage a Subscription page

What does GET /api/subscriptions/{id} return, and what does include_player add?

{"status": "success", "subscription": {...}} with the full subscription object (status, price, pending price, interval, item, billing window, period_seq, next_charge_at, trial fields, funding_rail, has_payment_method, metadata, consent, revenue share, paid_through and timestamps). include_player=true adds a player block. An id that is not your title's returns 404 SUBSCRIPTION_NOT_FOUND; a malformed id returns 400 INVALID_FIELD_VALUE.

Which query parameters does GET /api/subscriptions/player accept, and what does status=live expand to?

player_email (required), status (comma list), include_player (boolean), limit (1 to 100, default 50) and offset (0 to 100000, default 0). live expands to trialing, active, past_due and awaiting_authentication, and can be mixed with other values (status=live,canceled); unknown values return 400 STATUS_INVALID. Results are newest first with a stable tiebreaker.

Why does a player with no subscriptions return an empty list instead of 404?

The list answers "what does this email hold in your title", and for a member who has nothing (or who does not exist yet) the true answer is an empty list with total_count: 0. Returning 404 would make an ordinary empty state look like an error, and would also reveal whether an email is known.

How do paid_through, current_period_end and next_charge_at differ?

paid_through is the entitlement boundary (the furthest date actually paid, null until a charge succeeds). current_period_end is the end of the billing window Invo is currently billing or about to bill, a projection rather than entitlement. next_charge_at is when Invo next acts on the subscription: the next renewal, the next retry in dunning, or the challenge deadline in awaiting_authentication; it is null on terminal rows and on pending_steam_authorization.

What does pending_amount_usd mean?

It is a price change staged through /amount that has not been applied yet; it applies on the next successful renewal after the current window is billed at the old price. It is null when nothing is staged, and it can stay set indefinitely if the new price is more than double the price the member last agreed to or above the 25000.00 USD per-charge ceiling.

What do terminated_immediately and access_until mean on a cancel response, and what is NO_PAID_PERIOD?

access_until is when to revoke access: paid_through for an at-period-end cancel, or for an immediate cancel paid_through if it is in the future, else now. terminated_immediately: true with downgrade_reason: "NO_PAID_PERIOD" means you asked for an at-period-end cancel but nothing had been paid (or the paid window had already elapsed), so there was no paid access to preserve and the subscription ended at once. Read both on every cancel response.

How does POST /amount stage a price change, and what is the 25000.00 ceiling?

The new amount_usd is stored in pending_amount_usd and applied on the next successful renewal, with no proration; the response gives applies_from (null until something is paid) and applies_from_period_seq, and sending the current price clears the staged change. 25000.00 USD is the most Invo will charge in a single subscription charge: a price above it, or an increase to more than double the last agreed price, is staged but never applied, and the old price keeps billing. The ceiling is per charge, so a 500 USD per month plan billed annually (one 6,000 USD charge) is fine.

What does POST /payment-method require, and what errors occur for an expired or foreign card?

A body with player_card_id (alias card_id), plus wallet_only: false if the subscription is wallet-only; the card must belong to this player in this title and be unexpired. An expired card returns 400 PAYMENT_METHOD_EXPIRED; a card that is not this player's in this title returns 404 PAYMENT_METHOD_NOT_FOUND; a malformed id returns 400 PAYMENT_METHOD_INVALID and a missing one 400 PAYMENT_METHOD_REQUIRED. On the platform store rail it returns 409 NOT_A_CARD_SUBSCRIPTION.

When does Invo attach a newer saved card automatically?

At each charge attempt, only when the subscription has no usable card: none attached, or the attached card has been removed, has expired, or can no longer be charged. Invo then picks the member's newest unexpired saved card for that title, charges it and records it, so the next read names it. A valid card you chose is never replaced by a newer one; use /payment-method to switch.

Which operations return 503 during a maintenance pause, and which still work?

Create (/subscribe, /steam/init), /amount, /payment-method and card setup return 503 with error: "flow_paused"; refunds and the sandbox clock tools return 503 FLOW_PAUSED. Cancel is never blocked, and reads and lists keep working. Retry paused calls later with the same idempotency key.

Renewals and Dunning

Read the Renewals and Dunning page

When is each period charged relative to next_charge_at, and why can a renewal run up to an hour after the original signup time?

Billing is in advance: each later period is charged at next_charge_at (the end of the period just paid), and the renewal engine runs every minute, so a due subscription is picked up within a minute or two. At creation Invo adds up to one hour of random offset to the first period's end and carries it forward, so members who subscribed in the same minute renew spread across an hour rather than all at once.

If Invo falls behind several periods, are they back-billed?

No. Invo charges at most one period per subscription per run, and whole periods that elapsed while Invo was behind are recorded as forgiven, never charged. You see the gap as a jump in period_seq on the next subscription.renewed.

How much is charged each period on a card subscription, and does the wallet change?

The card is charged the full price every period. The coins it buys are minted and spent in the same step, so the member's wallet never pays for a card subscription and its balance does not change (funding.balance_applied_coins: "0.00", card_charged_usd equal to the price, new_balance unchanged).

What do the no_payment_method and wallet_negative failures mean?

no_payment_method means the member has no saved card at all, so nothing could be charged whatever coins they hold; a card they save before the next retry is adopted automatically. wallet_negative means the member's coin balance is below zero (which only a refund of coins they had already spent can cause), so nothing was charged; the retry succeeds once the balance is back to zero or above.

How is a platform store rail top-up calculated when the wallet holds some coins, and what is the minimum charge?

Invo spends the wallet first and charges the storefront the smallest amount that buys the missing coins for the member's country, never above the full price and never below the 1.00 USD minimum top-up; any coins left over stay in the wallet. Example: 9.99 USD (69 coins in the US) with 20 coins in the wallet charges 7.00 USD for the missing 49.

What is the retry schedule after a failed charge, and what are status and next_charge_at at each step?

Failure: past_due, next_charge_at = now + 2 days. Retry 1 fails: past_due, + 3 days. Retry 2 fails: past_due, + 2 days. Retry 3 fails: expired, ended_at set, next_charge_at null. Any successful retry returns the subscription to active with the period advanced.

When should access be revoked relative to final_period_end?

At final_period_end on subscription.expired, which is the last instant the member paid for, not at ended_at. It is null when nothing was ever collected (an expiry on period 1, or a platform store rail row never authorised), which means revoke now.

Why does a declined paid-trial charge expire immediately with expire_cause: trial_payment_failed?

A paid trial's own charge is the member's first commitment, and a card refusal there means the subscription never started, so there is no paid access to protect with a retry ladder. Invo ends it at once as expired (subscription.payment_failed with retries_remaining: 0, then subscription.expired with expire_cause: "trial_payment_failed") and frees the item for a fresh subscription. A temporary error, or wallet_negative, goes through the ordinary ladder instead.

What happens in awaiting_authentication, and how long does the confirmation link last?

Nothing is charged, no retry is consumed, access is retained, and next_charge_at moves to the challenge deadline; Invo sends subscription.authentication_required with confirmation_url and expires_at. The link lives at most 72 hours and never past the next scheduled charge; if it lapses, Invo tries the card again and ordinary dunning resumes. A card that asks for authentication on more than two consecutive attempts for the same period is treated as declined.

Why might next_charge_at move a day at a time with no event?

That is a deferral, not a failure: no event is sent, no retry is consumed and access is unaffected. Today it has two causes: the price is above the 25000.00 USD per-charge ceiling (never charged), or, on the platform store rail, Invo cannot issue currency for your title (billing setup incomplete, rail suspended, credit unavailable) or the title's storefront configuration is unusable. Check the price, then your billing setup in the console.

How does the per-charge ceiling apply to annual plans?

The 25000.00 USD ceiling is measured per charge, so an annual plan is measured on the whole annual amount: 500 USD per month billed annually is one 6,000 USD charge and passes, while a plan whose single charge exceeds 25000.00 is never charged.

What does a settlement_error payment_failed mean when the card was already charged?

The card charge succeeded but Invo could not complete the renewal in the same step. The retry finishes the period without charging the card again, and subscription.renewed follows; do not ask the member to pay again.

Subscription Webhooks

Read the Subscription Webhooks page

Is there a subscription.created event, and which event is the first signal?

No; there is also no subscription.amount_changed. The /subscribe (or /steam/finalize) response is your creation signal, and the first webhook is subscription.renewed for the first paid period.

Does a free trial send subscription.renewed for period 1?

No. On a free trial nothing is charged for period 1, so the first subscription.renewed is for period 2 at trial_end. A paid trial does send one for period 1, with is_trial: true and the trial price.

How do I find the period just paid on subscription.renewed?

Read the event's period_seq (the period just paid) together with period_start and period_end; period_end equals the new paid_through. The current_period_* and next_charge_at fields on the event describe the next window.

Which fields on subscription.canceled and subscription.expired set the revocation time?

On subscription.canceled, revoke at access_until (null when no paid period remains; a past value means revoke now). On subscription.expired, revoke at final_period_end (null if nothing was ever paid), not at ended_at.

Why should payment_failed, past_due and authentication_required never revoke access?

They fire while the member is still entitled: dunning keeps access through the whole grace window, and an authentication challenge is not a failure at all (access_retained: true on all three). Only subscription.expired or subscription.canceled means the membership has ended; revoking earlier cuts off members who fix their card during the window.

How do I use the offline test vector to check my verifier?

Feed your verifier the secret whsec_test_0123456789abcdef, the header X-Invo-Signature: t=1789257600,v1=e5b42e347454600d1ed5cd6e4b9bb4a9efdd3c1de6f8318076078e673e249965 and the exact body from the webhooks page (no trailing newline), with the 300-second timestamp check disabled or the clock pinned to 1789257600; it must accept. The signature is lowercase hex HMAC-SHA256 of "<t>." + raw_body with your secret. Do this before registering anything.

How do I calculate partner earnings from split.partner_revenue_usd without subtracting the international fee twice?

Take split.partner_revenue_usd (the gross after Invo's fee) and subtract split.international_card_fee_usd once. split.pass_through_fees_usd carries the same figure under its older name, so never subtract both. Then subtract refund.partner_debited_usd for any refund and partner_debited_usd for any lost chargeback. Example: a 9.99 USD Open-tier period on a non-US card is 9.49 minus 0.15 = 9.34.

How do refund_requested, refund_approved and refund_rejected differ?

subscription.refund_requested fires when a refund over your daily self-serve limit becomes a request (status: "pending", nothing moved). subscription.refund_approved (status: "approved", decided_at, transaction_id) means the refund ran, and arrives with subscription.refunded (and subscription.canceled on a full refund). subscription.refund_rejected (status: "rejected", decided_at, decision_reason) means nothing moved, and that client_request_id now returns 409 REFUND_REQUEST_REJECTED.

What is the order of subscription.refunded and subscription.canceled on a full refund?

There is no guaranteed order: both are sent in the same step and may arrive either way round. subscription.canceled carries cancel_cause: "full_refund", and subscription.refunded carries subscription_canceled: true.

How do I account for a lost chargeback on purchase.disputed?

When order_type is "subscription_renewal" and dispute_status is "lost", subtract partner_debited_usd (your share reversed plus Invo's card fee on that period, card_fee_charged_to_partner_usd) for that subscription_id and period_seq; it is 0.00 when guarantee_covered is true. Key your books on dispute_id so it is recorded once. The subscription is not canceled; cancel it yourself if you no longer want to bill that member.

Subscription Refunds

Read the Subscription Refunds page

What must client_request_id contain on POST /api/subscriptions/{id}/refund?

A string of at most 255 characters, unique per refund and not beginning with sub_ (reserved); reuse it on every retry of the same refund. Errors: 400 CLIENT_REQUEST_ID_REQUIRED, CLIENT_REQUEST_ID_INVALID, CLIENT_REQUEST_ID_TOO_LONG, CLIENT_REQUEST_ID_RESERVED.

Why must I branch on 200 refunded versus 202 pending_approval?

A 200 with status: "refunded" means the refund ran. A 202 with status: "pending_approval" means it would exceed your title's daily self-serve limit, so nothing moved and it became a request for Invo to review; the body has the same keys as a receipt, with zero amounts, plus request_id. Treating a 202 as a refund would tell a member money came back when it has not.

When does a refund become a request under the daily limit, and what does executed_today_usd show?

When the refund would take your title's self-served refunds for the current UTC day past the daily limit (reaching it exactly still runs); a single refund larger than the limit always becomes a request. By default the limit is never below the largest single subscription charge (25000.00 USD today), so any one period can be refunded on its own, but Invo can set a specific, possibly lower, limit for a title. executed_today_usd is what has already been refunded self-serve today, shown beside daily_cap_usd.

How long after a charge can a period be refunded?

30 days from when the period was charged; after that /refund returns 409 REFUND_WINDOW_CLOSED with period_charged_at, refund_window_days and window_closed_at. A retry of a refund that already ran still returns its receipt.

What happens if I omit period_seq?

Invo refunds the most recent billed period (paid, or partly refunded). The trial period of a paid trial is period_seq 1 if you want to target it.

How do refunded_amount_usd, card_refunded_usd and remaining_refundable_usd relate on a partial refund?

refunded_amount_usd is what this call refunded; on a card period charged since 2026-09-17 card_refunded_usd equals it, because the refund goes back to the card. total_refunded_amount_usd is cumulative for the period and remaining_refundable_usd is what is left, so the partials and the final refund add up to exactly the card charge.

Why is the 1.5% international card fee not returned?

It covers the cost of processing a card issued outside the US, which was incurred when the period was charged and is not recovered when the money goes back. It therefore stays charged when the period is refunded.

Does a full refund cancel the subscription?

Yes. A refund that completes a period's full amount cancels the subscription immediately (subscription_canceled: true, subscription_status: "canceled"), sends subscription.canceled with cancel_cause: "full_refund", and the refunded period stops counting toward paid_through. A partial refund leaves the subscription live; a subscription already canceled or expired is not canceled again.

Can a period charged through the platform store rail be refunded?

Not yet: /refund on such a period returns 409 STEAM_REFUND_NOT_SUPPORTED before anything moves. A platform store rail period the wallet covered entirely (funding.steam_charged_usd was 0.00) can be refunded in coins (funding_shape: "wallet"), within the same limits.

How do I list refund requests?

From your developer console session: GET /api/dev/subscriptions/refund-requests?status=pending|approved|rejected|all&game_id=&limit=&offset= (default pending, limit 1 to 100, default 50). It returns items and total for titles you own only; it is not available with the game key.

Does Invo email the member, and is my reason included?

Yes, when a refund executes Invo emails the member the amount, where it went (card or balance) and whether the subscription ended. Your reason is not included in that email. No email is sent for a replay, a refund request or a refusal.

Subscription Sandbox Testing

Read the Subscription Sandbox Testing page

How do I simulate a paid trial that converts to full price?

Save the 4242 4242 4242 4242 card for a member, then /subscribe with amount_usd: "29.00", trial_days: 7, trial_amount_usd: "1.00" and the player_card_id: expect trialing, first_charge.status: "paid" at 1.00, and subscription.renewed with period_seq: 1, is_trial: true. Then call advance-clock and force-renewal (sandbox base, no /api, with X-Sandbox-Clock-Key): expect subscription.renewed with period_seq: 2, is_trial: false, amount_usd: "29.00" and status active.

How do I walk a card subscription from active through past_due to expired?

Call POST {sandbox base}/subscriptions/{id}/force-failure four times. Expect past_due and subscription.past_due after the first, subscription.payment_failed on each, and subscription.expired with final_period_end on the fourth; the response reports retry_budget_used, retry_schedule_days and retries_remaining.

How do I drive real declines with a declining test card through /payment-method instead of force-failure?

Save the test token pm_card_chargeDeclined for the member with /setup-intent, attach it with POST /api/subscriptions/{id}/payment-method, then repeat advance-clock and force-renewal. Each renewal is genuinely declined (failure_code: "card_declined") and walks the real dunning ladder to expired after four attempts.

How do I trigger a real awaiting_authentication state with the authentication test card?

Save 4000 0025 0000 3155 on the hosted card page (or pm_card_threeDSecure2Required through /setup-intent) for a member, then subscribe: expect first_charge.status: "requires_action" with a confirmation_url and a subscription.authentication_required delivery. Open the link, complete the challenge, and subscription.renewed arrives. force-auth-challenge only moves the status and does not mint a link.

How do I test a price change so the old price bills one renewal and the new price the next?

POST /api/subscriptions/{id}/amount with the new price, then advance-clock and force-renewal: that renewal bills the old price. Repeat advance-clock and force-renewal: the next renewal bills the new price.

What happens when I cancel at period end and then force a renewal?

force-renewal returns 200 with action: "retired-at-period-end" and retired: true; nothing is billed and the subscription becomes canceled, exactly as production does at paid_through. No second subscription.canceled is sent; the one fired at cancel time is the only one.

How do I run the platform store rail flow in sandbox and confirm sandbox_auto_approved: true?

With the sandbox title enabled for the platform store rail (app id and publisher key verified in sandbox mode, billing set up) and the primary or steam channel key, call /steam/init with a real platform store id (201, pending_steam_authorization), then /steam/finalize. Expect 200 with first_charge.status: "paid", paid_period_seq: 1, steam_agreement_status: "active" and sandbox_auto_approved: true, plus subscription.renewed with funding.rail: "steam". Then advance-clock and force-renewal show engine_stats.steam_charged: 1 and sandbox_synthetic: 1.

Which platform store rail operations are blocked in sandbox?

None are blocked specifically by sandbox: the rules are the same as production, so a price increase returns 409 STEAM_REAUTHORIZATION_REQUIRED, /payment-method returns 409 NOT_A_CARD_SUBSCRIPTION, and a refund of a storefront-charged period returns 409 STEAM_REFUND_NOT_SUPPORTED. What differs is that sandbox never calls the storefront: finalize auto-approves, renewals treat the storefront charge as captured, and cancel does not end a real agreement. force-failure does not model a storefront refusal.

What happens to a pending platform store rail row I never finalize?

It expires 24 hours after init with subscription.expired, failure_code: "steam_authorization_abandoned" and final_period_end: null, with no payment_failed before it. Start again with a new client_request_id.

Subscription Error Reference

Read the Subscription Error Reference page

What should I do on 400 CLIENT_REQUEST_ID_RESERVED and 403 GAME_NOT_LIVE?

CLIENT_REQUEST_ID_RESERVED means your key begins with sub_, which Invo reserves; generate keys with a different prefix. GAME_NOT_LIVE means the title is in testing; set it live in the developer console (the same action in sandbox and production) and retry.

Should I reuse the same key after 503 flow_paused?

Yes. Nothing was created or changed, so retry later with the same client_request_id (or the same refund key); a new key risks a duplicate once the pause lifts.

How do I fix 400 TRIAL_AMOUNT_BELOW_FEE_FLOOR and PAYMENT_METHOD_REQUIRED?

TRIAL_AMOUNT_BELOW_FEE_FLOOR means the trial price would leave you nothing after Invo's card fee on that charge; raise trial_amount_usd to at least the price the error message names. PAYMENT_METHOD_REQUIRED on /subscribe means a paid trial was requested for a member with no usable saved card; save the card first, then subscribe (on /payment-method it means you sent no card id).

What does 409 WALLET_ONLY_SUBSCRIPTION mean on /payment-method?

You tried to attach a card to a wallet_only subscription without turning the flag off. Send wallet_only: false with the player_card_id; wallet_only is deprecated and such a subscription can never be paid until you convert it.

How should I retry after 502 REFUND_MISMATCH versus 502 PROCESSOR_ERROR?

PROCESSOR_ERROR means the card processor refused or failed the refund: retry with the same client_request_id, and contact Invo if it persists. REFUND_MISMATCH needs manual reconciliation (for example a refund for this request with a different amount, or a charge already fully refunded elsewhere): contact Invo and do not retry in a loop.

How do I correct 400 AMOUNT_EXCEEDS_REMAINING?

The body carries remaining_refundable_usd and already_refunded_usd; resend with amount_usd at or below remaining_refundable_usd, or omit amount_usd to refund the whole remainder.

What does 429 spending_limit_exceeded on /steam/init require?

The price would push the member past their spending limit for your title (hourly, daily and monthly USD caps per member per game). Retry later; if a title needs higher limits, ask your Invo contact.

What do 409 SETUP_REFERENCE_REUSED, 400 still_requires_action and 500 CARD_PERSIST_FAILED mean on setup-intent?

SETUP_REFERENCE_REUSED: you reused a setup_reference with different parameters; use a new reference for a new setup. still_requires_action (from /setup-intent/confirm): the client has not finished confirming the card; confirm with the card form, then call confirm again. CARD_PERSIST_FAILED: the card was authorised but not recorded; retry /setup-intent/confirm with the same setup_intent_id.

Subscription Launch Checklist and Traps

Read the Subscription Launch Checklist and Traps page

What must be true of the sandbox title before I can create a subscription?

It must be live (otherwise 403 GAME_NOT_LIVE) with a currency configured (otherwise 400 CURRENCY_NOT_CONFIGURED), and you must call the sandbox base https://sandbox.invo.network/sandbox with the sandbox key. Register and prove your webhook target before the first subscription.

Why must the webhook be registered before the first subscription exists?

Events fired before a target is registered are never delivered to it later, and several carry data you cannot fetch any other way (for example confirmation_url on subscription.authentication_required, and the funding and split figures on subscription.renewed). The first subscription.renewed fires inside /subscribe.

Which subscription events should my webhook subscribe to?

All ten: subscription.renewed, subscription.payment_failed, subscription.authentication_required, subscription.past_due, subscription.canceled, subscription.expired, subscription.refunded, subscription.refund_requested, subscription.refund_approved, subscription.refund_rejected. Add purchase.completed (so you can recognise and skip a card renewal's mint leg) and purchase.disputed (chargebacks), or subscribe to ["*"].

What order of operations must I follow for a paid trial?

Save the card first, then subscribe: mint POST /api/checkout/card-setup-sessions (with player_name for a new member), send the member to card_setup_url, read the card id from GET /api/currency-purchases/player-cards, then call /subscribe with player_card_id, trial_days or trial_end, and trial_amount_usd. The trial price is charged inside that call.

Why must client_request_id be persisted before calling /subscribe?

So a crash or timeout between the call and the response can be recovered by replaying the same key, which returns the existing subscription without charging again. A fresh key in a retry lands on 409 ACTIVE_SUBSCRIPTION_EXISTS at best and creates a second subscription at worst.

How do I reconcile period 1 by matching paid_period_seq to subscription.renewed?

Join the create response and the event on subscription_id, and match first_charge.paid_period_seq to the event's period_seq (not subscription.period_seq, which already reads the next period). Extend access to the event's period_end (equal to current_period_start and the new paid_through), and book split.partner_revenue_usd from the event.

What must consent contain on every create?

consent_at, consent_ip (the member's IP as your server saw it), consent_user_agent, disclosed_amount_usd (the price you showed), disclosed_interval and terms_version, plus for a paid trial disclosed_trial_amount_usd and disclosed_trial_days or disclosed_trial_end. Consent is not enforced and a mismatch does not block the create, but it is the record a disputed recurring charge is argued from.

Can coins pay for a card subscription?

No. Since 2026-09-17 a card subscription is paid only by card, charged the full price every period; a member with coins but no saved card fails with failure_code: "no_payment_method". Only the platform store rail spends the wallet first.

Why would a card saved through a saved_card_id purchase fail to back a subscription?

A purchase made with an already-saved card (saved_card_id) does not record the off-session consent a recurring charge needs, so that card cannot back a subscription. Have the member save the card again through the hosted card page, /setup-intent, or a purchase of a new card with save_card: true.

Why do price increases above double stay pending?

As a safety limit, Invo never applies a staged price more than double the price the member last agreed to (nor one above the 25000.00 USD per-charge ceiling); it stays in pending_amount_usd and the old price keeps billing. Raise prices in steps, or start a new subscription the member agrees to.

Subscription Reporting and Attribution

Read the Subscription Reporting and Attribution page

Which endpoint do I call to pay recipients for a closed month, and which field holds the amount?

GET /api/subscriptions/reporting/attribution?start=...&end=... with your game key; pay each recipient recipients[].net_attributed_usd, which is already net of reversals in the window. Invo pays no recipient; you pay them from your own settlement.

How does GET /api/subscriptions/reporting/attribution decide which window a renewal belongs to?

By when it settled (was paid), not when its period opened: start is inclusive and end exclusive. A July period that settled on 2 August is in August, and each refund reversal is reported in the window the refund happened in, so a closed window always reproduces the same numbers.

How do limit and offset work, and how is sorting kept stable?

They page over recipients, not renewals: limit defaults to 50 and is silently clamped to 100; offset is 0 to 100000. Recipients are sorted by net_attributed_usd descending with recipient id ascending as a tiebreaker, so none appears on two pages or none. Totals are never paginated.

Does the recipient_player_id filter change totals?

No. The platform-wide figures in totals are accumulated before the filter is applied; the filter only narrows the recipients list.

What triggers 400 WINDOW_TOO_LARGE?

A window containing more than 20,000 settled renewals (body carries settled_renewals, max_renewals) or more than 20,000 refunded renewals (reversals, max_rows). Narrow the range and run it in parts; a range over 400 days is WINDOW_TOO_LONG instead.

Why does changing a recipient's rate leave past reports unchanged?

Attributed amounts are the snapshot recorded when each renewal settled (the same object sent on subscription.renewed), never recomputed from today's rate. The detail endpoints show the current rate separately as current_revenue_share with is_payout_basis: false and applies_to: "future_renewals_only"; a renewal line's historical rate is percent_at_renewal.

What does reversal_basis: period_row mean, and how do I use reversals_without_receipts?

period_row is the fallback used when the per-refund records for a renewal are unavailable: the report uses the cumulative refunded total on the renewal and puts the whole reversal in the window of the latest refund, and any revenue clawback on it is not visible. totals.reversals_without_receipts counts such lines; check it is zero before paying, and review any line it flags.

Why is attribution null on some cycles?

Cycles that were never paid (pending, failed, forgiven) earned no one anything and carry attribution: null. A paid cycle can also be null, with attribution_unavailable_reason set to no_revenue_share_configured (no share existed, a real answer), spend_order_not_found, attribution_not_recorded or attribution_malformed; investigate the last three before paying from that line.

What does the period detail endpoint return in fee_legs, refunds.events and charge?

GET /api/subscriptions/reporting/periods/{period_id} returns fee_legs (the canonical two-leg split: invo_fee_usd, partner_revenue_usd, settled and reversed figures, pass_through_fees_usd, chargeback_fees_usd, disputed_amount_held_usd, card_fee_charged_to_partner_usd and partner_revenue_after_fees_usd), refunds.events (one entry per refund with its amount, cumulative before and after, timestamp and the reversal it caused) and charge (attempts_for_period, paid_attempt_no, paid_attempt_at, outcome).

Why can partner_revenue_after_fees_usd go negative on a full refund?

A full refund returns the whole price to the member, and you bear all of it: your revenue is reversed and Invo's card fee on that charge is charged to you (card_fee_charged_to_partner_usd). The renewal therefore ends at minus Invo's card fee, and also minus any international card fee, which is not returned. Example: a 100.00 USD Open-tier period refunded in full ends at -3.65.

What should I do with a recipient where resolved is false?

The recipient's player record could not be found, so the row is reported with identity fields null rather than dropped. Investigate it before paying; do not skip it silently.

How do the 500-line caps and truncation flags interact with totals?

Each recipient row includes at most 500 inline renewal lines (periods) and 500 reversal lines (reversals), with periods_truncated or reversals_truncated set when the cap is hit. Totals and per-recipient figures are always computed over everything and are never truncated; use the per-subscription history endpoint for the lines beyond the cap.

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