Entitlement
This capability is granted by an API key scoped to the `` product (product face). See the product reference below.
Shared consumer entitlement/paywall capability. A (user_id, content-item, mode/tier)-scoped consumer access-DECISION engine (modes hard/soft/metered/time_based/entitlement) + a per-consumer metered free-quota with period reset + a per-category gate-rule engine + the canonical tier ladder + a unified consumer subscription state machine (create/supersede/cancel/pause/resume/prorate) + windowed entitlement grants. On a gate requiring payment the charge is DELEGATED to billing-metering (/v1/charge) and its 402 is relayed — this capability never re-implements charging. Distinct from billing-metering's (organization_id, metric) B2B tenant-metering.
- Group: Commerce & revenue
- Contract:
contracts/entitlement/v1/openapi.yaml - Public base:
https://api.vagarylabs.com(the consolidated API gateway — one host, per-brand siblingapi.<zone>) - Auth: a product API key (
vgk_…) issued from the console —Authorization: Bearer vgk_… - Product face (customer-keyed):
https://api.vagarylabs.com/product/v1/entitlement/check
Endpoints
| Method | Path | Summary |
|---|---|---|
POST | /v1/access/check | The consumer access decision for a content item. Resolves the effective gate mode (a category rule overrides the org default; a free item with no active rule short-circuits to grant) then dispatches on mode. Item facts (access_tier/category/published_at) and the consumer user_id are CALLER-SUPPLIED inputs (I4 — the capability never fetches platform content). Returns {has_access, reason, preview, required_tier, mode, remaining?}. |
POST | /v1/view/{item_id} | Record a metered content view for a consumer (advances the metered free-quota); returns remaining |
GET | /v1/remaining | The consumer's remaining free-quota this period |
POST | /v1/reset-usage/{user_id} | Reset (clear) a consumer's metered usage |
GET | /v1/config | The org's gate config (enabled, free_article_limit, reset_period, default_mode, time_window_hours) |
PUT | /v1/config | Update the org's gate config (persistent per-org; the platform kept this in-memory) |
GET | /v1/tiers | The canonical tier ladder (mechanism — order + labels; PRICES/FEATURES are domain packaging) |
GET | /v1/rules | List the org's per-category gate rules |
PUT | /v1/rules/{category} | Set (upsert) a per-category gate rule {mode, required_tier, enabled} — the domain override point |
DELETE | /v1/rules/{category} | Remove a per-category gate rule |
POST | /v1/memberships | Create (supersede-on-create) a consumer subscription — at most one active per (org, user). If a payment block is present the charge is DELEGATED to billing-metering FIRST (fail-closed — no active subscription is written unless the charge is actually COLLECTED); a billing 402/422 is relayed verbatim, and a charge that only created a hosted checkout (requires_action) is relayed as 402 with its hosted_url rather than granting access. |
GET | /v1/memberships/active | The consumer's active subscription (by user_id) |
DELETE | /v1/memberships/active | Cancel the consumer's active subscription |
GET | /v1/memberships/{subscription_id} | A subscription by id |
POST | /v1/memberships/{subscription_id}/pause | Pause an active subscription |
POST | /v1/memberships/{subscription_id}/resume | Resume a paused subscription (shifts next_billing_date by the pause duration) |
POST | /v1/memberships/{subscription_id}/prorate | Change a subscription's plan with proration. tier_prices is DOMAIN pricing the caller passes (the capability hardcodes no price); it computes the prorated credit/charge over the remaining billing cycle. |
POST | /v1/entitlements | Grant a windowed / one-time entitlement to a consumer for an item (the entitlement-window mode). If a payment block is present the charge is DELEGATED to billing-metering FIRST (fail-closed; a billing 402 is relayed). ttl_seconds (or expires_at epoch) bounds the window; omit both for a permanent grant. |
GET | /v1/entitlements/check | Whether a consumer holds an active (non-expired) entitlement for an item |
GET | /health | Liveness + store/tenancy/billing-consume posture (never the DSN itself) |
GET | /metrics | Prometheus exposition |
Schemas
Error
| Field | Type | Description |
|---|---|---|
error | string | stable machine code (e.g. validation_error, not_found, unauthorized) |
reason | string | human-readable one-line explanation (no secrets/PII) |
detail | object | |
request_id | string |
Payment
Optional payment block — when present, the charge is DELEGATED to billing-metering /v1/charge (this capability re-implements no charging). A billing failure (402/422) is relayed and the gated action is NOT applied (fail-closed).
SEGMENT ROUTING (ADR-110 D3 + its E14 addendum): supply EITHER rail OR customer_country. With a country, BILLING-METERING selects the rail — India to razorpay (Vagary Labs LLP is the seller of record), rest of world to the Dodo Merchant of Record (the MoR becomes the seller, issues its own invoice, and our tax is suppressed). Entitlement does NOT contain that map: rail choice is a tax decision owned by the money layer, and a copy here would be a second authority for one fact (payment-rail-build-vs-buy.md §3 rules 4-5). NOTE for subscriptions: neither rail supports unattended renewal from a stored card, so each period needs a fresh customer-completed checkout — call billing-metering /v1/rails/select to read unattended_renewal_supported before designing a renewal.
| Field | Type | Description |
|---|---|---|
rail | string | billing-metering rail (paypal |
customer_country | string | ISO 3166-1 alpha-2 of the CUSTOMER — forwarded to billing-metering, which selects the rail. Unrecognised/absent resolves to the Merchant-of-Record rail (the fail-safe direction). |
select_rail | boolean | apply ADR-110 D3 routing with no country known (resolves to the MoR rail). Absent = false. |
amount_cents | integer | |
currency | string | |
recurring | boolean | |
idempotency_key | string |
AccessCheckRequest
| Field | Type | Description |
|---|---|---|
item_id | string | |
user_id | string | null/absent = anonymous consumer |
access_tier | string | the item's required tier (caller-supplied, I4) |
category | string | selects a per-category gate rule if one exists |
published_at | number | epoch seconds (time_based mode input) |
mode | string | optional per-request mode override |
AccessDecision
| Field | Type | Description |
|---|---|---|
has_access | boolean | |
reason | string | machine reason (free_content |
preview | boolean | may a preview be shown on denial |
required_tier | string | |
mode | string | |
remaining | integer | metered mode only — free views left after this one |
organization_id | string | |
item_id | string |
ViewRequest
| Field | Type | Description |
|---|---|---|
user_id | string |
Remaining
| Field | Type | Description |
|---|---|---|
user_id | string | |
limit | integer | |
used | integer | |
remaining | integer | |
reset_period | string |
GateConfig
| Field | Type | Description |
|---|---|---|
organization_id | string | |
enabled | boolean | |
free_article_limit | integer | |
reset_period | string | |
default_mode | string | |
time_window_hours | integer |
RuleCreate
| Field | Type | Description |
|---|---|---|
mode | string | |
required_tier | string | |
enabled | boolean |
Generated by scripts/gen-capability-docs.py from contracts/entitlement/v1/openapi.yaml — the contract IS the source of truth; edit the contract, not this page.