Subscriptions: a membership or season pass sold on the web, renewed by card
A subscription is a plan the player buys in your web store and that renews automatically: a monthly VIP membership, a season pass, a "shard stipend" that drops currency every month. Appside charges the card each cycle and tells your backend what happened. Your backend owns the entitlement: it grants on subscription.started, keeps granting on subscription.renewed, and revokes on subscription.expired. The store never changes a wallet or a flag itself.
Plans
A plan is created in Studio → Catalogue → Plans:
| Field | Values |
|---|---|
plan_id | your key, [a-z0-9-]{2,40}, stable once published |
name | shown on the card and the receipt |
period | month or year |
prices | one price per currency: USD, EUR and GBP in v1; the player pays in the currency the store shows them, and the subscription keeps that currency |
trial_days | 0 to 30; the card is stored at sign-up and first charged when the trial ends |
benefits[] | up to 6 lines shown on the card; text only, the game decides what they mean |
state | draft, live, paused (no new subscribers), retired (existing run to period end) |
A store can have several plans. Switching between plans (upgrade, downgrade, proration) is not in v1: a player cancels one and subscribes to another.
Events to your Fulfillment URL
Every change is delivered to the same Fulfillment URL as orders, as a signed HTTPS POST with the same headers, signature, retries and dedupe rules as order.paid. Acknowledge with 2xx; dedupe on idempotency-key.
{
"type": "subscription.renewed",
"event_id": "SUB-9a2f…:3",
"mode": "test",
"tenant": "orbit",
"player_id": "player-123",
"probe": false,
"subscription": {
"id": "SUB-9a2f…",
"plan_id": "star-pass",
"status": "active",
"cycle": 3,
"period_start": "2026-01-15T10:00:00Z",
"period_end": "2026-02-15T10:00:00Z",
"price": 499,
"currency": "USD",
"cancel_at_period_end": false
}
}
| Event | When | What your backend does |
|---|---|---|
subscription.started | first charge paid, or trial began (cycle 1; price 0 during a trial) | grant the benefits until period_end |
subscription.renewed | a renewal charge paid | extend the benefits to the new period_end |
subscription.payment_failed | a renewal charge failed; grace_until is set | keep the benefits until grace_until; optionally tell the player in-game |
subscription.canceled | the player canceled; cancel_at_period_end is true | nothing yet; benefits run to period_end |
subscription.expired | period_end or grace_until passed without a paid cycle | revoke the benefits |
subscription.refunded | a cycle was refunded from Studio; refunded_cycle is set | your decision: usually revoke for that cycle |
event_id is <subscription_id>:<cycle> for started and renewed, and <subscription_id>:<cycle>:<event> for the others, so each is granted or revoked exactly once. Events for one subscription arrive in order; if your backend is down, retries keep the order. probe: true is the Studio Send test event: acknowledge it, grant nothing.
price is in minor units (cents) like order.price. status is one of trialing, active, past_due, canceled, expired.
What the player sees
- A Membership card in the store with the plan, the price per period, the benefits and, if any, the trial. Checkout is the same checkout as a pack: card, saved for renewals, with the renewal terms shown before the player confirms.
- In the player area: the plan, the next charge date and amount, the payment history, and Cancel, one tap, effective at period end, no questions asked. Cancelling is final for that subscription: the membership runs to the paid date, and the player can subscribe again afterwards.
- After a failed renewal: a "payment failed, update your card" notice with a retry; no benefits are removed until
grace_until.
Access is tied to the player_id in the player token. If the same player also holds an in-app subscription, that is your union to compute; the store knows only about the web plan.
Limits and rules
- Cards only in v1. Direct Debit, wallets and other methods come later.
- Plan prices in USD, EUR and GBP in v1; a subscription keeps the currency it started in. Other currencies later.
- Trials: a plan can be drafted with
trial_days, but publishing a live plan with a trial is blocked in this version. - Resume: not available before the paid period ends; the player subscribes again afterwards (a new subscription).
- Grace after a failed renewal: 7 days with retries on days 1, 3 and 5; then
subscription.expired. - Refunds are per cycle from Studio and send
subscription.refunded. - Taxes and the merchant of record are yours, as everywhere in Appside.
- Rate: the published per-payment rate, applied to each paid cycle. Nothing per plan or per subscriber.
Test mode
Plans and subscriptions work in test mode with the test card from the testing page (Google Pay in the sandbox works too). Renewals follow the real monthly or annual cycle; payment_failed and expired are produced by the provider's retry cycle, so plan a full cycle for an end-to-end test of those.
Example (Node)
app.post('/appside/fulfillment', verifyAppside, async (req, res) => {
const ev = req.body;
if (ev.probe) return res.sendStatus(200);
if (await seen(ev.event_id)) return res.sendStatus(200);
const s = ev.subscription;
switch (ev.type) {
case 'subscription.started':
case 'subscription.renewed':
await grantPass(ev.player_id, s.plan_id, new Date(s.period_end)); break;
case 'subscription.payment_failed':
await grantPass(ev.player_id, s.plan_id, new Date(s.grace_until)); break;
case 'subscription.expired':
case 'subscription.refunded':
await revokePass(ev.player_id, s.plan_id); break;
case 'subscription.canceled':
break; // runs to period_end; nothing to do yet
default:
return handleOrderEvent(ev, res);
}
await markSeen(ev.event_id);
res.sendStatus(200);
});