Subscriptions

Recurring memberships on the Invo rail. You call one endpoint when a member signs up; Invo owns the billing clock, the charge, the retries, and the recurring currency grant, and tells you what happened over webhooks. You own the entitlement — what the member actually gets for being a subscriber.

Who this is for: partners selling a repeating membership — a monthly or yearly pass, a guild tier, a supporter plan. Every endpoint is server-to-server and authenticated with your X-Game-Secret-Key (see Authentication). Sandbox uses the /sandbox path prefix, exactly as elsewhere (see Sandbox Testing).

1. The ownership model

A subscription is standing authority to charge a member on a schedule. Invo holds that authority, is the merchant of record on the charge, and runs the whole money path: computing when the next charge is due, spending the member's balance, topping up from a card when the balance is short, minting and granting the currency, splitting the fee, retrying failures on a schedule, and expiring the agreement when the retries run out.

You never hold card data, you never run a billing cron, and you never reconcile a partial charge. What you own is the entitlement: the mapping from item_id to whatever being a member means in your product, and the decision to grant or revoke it when we tell you the subscription changed state.

There are no plan objects

The price lives on the subscription. You pass amount_usd at signup and that is the agreement — there is nothing to create in advance, nothing to keep in sync, and nothing to garbage-collect when a tier is retired. Two members on "the same plan" at different prices is simply two subscriptions with two amounts. Prices are always in USD; the currency granted on each renewal is derived from it (see Virtual Currency Setup).

One live subscription per member, per item

A given member can hold at most one live subscription to a given item_id in your game. A second create attempt returns 409 ACTIVE_SUBSCRIPTION_EXISTS with the id of the one that already exists. This is the guard that survives the most common integration bug there is — regenerating your idempotency key inside your own retry loop — and stops a member being billed twice a month forever. Canceled and expired history does not block re-subscribing.

2. Lifecycle and status values

These are the only values status ever takes. Four of them are live — the subscription still occupies the member's one-per-item slot and still has a next charge scheduled. Two are terminal.

StatusLive?What put it here, and what leaves it
trialingyesCreated with trial_days or trial_end. The trial window is the first billing period and is never charged for. At trial end the first paid cycle opens and the status becomes active on the first successful charge.
activeyesThe normal state. Created without a trial, or recovered from past_due by a successful retry. Leaves on a failed charge, a step-up challenge, or a cancellation.
past_dueyesA charge failed and the retry schedule is running. Access is retained for the whole grace window — this is not the point at which you revoke. Returns to active when a retry succeeds; becomes expired when the retry budget is exhausted.
awaiting_authenticationyesThe member's bank asked the cardholder to re-authenticate before the charge completes. Not a failure and not a decline — no retry was consumed and no access was lost. See section 6.
cancelednoCancelled by you (immediately, or at the end of the paid period). Terminal — the agreement is closed and will never be charged again.
expirednoDunning ran out of retries. Terminal. This is the event that revokes entitlement, and it reports the last period the member actually paid for.

Two clocks — do not confuse them

current_period_end is the billing boundary. It exists from the instant a subscription is created, before any money has moved, and it is a projection.

paid_through is the entitlement boundary: the furthest period end the member has actually paid for, or null if nothing has been charged yet. Every access decision you make should read paid_through. Quoting current_period_end as "access until" claims a member paid for a month they were never billed for.

3. Dual funding — balance first, card for the shortfall

This is the part that differs most from a conventional subscription rail, and it is the part that decides your retention. On renewal day Invo spends the currency the member already holds. If that covers the renewal, no card is touched at all. If the balance is short, the difference is charged to the card on file, minted, and settled from the balance in the same transaction.

Leg 1 — the balance

The member's existing balance is applied first, always. A member who tops up regularly may go a whole year without a card ever being charged. The renewal record reports exactly how much came from here as balance_applied_coins.

Leg 2 — the card top-up

Only the shortfall is charged, off-session, to a card the member saved on an Invo-hosted page. Invo is merchant of record; you never see or store card data and your PCI scope is unchanged. Reported as card_charged_usd.

Why this matters more than it looks

A balance-only subscription makes "the member forgot to top up" the ordinary failure mode rather than the edge case, and it loses people who fully intended to keep paying. The card backstop removes that failure mode without fracturing the balance story: the member's ledger still reads currency in, currency out. The cost to you is one card capture at signup — a redirect to a page we host.

Opting out: wallet_only

Pass wallet_only: true at creation and Invo will never touch a card for that member — the renewal succeeds only if the balance covers it, and fails into ordinary dunning if it does not. The flag is deliberately sticky: attaching a card to a wallet_only subscription is refused with 409 WALLET_ONLY_SUBSCRIPTION unless you explicitly send wallet_only: false in the same call, so nobody ends up holding a billable card they believed was disabled.

4. What a successful renewal does

Every renewal produces one retrievable record — the subscription equivalent of an invoice — and one webhook. The currency grant, the fee split, the ledger entries and the webhook are all committed together: if you received subscription.renewed, the money moved.

subscription.renewed

{
  "event": "subscription.renewed",
  "data": {
    "subscription_id": "SUB_...",
    "item_id": "guild_gold",
    "period_seq": 4,
    "transaction_id": "TXN_...",
    "order_id": "ORD_...",
    "player_email": "member@example.com",
    "identity_id": "...",
    "amount_usd": "9.99",
    "amount_coins": "99.90",
    "period_start": "2026-08-01T09:14:22.481-04:00",
    "period_end":   "2026-09-01T09:14:22.481-04:00",
    "current_period_start": "2026-09-01T09:14:22.481-04:00",
    "current_period_end":   "2026-10-01T09:14:22.481-04:00",
    "next_charge_at": "2026-09-01T09:14:22.481-04:00",
    "funding": {
      "balance_applied_coins": "40.00",
      "card_charged_usd": "5.99",
      "minted_coins": "59.90",
      "new_balance": "0.00"
    },
    "split": {
      "total_usd": "9.99",
      "invo_fee_usd": "...",
      "partner_revenue_usd": "...",
      "invo_fee_percent": "..."
    },
    "revenue_share_attribution": null,
    "metadata": { }
  }
}

A card-funded renewal also fires purchase.completed

The top-up leg is a genuine currency purchase — currency really was minted — so it emits the same event any other purchase does. If your purchase.completed handler grants something, it will now grant on renewals too, and the member gets it twice. Correlate on order_id, or branch on metadata.source, before you fulfil. Balance-funded renewals do not produce this second event.

5. When a charge fails

Invo retries on a schedule — by default 3 retries over 7 days, then the subscription expires. The schedule is configurable per game, and the full mechanics live in Renewals & Dunning. Two things about it routinely surprise integrators:

  • subscription.payment_failed fires on the FIRST failure, not the last — while the member still has access and can still fix it. That is the entire point of a grace window. The payload carries grace_period_end, retries_remaining, retry_at, and access_retained: true. Send the "top up or update your card" message here.
  • Access is retained through the whole window. Neither payment_failed nor past_due is a revocation signal; both carry access_retained: true. Soft-degrade if you like, but keep the member's row. Only subscription.expired revokes.

subscription.past_due is the state transition and fires once; payment_failed is per attempt and fires on each one, with first_failure distinguishing them. If you only want "this member is in arrears", handle past_due and ignore the per-attempt traffic. subscription.expired carries final_period_end — the last instant the member actually paid for — and access_retained: false.

6. When the member must re-authenticate

This is the one you must build UI for

Some renewals — mostly UK and EU cardholders — require the member to re-authenticate with their bank before the charge can complete. This is regulatory; retrying cannot solve it and neither can we. Invo emits subscription.authentication_required carrying a confirmation_url and an expires_at, and you must surface that link to the member. If nobody shows it to them, they cannot complete it, and a member who would gladly have paid eventually converts to a decline and expires.

What the member sees: a page Invo serves, at that exact link, that asks their bank for the confirmation and then completes the renewal. They do not re-enter card details and they do not create anything new — the page confirms the charge that already exists, which is why a double-click, a refresh, or a stale tab cannot take money twice.

What is true while a challenge is open:

  • The subscription sits at awaiting_authentication, not past_due.
  • No retry budget is consumed (retry_consumed: false) and the member is not expired while it is live.
  • access_retained is true. Do not wire this event to your cancellation flow.
  • card_amount_usd is what the card is actually being asked for — the balance shortfall, not the full renewal price.

The link is a bearer capability with its own token: single-use in effect, expiring, and not derivable from subscription_id. Relay the one in the event, never construct your own, and do not log it next to identifiers you publish. If it expires unused, the subscription returns to ordinary dunning rather than being stranded.

7. Cancellation and the paid-through date

POST /api/subscriptions/{subscription_id}/cancel (full reference in Manage a Subscription) takes at_period_end, which defaults to true: the member keeps the access they already paid for, no further charge is made, and the agreement retires at the paid-through boundary. Passing false ends it immediately.

Either way you get one subscription.canceled event, at request time, carrying cancel_at_period_end, effective_at, and access_until. An at-period-end cancel is not a promise followed by a second event later — revoke at the effective date you were given.

The subtlety: a never-charged subscription cancels immediately

at_period_end: true on a subscription that has never been charged (or whose paid window has already elapsed) is downgraded to an immediate cancellation. The response says so explicitly: terminated_immediately: true, downgrade_reason: "NO_PAID_PERIOD", and access_until set to now. There is no paid period to preserve, and honouring the flag would hand out a free cycle to anyone who signs up and cancels a minute later. Read terminated_immediately rather than assuming your flag was honoured. Cancelling an already-terminal subscription is not an error — it returns 200 with already_canceled: true.

8. Changing the price

POST /api/subscriptions/{subscription_id}/amount (see Manage a Subscription) stages a new price. It applies from the next period and there is no proration — the current cycle bills at the price it opened with. Until then the queued value is visible as pending_amount_usd on every read, and the response reports applies_from_period_seq. Sending the current price withdraws a queued change.

applies_from is null until something has actually been paid, because before the first successful charge there is no settled boundary to quote.

Two guardrails sit on the renewal itself, and both refuse rather than clamp — a refused renewal is recoverable, a wrong charge is not. A single renewal above $500 is not charged, and a queued price rise of more than what the member was last disclosed is not promoted (the subscription keeps billing the old price until a human confirms). Sending consent.disclosed_amount_usd at signup is what makes the second guardrail tight, because it baselines against what the member was actually shown.

9. Revenue attribution (reporting, not remittance)

If a third party in your game earns a share of a membership — a guild founder, a creator — you can record that arrangement with revenue_share at signup. On every settled renewal Invo computes what that recipient earned, records it, ships it on subscription.renewed as revenue_share_attribution, and exposes it through three reporting endpoints — see Subscription Reporting.

Invo does not pay that recipient. Every attribution figure carries settled_by_invo: false. Invo settles to you; you pay your own recipients out of your own revenue, on your own rails. The attribution data exists so that paying them is a lookup rather than a reconstruction — every figure derives from settled renewals, never from a stored rate. v1 supports exactly one recipient per subscription, and the recipient must already exist as a player in the same game.

10. The endpoints

MethodPathPurpose
POST/api/subscriptions/subscribeCreate. Idempotent on client_request_id. See Create a Subscription.
GET/api/subscriptions/{subscription_id}Read one.
GET/api/subscriptions/player?player_email=List a member's subscriptions. Filter with status (comma list, or live), page with limit/offset.
POST/api/subscriptions/{subscription_id}/cancelat_period_end (default true), optional reason.
POST/api/subscriptions/{subscription_id}/amountStage a price change for the next period.
POST/api/subscriptions/{subscription_id}/payment-methodAttach or replace the backing card, by saved-card id.
POST/api/subscriptions/{subscription_id}/refundRefund a renewal, full or partial. Idempotent on client_request_id.
GET/api/subscriptions/reporting/attributionAttribution aggregated per recipient over a settlement window.
GET/api/subscriptions/reporting/subscriptions/{subscription_id}/attributionOne subscription's attribution history.
GET/api/subscriptions/reporting/periods/{period_id}Reconcile a single billed period.

Reads and writes are both scoped to the authenticated game — a subscription id from another tenant simply does not exist to you. Creating a subscription additionally requires a live game; a game in testing can read and cancel, but cannot open standing authority to charge a real consumer (403 GAME_NOT_LIVE).

11. Events to handle

Full payloads for each of these are in Subscription Webhooks. Subscribe and verify signatures as described in Receiving Webhooks; manage the subscription itself through the Webhook Management API, which also gives you delivery history and a per-delivery replay — a dropped delivery is recoverable, so build against it rather than around it. Every subscription event carries the same core block (ids, status, interval, period bounds, your metadata), so one decoder plus a switch on the event type is enough.

EventWhat you do
subscription.renewedExtend access, write your ledger entry, send a receipt. Carries the funding split and any attribution figure.
subscription.payment_failedAsk the member to top up or update their card. They still have access.
subscription.authentication_requiredSurface confirmation_url to the member. Not a failure.
subscription.past_dueSoft-degrade if you want to. Keep the entitlement row.
subscription.canceledRevoke at effective_at / access_until.
subscription.expiredRevoke now, decrement any member counters.
subscription.refundedReverse the entitlement for the refunded period.

12. What you build vs what we handle

The honest scope of the integration. Everything on the right is not your problem.

You buildInvo handles
A signup call to /subscribe with a stable idempotency keyCreating the agreement, the billing clock, and the first period
A redirect to the Invo-hosted card capture pageCard capture, storage, and merchant-of-record status
A webhook endpoint and an entitlement table keyed on item_idCharging on schedule, balance-first funding, minting and granting the currency, the fee split
A "fix your payment" message on payment_failedThe retry schedule, the grace window, and expiry
A visible link on authentication_requiredThe hosted re-authentication page and resuming the charge
Revocation on canceled / expired, reversal on refundedWorking out the true paid-through date, and refunding across balance and card correctly
Paying your own third-party recipientsComputing and reporting what each recipient earned, per settled renewal

Start here

Read Create a Subscription for the full request contract, then wire the seven events above using Subscription Webhooks. Manage a Subscription covers cancel, reprice, card changes and refunds; Renewals & Dunning covers what happens on billing day. Two rules will save you most of the debugging: gate access on paid_through, never on current_period_end; and treat subscription.authentication_required as a prompt to the member, never as a failure.