ServicesSubscription

Lapse, Grace, and the Entitlement Check

A subscription's timing is one line: starts_at opens the window, expires_at closes it, and if the plan has a grace period, grace_ends_at closes that. Statuses do not change by themselves - a background lapse worker (every 60 seconds by default) sweeps due subscriptions and leases, moving active/trialing to grace when the plan grants one, and grace (or graceless expiry) to expired. The entitlement check only reads.

Two admin endpoints expose the machinery - lapses due previews due candidates, lapses run sweeps them immediately (both require a platform admin token). The transcript below is real: a grant backdated two days with a one-day duration, swept twice.

snug subscription grant docs-wave --plan-id njtGtYfYpECGYYbjBqmh \
  --starts-at 2026-08-26T12:49:43Z --duration-days 1
snug --output json subscription lapses due     # admin
snug --output json subscription lapses run     # admin
{ "count": 1, "subscriptions": [
    { "due_at": "2026-08-27T12:49:43Z", "subscription_id": "tCZUKPBCbzYyTdnNaPFL" } ] }
{ "subscriptions_processed": 1,
  "subscriptions": [ { "id": "tCZUKPBCbzYyTdnNaPFL", "outcome": "grace" } ],
  "leases_processed": 0, "leases": [] }

A second sweep after the grace window elapses reports { "id": ..., "outcome": "expired" } - both transitions were observed live. Other outcomes are skipped (rescheduled or already moved) and missing (subscription deleted). Because the worker also runs on its own, a manual lapses run may find nothing to do.

Grace periods

Grace is plan policy: enabled, duration_days, and an access_level of full or limited - both keep allowed: true during grace, and the caller distinguishes them by reading status: "grace" from the check response and applying its own degraded experience. (access_level: none skips grace entirely: expiry goes straight to expired.) Verified during grace:

{ "allowed": true, "status": "grace", "plan_id": "njtGtYfYpECGYYbjBqmh",
  "expires_at": "2026-08-27T12:49:43Z", "subscription_id": "tCZUKPBCbzYyTdnNaPFL" }

The check is authoritative between sweeps

The check does not wait for the worker: it evaluates timestamps directly, so access ends the moment the window does, even if the status field still says active. Both gaps:

  • past expires_at, status still active - denied window_elapsed
  • past grace_ends_at, status still grace - denied grace_ended

Denial reasons

POST /api/v1/subscriptions/check walks the subscriber's subscriptions (owned plus seat memberships, up to a configured candidate cap) and returns the first that allows. On denial, reason reflects the last candidate examined. All reasons:

reasonMeaning
no_subscriptionThe user has no subscriptions at all
feature_not_in_planFeature not in the plan's features or grandfathered set, and no agent lease covers it
not_yet_activeFixed starts_at is still in the future
window_elapsedPast expires_at; the worker has not swept it yet
grace_endedIn grace but past grace_ends_at
paused / cancelled / expiredStatus blocks access
usage_exhaustedincluded_minutes fully consumed

Lease expiry

Agent seat leases ride the same sweep. A one-minute lease was created and left alone; within a worker cycle of its expires_at the lease flipped to expired, the parent subscription's seats.active_agent_leases emptied, and the agent-qualified check flipped from allowed: true (with lease_id) to feature_not_in_plan. Manual lease revoke has the same effect immediately. lapses run also reports these transitions in its leases array.

The exact request and response shapes are in the Subscription API reference.

On this page