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 stillactive- deniedwindow_elapsed - past
grace_ends_at, status stillgrace- deniedgrace_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:
reason | Meaning |
|---|---|
no_subscription | The user has no subscriptions at all |
feature_not_in_plan | Feature not in the plan's features or grandfathered set, and no agent lease covers it |
not_yet_active | Fixed starts_at is still in the future |
window_elapsed | Past expires_at; the worker has not swept it yet |
grace_ended | In grace but past grace_ends_at |
paused / cancelled / expired | Status blocks access |
usage_exhausted | included_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.