Subscription
Entitlement timing without payments: admin-defined plans bundle policies (trials, grace periods, pauses, group seats, usage-capped time, agent seat leasing), a grant opens a time-boxed access window, a background lapse worker walks subscriptions through grace into expiry, and one check endpoint answers "is this user entitled right now".
When to reach for it: SaaS-style plan tiers, free trials with grace periods, family/team seats, metered "included minutes", delegating a scope to an autonomous agent for a bounded window.
When not to: one-off purchases belong to Cart; free-form usage metering to Meter; locking a shared resource (not an entitlement) to Lease. There is no billing here - the grant itself is the whole "purchase".
Concepts
- Plans are policy bundles - features plus policies for the access
window, trial, grace, pause, group seats, usage caps, agent leasing,
upgrades, and grandfathering. Plan management is platform-admin only,
and
plan_idis generated - it is not the name. - A grant creates a subscription - an access window from
starts_at(defaults to now; may be fixed in the past or future) lastingduration_days, falling back to the plan default, then to 30. Users grant themselves; granting another user or agroup_idis admin-only. - One entitlement question -
POST /api/v1/subscriptions/checkscans the subscriber's subscriptions (owned plus seat memberships) and returnsallowedwith a machine-readable denialreason. It never mutates. - Statuses move on a timer -
active/trialinglapse intograceand thenexpiredvia a background worker; the full timeline is on Lapse, grace, and the entitlement check.
Plans (admin)
The CLI covers name, features, and default duration; the richer policies are set over HTTP:
snug subscription plan create --name Pro --feature advanced_api --default-duration-days 30
snug subscription plan list # q/sort_by/sort_order/page on the HTTP endpoint
snug subscription plan update --plan-id njtGtYfYpECGYYbjBqmh --retire truecurl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "docs4-pro", "features": ["advanced_api", "priority_jobs"],
"trial": {"enabled": true, "duration_days": 14, "one_trial_per_user": true},
"grace_period": {"enabled": true, "duration_days": 3, "access_level": "limited"}}' \
http://localhost:4000/api/v1/subscriptions/plans{
"status": 201,
"msg": "Created",
"data": {
"plan_id": "njtGtYfYpECGYYbjBqmh",
"name": "docs4-pro",
"status": "active",
"trial": { "enabled": true, "duration_days": 14, "one_trial_per_user": true, ... },
...
}
}Unset policies default to disabled. Plans are never deleted - retiring one
blocks new grants (400 invalid_request "plan is retired") while existing
subscriptions keep running.
Grant and check
snug subscription grant docs-wave --plan-id njtGtYfYpECGYYbjBqmh --duration-days 30 --reason "docs example"
snug subscription batch-grant --file grants.json # JSON array of grant objects, capped per request
snug subscription list docs-wave
snug --output json subscription check docs-wave --feature advanced_apiAn allowed check returns the matching subscription_id, plan_id,
status, and expires_at; a denial returns allowed: false plus a
reason - feature_not_in_plan for a feature outside the plan,
no_subscription for a user with none. Checking or listing another user
is 403 insufficient_permissions without an admin token. The full denial
catalog is on the lapse page.
Trials, pause, and resume
snug subscription trial-start --subscription-id FDRcjGyqrBrvsSrGQvjY --duration-days 5
snug subscription pause --subscription-id FDRcjGyqrBrvsSrGQvjY --reason "vacation"
snug subscription resume --subscription-id FDRcjGyqrBrvsSrGQvjYtrial-start rewrites the access window to the trial window and sets
status trialing. Verified: the plan must have trial.enabled; a
subscription trials once (400 trial_not_allowed on the second attempt);
with one_trial_per_user, cancelling and re-granting the same plan gets
409 trial_already_used.
Pausing requires pause_policy.enabled, denies checks with paused, and
is capped by max_pause_days_per_year. With extend_expiry_by_pause the
paused time is credited back: a verified 3-second pause moved expires_at
from 12:49:15 to 12:49:18. Resume restores trialing if a trial was
active, else active; double pause is 409 already_paused, resuming an
unpaused subscription 400 not_paused.
Group seats
With group_policy enabled on the plan, the owner manages a member list
capped by max_seats (the owner occupies the first seat):
snug subscription group add-member --subscription-id GMXayLNDxBUTrhQSfbJx --user-id docs4-other
snug subscription group remove-member --subscription-id GMXayLNDxBUTrhQSfbJx --user-id docs4-otherVerified with a second identity: after add-member, subscription list docs4-other shows the subscription and their entitlement check passes;
after the owner cancels, the member's check denies. Members cannot mutate
the subscription (403 insufficient_permissions). Duplicate adds are
409 member_already_exists, a full group is 400 seat_limit_reached,
the owner seat cannot be removed, and re-adding a removed user inside
seat_transfer_cooldown_days is 400 seat_transfer_cooldown.
Usage-capped time
With usage_capped_time enabled, consumption counts against the plan's
included_minutes per subscription window:
snug --output json subscription usage record --subscription-id gmXBVgJnEVsgTjTfeDAQ --minutes 1{ "consumed_minutes": 2, "exhausted": true, "included_minutes": 2,
"remaining_minutes": 0, "subscription_id": "gmXBVgJnEVsgTjTfeDAQ" }Once exhausted, checks deny with usage_exhausted even though the
subscription is still active. Recording against a plan without usage
tracking is 400 usage_not_allowed.
Upgrades (admin)
snug subscription upgrade --subscription-id prsquyzqMGNUqzcmZWZL --plan-id cKNdrZdAYmAKnHruJfwL --reason "sales upgrade"The subscription moves to the target plan and stays active. With
proration_mode: time_credit the remaining time is added to the new
plan's window (verified: 30 remaining days on a 90-day plan landed
expires_at 120 days out); the old plan's grandfathering keeps its
features passing checks via grandfathered_features. co_term_at pins
the new expiry to a shared date when the target plan allows co-terming.
Agent seat leases
A lease delegates plan-allowlisted scopes to an agent for a bounded time:
snug subscription lease create --subscription-id DztcRVUypVznwqVCHwJA \
--agent docs4-agent-verifier --minutes 15 --scope priority_jobs
snug subscription check docs-wave --feature priority_jobs --agent-id docs4-agent-verifier
snug subscription lease revoke --subscription-id DztcRVUypVznwqVCHwJA --lease-id LZzVeSLZPXsKHYYHtwNpVerified: without the lease the check denies feature_not_in_plan; with
it, the same check allows and carries lease_id. Scopes outside the
plan's allowed_scopes are 400 lease_scope_not_allowed, durations over
max_lease_minutes are 400 lease_duration_exceeded, and a lease never
outlives its subscription window. Expiry is automatic - see the
lapse page.
Limits and configuration
Default subscription duration (30 days), the lapse worker interval and
batch size, and batch-grant, seat, and lease ceilings are tunable via the
SUBSCRIPTION_* variables in the
Subscription CONFIG reference.
Plan listing takes the q/sort_by/pagination subset of the
shared search grammar, without filter.
Reference
- Subscription API - every endpoint, callable
- Related: Cart for one-off purchases, Meter for raw usage metering, Lease for distributed resource leases