Renewals, funding and dunning

What Invo does on every billing day: when it charges, how a period is funded from the wallet first and the rail second (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.

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 order, card rail, worked examples

Price 9.99 USD per month, so a period is worth 99.90 coins. Shortfall is converted to USD rounded up to the cent, and never below the 1.00 USD minimum top-up.

Wallet beforeShortfallCard chargedCoins mintedCoins spentWallet after
0.00 coins99.90 coins9.99 USD99.9099.900.00
20.00 coins79.90 coins7.99 USD79.9099.900.00
95.00 coins4.90 coins (0.49 USD, floored to the minimum)1.00 USD10.0099.905.10 (dust, spent next period)
120.00 coinsnonenothing; no card call0.0099.9020.10

The funding block on subscription.renewed reports balance_applied_coins (the pre-existing wallet portion), card_charged_usd, minted_coins and new_balance for every renewal.

The split follows the funding. The percentage fee applies to the whole period. The fixed 0.30 USD component applies only when the card was actually charged, and only to the card-funded portion. A period the wallet covered entirely carries the percentage alone. Read the split block on the event rather than recomputing it.

3. Funding order, Steam rail, worked examples

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 game, 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.

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, 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).

Failure codes

Railfailure_codeMeaning
cardcard_declinedThe issuer declined.
cardinsufficient_fundsWallet short and no chargeable card (card-less or wallet_only).
cardcard_expired, card_detached, card_mismatchThe 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_containmentSeven consecutive daily deferrals because the wallet holds value that cannot be spent in this game (section 6). Appears on subscription.expired only; no payment_failed precedes it.

failure_message is a short human-readable string beside every code.

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-game 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 (the wallet shortfall), which differs from amount_due_usd (the period price) whenever the member holds a balance.

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 500.00 USD. Such a subscription is never charged; keep prices at or below 500.00.
  • The member’s wallet holds value that cannot be spent in this game, and the shortfall cannot be topped up around it. The subscription reads past_due. After seven consecutive daily deferrals it expires: you receive subscription.expired with failure_code: "steam_containment" and final_period_end set (no subscription.payment_failed precedes it, because no charge was attempted). Revoke at final_period_end as for any expiry.
  • 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 three above is the reason. Check the price, then your 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.