Subscription lifecycle
This guide describes SaaS subscription states and handling for renewal, cancellation, plan changes, and expiry events.
Commercial SaaS subscriptions and Application API access grants are independent. Cancelling either does not automatically cancel the other or issue a refund.
State handling
Free plans require explicit activation on a published offer. They return billing_mode: free without payment, so active can also describe a free subscription.
Upgrade summaries include upgrade_from_subscription_id. The original free subscription remains active until payment is confirmed. Update each (subscription_id, resource_version) separately. An old subscription's termination cannot overwrite its replacement. After out-of-order events, query current subscriptions and member access again.
Closing new free activation preserves historical orders. Subscription events omit payment references, renewal processing and scheduling fields. Query amounts and payment outcomes through authorized order APIs.
Subscription event data contains the current commercial subscription summary. Use it to update your local subscription record. It is not a list of changed fields and does not include member permissions. Check current member access separately before enabling product features.
| Commercial state | Meaning | Integration behavior |
|---|---|---|
draft / pending_payment | Draft or awaiting payment | Show preparation/payment status; do not enable features from this alone |
trialing / trial_ending | Trial running or nearing its end | Display trial_end; check current authorized access |
active | Commercial subscription effective | Display the current plan/period; verify member access separately |
renewal_pending | Renewal window open | Display the reminder; never extend the current period locally |
payment_failed / past_due / grace_period | Payment failed, overdue, or in grace | Guide recovery; never extend grace_deadline locally |
plan_change_pending | Plan change not yet effective | Display current and scheduled plans; do not apply new entitlements early |
pause_pending / paused | Pause scheduled or effective | Distinguish scheduled and effective time; check current access |
cancellation_pending | Cancellation scheduled | Display cancel_at; do not treat it as already cancelled |
cancelled / expired | Cancellation effective or subscription expired | Restrict access and reconcile; offer resubscription and preserve history |
suspended | Commercial subscription restricted | Restrict access; allow only approved recovery/support operations |
terminated | This subscription terminated | Stop relying on this source; reconcile current member access and any replacement subscription, preserving history |
See the complete subscription event table for names, triggers, allowed data.status values, and related fields. Only approved and selected events are delivered; catalog coverage does not mean your application receives every event.
Scenario 1: activation → scheduled cancellation → cancellation effective
A customer purchases a monthly plan from October 6 to November 6, then schedules cancellation at period end on October 20. This is the business timeline for sub_demo_cancel, not a guaranteed network arrival order:
| Business time (UTC) | Complete event name | Version | Interpretation |
|---|---|---|---|
| October 6, 00:00 | oem.subscription.activated.v1 | 2 | Commercial state is active; see the complete HTTP request |
| October 20, 08:00 | oem.subscription.cancellation_scheduled.v1 | 3 | Cancellation is scheduled for November 6; do not display “cancelled” yet |
| November 6, 00:00 | oem.subscription.cancelled.v1 | 4 | Cancellation is effective; ended_at records the end; stop granting access from stale commercial state |
Complete scheduled-cancellation event
{
"event_id": "evt_demo_cancellation_scheduled",
"event_type": "oem.subscription.cancellation_scheduled.v1",
"schema_version": "1",
"occurred_at": "2026-10-20T08:00:00Z",
"producer": "opaque",
"tenant_id": "org_customer_demo",
"resource_id": "sub_demo_cancel",
"resource_version": 3,
"correlation_id": "req_demo_cancellation",
"data": {
"id": "sub_demo_cancel",
"organization_id": "org_customer_demo",
"open_platform_application_id": "app_demo_saas",
"environment": "sandbox",
"product_tenant_id": "tenant_demo_cancel",
"party_id": "party_customer_demo",
"order_id": "order_demo_initial",
"product_code": "relaydesk",
"plan_code": "basic_monthly_demo",
"plan_version": 1,
"status": "cancellation_pending",
"current_period_start": "2026-10-06T00:00:00Z",
"current_period_end": "2026-11-06T00:00:00Z",
"cancel_at": "2026-11-06T00:00:00Z",
"cancel_at_period_end": true,
"status_effective_at": "2026-10-20T08:00:00Z",
"operation_id": "op_demo_cancellation",
"reason_code": "example_customer_cancellation",
"resource_version": 3,
"created_at": "2026-10-05T23:55:00Z",
"updated_at": "2026-10-20T08:00:00Z"
}
}status_effective_at records entry into cancellation_pending; cancel_at records when cancellation takes effect. cancel_at_period_end: true means period-end cancellation in this example. plan_code remains the current plan. This neither proves a refund nor revokes API access.
Complete effective-cancellation event
{
"event_id": "evt_demo_cancelled",
"event_type": "oem.subscription.cancelled.v1",
"schema_version": "1",
"occurred_at": "2026-11-06T00:00:00Z",
"producer": "opaque",
"tenant_id": "org_customer_demo",
"resource_id": "sub_demo_cancel",
"resource_version": 4,
"correlation_id": "req_demo_cancellation",
"data": {
"id": "sub_demo_cancel",
"organization_id": "org_customer_demo",
"open_platform_application_id": "app_demo_saas",
"environment": "sandbox",
"product_tenant_id": "tenant_demo_cancel",
"party_id": "party_customer_demo",
"order_id": "order_demo_initial",
"product_code": "relaydesk",
"plan_code": "basic_monthly_demo",
"plan_version": 1,
"status": "cancelled",
"current_period_start": "2026-10-06T00:00:00Z",
"current_period_end": "2026-11-06T00:00:00Z",
"cancel_at": "2026-11-06T00:00:00Z",
"status_effective_at": "2026-11-06T00:00:00Z",
"ended_at": "2026-11-06T00:00:00Z",
"operation_id": "op_demo_cancellation",
"reason_code": "example_customer_cancellation",
"resource_version": 4,
"created_at": "2026-10-05T23:55:00Z",
"updated_at": "2026-11-06T00:00:00Z"
}
}Version 4 is a newer summary of the same resource. If it arrives before version 3, persist the gap and reconcile; a late version 3 must never restore access. Cancellation and expiry are different terminal reasons: do not rewrite every cancelled state as expired.
Scenario 2: scheduled plan change → new plan effective
Another subscription, sub_demo_plan, currently uses basic_monthly_demo. The customer schedules pro_monthly_demo for the next period:
| Stage | Complete event name | Fields and handling |
|---|---|---|
| Scheduled, version 6 | oem.subscription.plan_change_scheduled.v1 | status=plan_change_pending; plan_code remains current; pending_plan_code=pro_monthly_demo, pending_plan_version=2, and pending_plan_effective_at=2026-11-06T00:00:00Z describe the schedule, not immediately effective entitlements |
| Effective, version 7 | oem.subscription.plan_changed.v1 | plan_code=pro_monthly_demo, plan_version=2 become current; this example is active, and member access is checked separately |
The complete event at effect follows. This is a different subscription; do not append it to the cancellation timeline:
{
"event_id": "evt_demo_plan_changed",
"event_type": "oem.subscription.plan_changed.v1",
"schema_version": "1",
"occurred_at": "2026-11-06T00:00:00Z",
"producer": "opaque",
"tenant_id": "org_customer_demo",
"resource_id": "sub_demo_plan",
"resource_version": 7,
"correlation_id": "req_demo_plan_change",
"data": {
"id": "sub_demo_plan",
"organization_id": "org_customer_demo",
"open_platform_application_id": "app_demo_saas",
"environment": "sandbox",
"product_tenant_id": "tenant_demo_plan",
"party_id": "party_customer_demo",
"order_id": "order_demo_plan_change",
"product_code": "relaydesk",
"plan_code": "pro_monthly_demo",
"plan_version": 2,
"status": "active",
"current_period_start": "2026-11-06T00:00:00Z",
"current_period_end": "2026-12-06T00:00:00Z",
"status_effective_at": "2026-11-06T00:00:00Z",
"operation_id": "op_demo_plan_change",
"reason_code": "example_plan_change_effective",
"resource_version": 7,
"created_at": "2026-10-05T23:55:00Z",
"updated_at": "2026-11-06T00:00:00Z"
}
}The event may omit the previous plan. Compare your previously verified subscription record with the new summary to display changes. Query amounts and access separately; do not add missing old_plan, new_plan or other fields.
plan_version identifies the plan definition version; resource_version identifies the subscription version. Use each for its own resource.
Scenario 3: renewal failure → grace → expiry
The current period for sub_demo_expiry ends on November 6. In this example, renewal fails and the grace period ends on November 9. Use timestamps from the response rather than a fixed three-day period:
| Stage | Complete event name | Integration behavior |
|---|---|---|
| Renewal window opens | oem.subscription.renewal_upcoming.v1 | Read renewal_window_start; a reminder does not prove renewal |
| Payment fails | oem.subscription.payment_failed.v1 | Read the actual status: payment_failed, past_due, or grace_period |
| Overdue/grace | oem.subscription.past_due.v1 | Display grace_deadline; query current access |
| Recovery succeeds (alternative branch) | oem.subscription.renewed.v1 | Summary becomes active; read new period boundaries rather than adding a month locally |
| No recovery, expiry (this example) | oem.subscription.expired.v1 | Commercial subscription ends; restrict access, reconcile, and preserve history |
The expiry event includes the full period and grace timestamps:
{
"event_id": "evt_demo_expired",
"event_type": "oem.subscription.expired.v1",
"schema_version": "1",
"occurred_at": "2026-11-09T00:00:00Z",
"producer": "opaque",
"tenant_id": "org_customer_demo",
"resource_id": "sub_demo_expiry",
"resource_version": 7,
"correlation_id": "req_demo_renewal_failure",
"data": {
"id": "sub_demo_expiry",
"organization_id": "org_customer_demo",
"open_platform_application_id": "app_demo_saas",
"environment": "sandbox",
"product_tenant_id": "tenant_demo_expiry",
"party_id": "party_customer_demo",
"order_id": "order_demo_renewal",
"product_code": "relaydesk",
"plan_code": "basic_monthly_demo",
"plan_version": 1,
"status": "expired",
"current_period_start": "2026-10-06T00:00:00Z",
"current_period_end": "2026-11-06T00:00:00Z",
"grace_deadline": "2026-11-09T00:00:00Z",
"status_effective_at": "2026-11-09T00:00:00Z",
"ended_at": "2026-11-09T00:00:00Z",
"operation_id": "op_demo_renewal",
"reason_code": "example_grace_expired",
"resource_version": 7,
"created_at": "2026-10-05T23:55:00Z",
"updated_at": "2026-11-09T00:00:00Z"
}
}These JSON examples are complete business events after removing the transport Verification Token. All IDs, plans, times, and reason codes are synthetic. producer: "opaque" is an opaque-source placeholder; use the issued schema for actual producer enums and reason-code semantics. Download scheduled cancellation, effective cancellation, plan change, and expiry for implementation checks.
Update subscription state
After a paid upgrade is confirmed, assigned members and group sources from the original free subscription move to the new subscription. Overlapping sources still count each member once. Product fulfillment and current member access checks remain required. Later subscription revisions do not restore explicitly revoked sources.
The status API and resource_version are authoritative. Never let an older webhook overwrite newer state, and never extend access locally because an event has not arrived.
If the local version is N and N+2 arrives first, save the event and wait for N+1. After the API-defined waiting period, check through an authorized snapshot or status API.
Also select oem.subscription.updated.v1. An accepted cancellation or restoration request first produces this event with a new revision and the actual current state. It does not prove payment-provider confirmation. Only confirmation produces oem.subscription.cancellation_scheduled.v1 or oem.subscription.resumed.v1. Preserve the current period while waiting; do not display cancellation or restoration as complete. Selecting only the older terminal events can leave intermediate revisions missing.
Re-subscribing creates a new subscription and order. Do not change an expired record directly back to active, and do not delete historical payment/refund evidence or audit records.
The current third-party SaaS commercial plan supports a fixed monthly or yearly usage fee. Stored-value balances, usage billing, and overage charges are not offered.
Handle member access after subscription changes
Successful sign-in completes authentication. oem.subscription.activated.v1 activates a commercial subscription; oem.product.activated.v1 activates organization-level product access. None enables a member by itself.
- On
oem.access_context.changed.v1, first restrict the target member’s previous permissions. - Use the current application service identity with
openplatform.api.invoketo callPOST /open-platform/v1/applications/{id}/events/{event_id}/access-snapshotwith{}. The customer and member come from the original event recorded for this application; callers cannot select another customer. Do not substitute the current signed-in user context endpoint. - Match Application, environment, purchasing Organization, and product exactly. Apply current
roles,entitlements,capabilities, andaccess_revisiononly when the member'sproduct_access_allowedis booleantrueand the identity, Membership, and ProductAccess are valid. - Clear local effective permissions when
product_access_allowedisfalse. Keep access restricted for missing or non-boolean values, uncertain versions or failed queries. Organization purchase, aVIPRole oractiveMembership alone does not establish access.
Commercial resource_version and member access_revision belong to different resources; never compare them with each other. Subscription and access events can retry or arrive out of order independently. Handle each resource's own versions. See effective access changes for fields.
Enable the member-access feature for the application. Readback rejects an inactive service identity, application, environment, original event or feature. Keep the event and retry a temporary dependency failure. See Product access snapshot for requests and failure handling.
Subscriptions and users
Correlate subscriptions by Application, environment, customer space and subscription ID. A shared user alias does not merge subscriptions. Renewal does not depend on the last signed-in person; verify member access independently. See Applications, users and events.