Renewals, funding and dunning

What Invo does on every billing day: when it charges, how a period is funded (the card pays the full price; on Steam the wallet goes first and Steam tops up; worked examples on both rails), what happens when a charge fails, the retry ladder and the grace window, the card authentication pause, and the handful of deferrals that produce no event.

1. When Invo charges

  • Billing is in advance: a period is charged at its start. The first period is charged inside /subscribe (or at /steam/finalize); each later period is charged at next_charge_at, which is the end of the period just paid.
  • At creation Invo adds up to one hour of random offset to the first period’s end, so a cohort of members who subscribed in the same minute renews spread across an hour a month later. The offset carries forward for the life of the subscription.
  • The renewal engine runs every minute in both environments. A subscription is picked up within a minute or two of next_charge_at, not at the exact second.
  • Invo charges at most one period per subscription per run. If Invo ever falls behind by whole periods (an outage on Invo’s side), the elapsed periods are forgiven, never back-billed; you see the gap as a jump in period_seq on the next subscription.renewed.
  • Trials. The trial window is period 1. A free trial charges nothing for it; a paid trial charges trial_amount_usd for it inside /subscribe. Either way amount_usd is charged as period 2 at trial_end, and from then on the subscription renews like any other. The step from a paid trial’s price to the regular price is not a price change and is never held back by the price-increase limits. If a paid trial’s own charge only succeeds after trial_end (a late authentication or a late retry), period 2 is charged straight after it: two charges back to back. See the card road, trials.

Do not poll for renewals. Subscribe to subscription.renewed. It is the source of truth for entitlement and it carries the funding and split figures you cannot get from GET.

2. Funding, card rail, worked examples

Card only (since 2026-09-17). The card pays the full price every period, whatever the member’s wallet holds. The wallet never pays for a card subscription and never moves: the coins the charge buys are minted and spent in the same step. There is no shortfall, no rounding and no minimum top-up on the card rail.

Price 9.99 USD per month, so a period is worth 99.90 coins.

Wallet beforeCard chargedCoins mintedCoins spentWallet after
0.00 coins9.99 USD99.9099.900.00
20.00 coins9.99 USD99.9099.9020.00
120.00 coins9.99 USD99.9099.90120.00
any, no usable cardnothing0.000.00unchanged; the charge fails into dunning (no_payment_method)
below zeronothing0.000.00unchanged; the charge fails into dunning (wallet_negative)

The funding block on subscription.renewed still carries every field it always did. On the card rail balance_applied_coins is always "0.00", card_charged_usd is the full price, minted_coins equals amount_coins, and new_balance is the member’s unchanged balance. A price below the card processor’s own minimum charge is declined by the processor and goes through dunning; Invo does not round a card charge up to a minimum. Save a card before /subscribe: a member with coins but no saved card is not charged from the coins.

The split. On the card rail Invo’s fee is the subscription fee for your tier, not the 10 percent item-purchase fee. On the Open tier that is 4.5 percent plus a fixed 0.30 USD, on the full price: a 9.99 period pays Invo 0.75 and you 9.24. On the Merchant of Record tier it is 5.5 percent plus a fixed 0.50 USD per card charge, so the same period pays Invo 1.05 and you 8.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 that fee on either tier, reported as split.international_card_fee_usd (split.partner_revenue_usd stays the gross figure; split.pass_through_fees_usd carries the same figure, so never add the two). Read the split block on the event rather than recomputing it. On the Open tier you are the seller and Invo adds no tax to the charge; see Tiers.

Every paid card renewal also sends purchase.completed for the coins its charge minted. Recognise it by data.metadata.source == "subscription_renewal" (with data.metadata.subscription_id). Do not grant currency or access from it: those coins are spent by the same renewal. Act on subscription.renewed only; its mint_order_id is that event’s order_id. And if a renewal’s card charge succeeds but Invo cannot complete the renewal in the same step, you may see subscription.payment_failed with a settlement failure_code (for example settlement_error); the retry then completes the period without charging the card again, and subscription.renewed follows.

3. Funding order, Steam rail, worked examples

The Steam rail is unchanged by the card-only change. Price 9.99 USD per month, US member, so a period is worth 69 coins. Period 1 always charges the full price (that is what creates the agreement). From period 2 the wallet goes first, and Steam is charged the smallest amount that buys the missing coins, never above the price and never below the 1.00 USD minimum top-up.

PeriodWallet beforeSteam chargedCoins mintedCoins spentWallet after
1 (always full price)20 coins9.99 USD696920
2, empty wallet09.99 USD69690
2, partial wallet207.00 USD (smallest charge that buys the missing 49)49690
2, full wallet80nothing; no Steam call06911

For a German member the same price is worth 58 coins and the partial top-up above would be 6.46 USD. The country is fixed at init, so every period of a subscription is worth the same coins. On Steam the split is computed on what the coins are worth (basis: "steam_net"), not on the price; the top-level amount_usd on the event is always the price.

4. When a charge fails

Failure schedule, per title, default retry after 2 days, then 3 days, then 2 days, then expire: three retries over seven days.

MomentSubscriptionEventsMember access
Charge failspast_due, next_charge_at = now + 2 dayssubscription.payment_failed (first_failure: true, retries_remaining: 3, grace_period_end = now + 7 days), then subscription.past_dueRetained.
Retry 1 failspast_due, next_charge_at = +3 dayssubscription.payment_failed (first_failure: false, retries_remaining: 2)Retained.
Retry 2 failspast_due, next_charge_at = +2 dayssubscription.payment_failed (retries_remaining: 1)Retained.
Retry 3 failsexpired, ended_at set, next_charge_at nullsubscription.payment_failed (retries_remaining: 0), then subscription.expired with final_period_endRevoke at final_period_end (the last paid boundary; null if nothing was ever paid).
Any retry succeedsactive, period advancedsubscription.renewedContinue.

The one exception: a paid trial’s own charge. When the card refuses the trial charge (outcome card_declined, or insufficient_funds because no card could be charged), there is no ladder: the subscription ends at once as expired, and you receive subscription.payment_failed with retries_remaining: 0 then subscription.expired with expire_cause: "trial_payment_failed". A temporary error (outcome: "error", or failure_code wallet_negative) takes the ordinary ladder above, and a later retry that is declined ends the trial the same way. Period 2 onwards is ordinary dunning.

grace_period_end is computed from the live schedule at the moment of each failure, so it is always the date Invo will actually honour. Access is retained through the whole grace window; only subscription.expired revokes it. A member who fixes their card during the window (through /payment-method, by saving a new card on the hosted card page, or on Steam by topping up their wallet) is charged at the next retry; you can also force an immediate retry in sandbox (advance-clock with intervals: 0). Invo re-resolves the card at every attempt: a subscription whose card is absent, detached or expired adopts the member’s newest saved card and records it, so the next read names the card that was charged. There is no retry-now call in production; the retry runs at next_charge_at.

Failure codes

Railfailure_codeMeaning
cardcard_declinedThe issuer declined.
cardno_payment_methodThe member has no saved card at all. Since 2026-09-17 this fails the charge whatever the wallet holds. A card the member saves before the next retry is adopted automatically at that retry.
cardwallet_onlyThe subscription opted out of card charges (wallet_only, deprecated), so it can never be paid and lapses at the end of the ladder. Nothing is adopted until you flip the flag through /payment-method.
cardwallet_negativeNew (2026-09-17), additive. The member’s coin balance is below zero, which only a refund of coins they had already spent can cause. Nothing was charged. The retry succeeds once the balance is back to zero or above.
cardcard_expired, card_detached, card_mismatch, no_customer_snapshotThe attached card is no longer usable and no fallback card was found.
cardprocessing_error, settlement_errorA processing or settlement failure on Invo’s side. Retried on the same ladder.
steamsteam_agreement_unavailable, steam_agreement_canceled, steam_account_lockedThe agreement or the account cannot be charged.
steamsteam_<code>A Steam refusal, with Steam’s code appended.
steamsteam_authorization_abandonedThe member never authorised within 24 hours. Appears on subscription.expired.
steamsteam_containmentNo longer returned. Spending is not restricted by where the currency was bought, so a renewal is never deferred or expired for this reason. Kept here so an integrator who already handles the code knows it will not arrive; you can stop handling it.

failure_message is a short human-readable string beside every code. insufficient_funds is never a failure_code: it is the attempt outcome on subscription.payment_failed (one of card_declined, insufficient_funds, error), and on an attempt with no usable card the failure_code beside it names why no card could be charged. A handler branching on failure_code == "insufficient_funds" matches nothing.

5. Card authentication (awaiting_authentication)

Some card issuers require the cardholder to authenticate a recurring charge. When that happens:

  1. Nothing is charged. No retry is consumed. Access is retained.
  2. The subscription moves to awaiting_authentication and next_charge_at is pushed to the challenge deadline.
  3. Invo mints a single-use confirmation link and sends subscription.authentication_required carrying confirmation_url and expires_at. On the first charge the same link is also on the /subscribe 201 (first_charge.confirmation_url).
  4. You relay the link to the member (email, in-app message). The link opens an Invo-hosted page at /subscription-auth?token=... on Invo’s checkout host; the member authenticates with their issuer there. Do not frame it, and do not log the URL beside identifiers you publish; it is a bearer link.
  5. On success the renewal resumes: subscription.renewed fires, the subscription is active. The page tells a member who refreshes that the payment is confirmed.
  6. The link lives at most 72 hours, and never past the next scheduled charge. If it lapses, the page says so, Invo tries the card again on its own, and ordinary dunning resumes. A card that asks for authentication on more than two consecutive attempts for the same period is treated as declined.

The card_amount_usd on the event is what the card is being asked for. Since 2026-09-17 that is the full period price, the same figure as amount_due_usd, on every card challenge (a challenge opened before that date can still show the older wallet-shortfall figure). Both fields stay.

authentication_required is not a failure. Do not send the member a “payment failed” message; send them the link. Do not revoke access. The only way to reproduce the link if you lost it is to replay the webhook delivery.

6. Deferrals you will not be told about

A few renewal outcomes are deferrals rather than failures. No event is sent, no retry budget is consumed, and the member’s access is unaffected. You can see them on GET as an unchanged (or past_due) status with next_charge_at about 24 hours out:

  • The price is above the platform’s per-charge ceiling of 25000.00 USD. Such a subscription is never charged. The ceiling is per charge, so an annual plan is measured on the whole annual amount rather than the monthly equivalent: $500 per month billed annually is one $6,000 charge and passes.
  • Withdrawn. A renewal was once deferred when the wallet held currency bought through Steam and the game was not a Steam title, expiring after seven consecutive daily deferrals with failure_code: "steam_containment". There is no such deferral. A balance can be spent in any title regardless of where the currency was bought, so a renewal spends the whole wallet and the code is never sent. Nothing to handle.
  • On Steam, Invo cannot issue currency for your title right now (billing setup incomplete, rail suspended, credit unavailable), or the title’s Steam configuration is unusable. The deferral repeats daily until it is fixed on your side.

If a subscription’s next_charge_at keeps moving a day at a time with no event, one of the two live deferrals above is the reason. Check the price, then your Steam billing setup in the console.

Related

  • Webhooks: the exact payloads of subscription.renewed, payment_failed, past_due, authentication_required and expired.
  • Sandbox: walk a subscription through the whole ladder in minutes with force-failure.
  • Manage: /payment-method heals a card-less or declined subscription before the next retry.
  • Refunds and chargebacks: what a refund or a lost chargeback does to a paid period.