ServicesSubscription

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_id is 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) lasting duration_days, falling back to the plan default, then to 30. Users grant themselves; granting another user or a group_id is admin-only.
  • One entitlement question - POST /api/v1/subscriptions/check scans the subscriber's subscriptions (owned plus seat memberships) and returns allowed with a machine-readable denial reason. It never mutates.
  • Statuses move on a timer - active/trialing lapse into grace and then expired via 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 true
curl -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_api

An 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 FDRcjGyqrBrvsSrGQvjY

trial-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-other

Verified 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 LZzVeSLZPXsKHYYHtwNp

Verified: 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

On this page