Skip to main content

Self-serve subscription state

Accounts that pay by card manage their own subscription — starting one, changing plan, buying add-ons, cancelling and resubscribing. This page covers the fields that say where such an account stands and what it may do next, so a billing screen never has to work either out from raw status text, and how to price a change before the customer commits to it.

Two fields, two questions​

QuestionField
Is this account provisioned, or does it need to pay?Account.selfServeSubscriptionState
Where does its subscription stand, and what can it change?Account.selfServeAccountSubscription → state and capabilities

selfServeSubscriptionState covers accounts with no subscription at all — one that never completed checkout, or that is not self-serve in the first place. selfServeAccountSubscription is null for those, so start from selfServeSubscriptionState when you are deciding whether to show a billing screen at all, and read the subscription's own fields once you are on it.

Neither says what the account is entitled to. That stays with the *Available flags on Account.subscription — a subscription can be perfectly healthy and still not include the product you are gating on.

Operation: query GetSelfServeBillingState($accountId: ObjectID!) { getAccount(id: $accountId) { id selfServeSubscriptionState selfServeAccountSubscription { state status cancelAtPeriodEnd currentPeriodEnd trialEnd planCode planName capabilities { canCancel canReactivate canResubscribe canUpdate } } } }Variables: { "accountId": "TjAwN0FjY291bnQxMjM0NQ" }
GetSelfServeBillingStateTry in Explorer
GraphQL
query GetSelfServeBillingState($accountId: ObjectID!) {
getAccount(id: $accountId) {
id
selfServeSubscriptionState
selfServeAccountSubscription {
state
status
cancelAtPeriodEnd
currentPeriodEnd
trialEnd
planCode
planName
capabilities {
canCancel
canReactivate
canResubscribe
canUpdate
}
}
}
}

Subscription state​

selfServeAccountSubscription.state is one typed value covering the whole lifecycle. Branch on it rather than on status, which carries the payment provider's own vocabulary and is not enumerated in this schema.

StateMeaning
ActivePaid, running, not set to cancel.
TrialingIn a trial that converts to paid at trialEnd.
CancellingStill live, but set to end. The customer keeps everything until it does.
PaymentFailedPayment is outstanding. Usually still granting access while the card is retried — do not read it as a loss of access.
PaymentIncompleteThe first payment was never completed, so the subscription has never granted anything.
PausedPaused on our side; only Lumar can lift it.
EndedOver for good, by cancellation or an expired first payment.

Where a subscription qualifies for more than one, the more urgent wins: a cancelling trial reports Cancelling, and an outstanding payment reports PaymentFailed whatever else is set.

state is deliberately wider than cancelAtPeriodEnd, which is only true when the end date falls on the current period — the usual case, and the one where currentPeriodEnd is the date to show. A cancellation dated anywhere else still reports Cancelling, so read cancelAtPeriodEnd before presenting currentPeriodEnd as the day access stops.

Capabilities​

capabilities reports which subscription mutations the subscription's current state allows. Each flag is taken from that mutation's own state precondition, so a screen can render the buttons it is given instead of deriving them from a status string.

They are eligibility, not a guarantee of success: a call can still fail for a reason that has nothing to do with the subscription's state. Resubscribing is the one to watch — it creates a new subscription and cannot inherit the cancelled one's card, so an account with no default payment method gets ACCOUNT_SUBSCRIPTION_PAYMENT_METHOD_MISSING and needs the billing portal, even though canResubscribe was true.

FlagMutation it gates
canUpdateupdateSelfServeAccountSubscription — plan, billing interval and add-ons all move through this one mutation
canCancelcancelSelfServeAccountSubscription
canReactivatereactivateCancellingSelfServeAccountSubscription — call off a pending cancellation
canResubscribereactivateCancelledSelfServeAccountSubscription — start again after the subscription ended

Plan and add-on changes are refused while a cancellation is pending: canReactivate is the way back, and the others become true again once it is called off.

canResubscribe is not simply "the subscription ended". Reactivation applies only to a cancelled subscription; one whose first payment expired instead has to go through checkout again, which selfServeSubscriptionState reports as CheckoutRequired. Read the capability rather than inferring it from Ended.

Pricing a change before making it​

updateSelfServeAccountSubscription moves the plan, the billing interval and the add-ons. Before offering it, call previewSelfServeAccountSubscriptionChange with the same plan, interval and add-on set to find out what the change costs and when it lands. It creates nothing and charges nothing — it prices the change against the live subscription and returns the answer.

Do not work the figure out yourself. A client that multiplies the monthly difference by the fraction of the period remaining will be wrong: proration is calculated per second, tax is applied to the result, and a change made earlier in the period may still be sitting unbilled on the next invoice.

Operation: mutation PreviewSubscriptionChange($input: PreviewSelfServeAccountSubscriptionChangeInput!) { previewSelfServeAccountSubscriptionChange(input: $input) { effect cancelsScheduledChange chargedAt amountDue { amount currency } recurringTotal { amount currency } appliesNow { planCode planName planInterval optionalAddons { id name quantity } } scheduledChange { effectiveAt planCode planName planInterval optionalAddons { id name quantity } } } }Variables: { "input": { "accountId": "TjAwN0FjY291bnQxMjM0NQ", "planCode": "geo-growth", "planInterval": "Month", "optionalAddons": [{ "addonId": "geo-growth-extra-content-evals", "quantity": 2 }] } }
PreviewSubscriptionChangeTry in Explorer
GraphQL
mutation PreviewSubscriptionChange($input: PreviewSelfServeAccountSubscriptionChangeInput!) {
previewSelfServeAccountSubscriptionChange(input: $input) {
effect
cancelsScheduledChange
chargedAt
amountDue {
amount
currency
}
recurringTotal {
amount
currency
}
appliesNow {
planCode
planName
planInterval
optionalAddons {
id
name
quantity
}
}
scheduledChange {
effectiveAt
planCode
planName
planInterval
optionalAddons {
id
name
quantity
}
}
}
}

A change does not always land in one piece​

effect says which halves of the requested change are in play, and the two state fields spell them out:

effectappliesNowscheduledChangeWhat the customer sees
NoChangenullnullThe subscription already holds the requested state.
CancelsScheduledChangenullnullNothing new lands, but a pending scheduled change would be called off.
Immediatethe new statenullEverything takes effect now.
DeferredToTrialEndnullthe new stateEverything takes effect when the trial ends.
DeferredToPeriodEndnullthe new stateEverything takes effect at the end of the paid period.
Splitpart of itthe restSome of it now, the rest at the end of the paid period.

CancelsScheduledChange is the one to read carefully. The requested state is the state the subscription is already in, so nothing new lands — but the account has a change scheduled against it, and confirming discards that schedule. This is how a customer calls off a downgrade they have changed their mind about, so do not treat it as a no-op and disable the confirm button; read selfServeAccountSubscription.scheduledChange to show them what they would be calling off.

On a paying subscription, an upgrade or an add-on addition takes effect straight away, so the customer gets what they are paying for. A downgrade, an add-on removal and a billing interval change wait for the period already paid for to run out. One request can contain both, which is Split — adding one add-on while dropping another gives the customer the new one today and keeps the old one until the period ends.

A trial reverses this, so do not present the paying rules to an account still in one. A plan change on its own waits for the trial to end, even an upgrade. Any add-on change at all — adding or removing — converts the subscription to paying there and then and applies the whole requested state with it, because add-ons never run free for the rest of a trial. Read effect rather than deriving the date from the kind of change.

appliesNow and scheduledChange each carry the full plan, interval and add-on set as of that moment, so a confirmation screen can show what the customer gets today against what they get later without deriving either from the request it sent. scheduledChange is the same thing selfServeAccountSubscription.scheduledChange reports once the change has been made.

Losing a change you had scheduled​

cancelsScheduledChange is true when confirming would discard a change already scheduled against the subscription and leave nothing in its place. An upgrade calls a pending change off outright, and any change made alongside one rewrites it, so a customer can lose a downgrade they had scheduled without effect alone saying so — it reads Immediate either way. Where the preview reports a scheduledChange of its own, the replacement is visible and the flag is false.

Read selfServeAccountSubscription.scheduledChange alongside it to show the customer what they would be giving up.

When the money moves​

chargedAt is when the invoice is raised, and it is usually not the moment the change is made. An immediate change is applied immediately but billed on the next invoice: the proration rides along with the following period's charge, so chargedAt is normally the end of the current period. The exception is a change that ends a trial — buying an add-on during one converts the subscription to paid there and then, and that invoice is raised immediately.

amountDue is what that invoice comes to, tax included. For a change that applies now it is that invoice net of any credit balance on the customer.

For a deferred change it is what the boundary will bill: the recurring amount the subscription holds by then, plus anything one-off already waiting on that invoice. That second part matters — an immediate change made earlier leaves its proration pending on the next invoice rather than charging it at the time, so an account that changed plan recently owes it whatever else it does next.

recurringTotal is what the subscription bills per interval once the whole change has landed — the figure to show as "then X per month". One-off charges on the same invoice are never counted in it.

amountDue is null in two cases. The first is a Split change. Both halves settle on the same invoice, in a combination the payment provider will only price once the immediate half has actually been applied, so there is no figure we can stand behind before the fact. recurringTotal, appliesNow and scheduledChange are all still returned: show the customer what changes and when, and leave the one-off amount to the invoice.

The second is an immediate change made while another change is already scheduled against the subscription. Confirming discards that schedule and applies the new state right away, and the prorated part of that cannot be priced ahead of time without making the change. recurringTotal is still exact — it is what the subscription bills once the dust settles.

Limits​

The preview needs the same Admin role as the change itself, and is rate limited per user — it is meant to be called when a customer settles on a combination, not on every keystroke as they explore. Debounce it behind the controls on your billing screen.

A preview is a snapshot. Proration is calculated per second, so an amount quoted and then left on screen while the customer hesitates drifts from what the invoice finally says. Re-run the preview if the confirmation step is not immediate.

A note on freshness​

state, capabilities and the preview's own classification all describe the subscription as Lumar last recorded it, which is why the first two cost nothing to read. An account is meant to hold one subscription at a time, but two checkouts completing together can leave it with more, and there the recorded one may not be the one Stripe is billing. If a mutation a capability offered is refused, re-query rather than treating it as final — and start from selfServeSubscriptionState, which asks Stripe directly.

The same applies to a preview. Its figures are priced against the live subscription, but which change it describes — the effect, and the states in appliesNow and scheduledChange — is worked out from the recorded one, so a change made elsewhere and not yet processed can leave a preview describing something other than what confirming would do. The change itself re-reads the subscription before acting, so it is the preview that lags rather than the outcome.