# When a Software Subscription Ends: Three Outcomes Your App Should Tell Apart

*Originally published on the [PermitCore blog](https://permitcore.dev/blog/what-happens-when-software-subscription-ends).*

"Your access has ended" can mean three very different things to the person staring at your app. A trial ran out. A subscription stopped renewing. Or the update period on a perpetual license is over, while the version they already own should keep working. If the app shows the same screen for all three, at least two of those customers get the wrong message.

This post walks through each case from the customer's side, then shows one small validation routine that tells them apart.

## Case 1: a trial that has run out

What the customer should see:

> Your trial has ended. Your projects are still here. Enter a paid license to enable export.

A good pattern is to keep the window, the license settings and the project viewer open, and gate only the paid action (export, in this example). Which actions stay available is your product decision; the license result tells the app *whether* the trial is still valid, and your code decides what that means for the UI. User files are never part of the license decision.

On the server side:

- Issue the trial key with an `ExpiresAt` date.
- For a hard cutoff, configure no grace period after expiry.
- After that date, an online validation returns `isValid: false` with `errorCode: "Expired"`.

Because expiry lives on the server and belongs to the key, reinstalling the app or clearing local data leaves the trial exactly where it was. If you want to offer a second trial, make that an explicit issuance policy rather than something a reinstall can trigger.

## Case 2: a subscription that has ended

What the customer should see:

> Your subscription access has ended. Open your account to renew, or enter another valid license. Your local files have not been removed.

Two moments look similar but deserve different messages:

1. **Payment trouble, still inside grace.** A failed renewal can open a grace window. For PermitCore Store subscriptions the payment grace defaults to 14 days, and you can change it per license or in the Store settings. During that window validation returns `isValid: true` together with `isInGracePeriod: true`, so the right UI is a friendly payment notice, not a lockout.
2. **Past the paid-through date and past any grace.** Now access has genuinely ended, and the message above applies.

Base the decision on the verified license result rather than on a checkout redirect or a billing timer stored in the app. Provider cancellation events can revoke a recurring license, and the server-side result always reflects that. The [Store reference](https://permitcore.dev/docs/store) documents the paid-through, grace and cancellation rules.

## Case 3: updates have ended, the owned version keeps running

What the customer should see:

> You can keep using version 1.8. Your update period has ended. Renew updates to use version 2.0.

This is the perpetual fallback model: the license has no access expiry, plus a maintenance (update) cutoff. The installed, eligible version validates successfully. A newer paid release is either refused or offered as an upgrade, according to your policy.

How to wire it up:

- PermitCore returns `MaintenanceExpiresAt` in the validation response.
- Keep a trusted release catalog with the publish date of every build, so you know which release was the last one inside the customer's update period.
- Set `MaxVersion` on the license to that eligible release and pass the running version when you validate. The eligible build passes; a later one comes back as `VersionBlocked`.
- Make the eligible installer easy to download again, and test project-file compatibility before you offer anyone a step back to an older build.

Treat the fallback as its own entitlement that you design on purpose: decide which version a buyer keeps and deliver it through your fulfilment flow, rather than expecting an ended subscription to turn into a perpetual license by itself. Revocation, product checks and device limits keep applying throughout. The [perpetual fallback guide](https://permitcore.dev/blog/perpetual-fallback-license) goes deeper on this design.

## One check, three outcomes

Here is the idea with the Python SDK (2.1.1). This version insists on a fresh online answer, so offline caching is switched off. Load the key from secure local storage and pass your own product ID. Device activation happens once, on first install; validation afterwards does not use up another seat.

```python
from permitcore import PermitCoreClient

client = PermitCoreClient(
    "https://api.permitcore.dev",
    enable_offline_cache=False,
    timeout=3,
)

result = client.validate(
    key,
    version=running_version,
    expected_product_id=product_id,
)

if result.is_valid:
    enable_licensed_features(result.features)
    if result.is_in_grace_period:
        show_notice("Payment needs attention; grace access is active.")
elif result.error_code == "Expired":
    show_license_screen("License expired. Renew or enter a paid key.")
elif result.error_code == "VersionBlocked":
    show_license_screen("Use your eligible version or renew updates.")
elif result.error_code == "RequestFailed":
    show_retry("Could not verify the license. Please retry.")
elif result.error_code:
    show_license_screen("License not accepted. Check your account.")
else:
    show_retry("Could not verify the license. Please retry.")
```

`show_notice`, `show_license_screen` and `show_retry` are your app's own UI callbacks. A few details worth keeping:

- **Trial wording comes from your app.** You know the key was a trial from your own context, so choose "your trial has ended" there.
- **No answer is not a "no".** An unreachable server tells you nothing about the subscription, which is why `RequestFailed` leads to a retry, never to a lockout message.
- **Offline support stays bounded.** If your app works offline, accept only verified signed proof that is still within its own expiry and matches the device and product. A perpetual license and an offline token are two separate deadlines.

## Test every cutoff before customers hit it

Create synthetic licenses and run through each state on purpose:

1. expired trial
2. subscription inside grace
3. subscription after grace
4. eligible version on a perpetual fallback license
5. newer, blocked version on the same license
6. revoked key
7. server unreachable

For each one, check the exact message the customer sees and confirm their files are untouched. The [validation reference](https://permitcore.dev/docs/api/validate), [license controls](https://permitcore.dev/docs/license-controls) and the [startup check guide](https://permitcore.dev/blog/license-check-at-app-startup) cover the underlying pieces.

---

Which of these three endings causes the most support email in your app today? I'd like to hear how you word it. 🙂

