Entitlements

The record behind “turn an app on/off for a user.” If this site has one load-bearing entity, it’s this one.

  1. What it represents
  2. Fields
    1. source
  3. Org-wide entitlements: scoping members in or out
    1. Attribution
  4. Status transitions
  5. This is the endpoint everything else points back to
  6. Example

What it represents

The join between a User (or Organization) and an Application: does this party own this app, and is it currently switched on. Independent of Role — see Access Control for why that split matters.

Fields

Field Type Notes
id string ent_ prefix.
user_id string, nullable Set unless this is an org-wide seat grant.
organization_id string, nullable Set for an org-wide grant — see source: org_seat below.
application_id string  
status enum active | disabled | expired | revoked. See Access Control → Step 1.
source enum purchase | trial | admin_grant | org_seat.
order_id string, nullable Set when source: purchase — links to the Order.
starts_at timestamp  
ends_at timestamp, nullable Set for trials and fixed-term subscriptions.
disabled_reason string, nullable Free text or enum, set when an admin flips status to disabled manually.
member_scope enum, nullable Only meaningful when source: org_seat. all_members (default) | allowlist | denylist. See Org-wide entitlements: scoping members in or out.
member_overrides array, nullable Only meaningful when member_scope is allowlist or denylist. Array of user_ids this org-wide grant’s default is flipped for.

source

Value When it’s used
purchase Standard commerce path — see Workflows → Purchase → access.
trial Time-boxed access, no payment yet. ends_at drives the transition to expired.
admin_grant Support/sales gave access directly — comped account, early access, internal testing. No order_id.
org_seat Granted implicitly because the user belongs to an Organization that owns the app. user_id is null — the grant is modeled at the org level and resolved per-member at read time, per member_scope below.

Org-wide entitlements: scoping members in or out

An org-wide (source: org_seat) Entitlement’s default is that every current and future member of the Organization gets the app — no per-member row to create or maintain. member_scope narrows that default, giving an org admin (OrganizationMembership.role: org_admin) a whitelist/exclusion list rather than all-or-nothing:

member_scope Who gets this grant
all_members (default) Every current and future member — unchanged behavior.
allowlist Only the members listed in member_overrides. Everyone else in the Organization does not get this app through this grant.
denylist Every member except the ones listed in member_overrides.

This governs only this one Organization’s own grant. It does not reach into, and cannot revoke, a member’s separate Entitlement to the same Application sourced some other way (a personal purchase, a trial, an admin_grant) — see Decisions → Organization-vs-User entitlement precedence for why that scoping is deliberate. A User’s actual access to an app is always the union of every active path available to them; excluding someone from one Organization’s seat grant only removes that path.

Attribution

Reading a member’s own entitlements (GET /v1/users/{id}/entitlements) surfaces why an org-sourced row is what it is, not just the resulting status — so anyone pulling a User’s data can see which Organization is responsible:

{
  "id": "ent_01JAGD4E5F6G7H8J9K0L1M2N3O",
  "user_id": null,
  "organization_id": "org_01JAFZ8Y7X6W5V4U3T2S1R0Q9P",
  "application_id": "app_invoicer",
  "status": "active",
  "source": "org_seat",
  "member_scope": "denylist",
  "member_overrides": ["usr_01JAG3Z9X8QS3F6K2M4N5P6R7S"],
  "granted_via": {
    "organization_id": "org_01JAFZ8Y7X6W5V4U3T2S1R0Q9P",
    "member_decision": "excluded"
  }
}

granted_via is computed per member at read time (it’s not stored — member_scope + member_overrides on the org-wide row is the source of truth) and only appears when the caller is asking about a specific member’s resolved standing, e.g. via GET /v1/users/{id}/entitlements, rather than the raw org-wide grant itself.

Status transitions

            ┌─────────────┐
 grant ───► │   active    │ ◄────────┐
            └──────┬──────┘          │ admin re-enables
                    │ admin disables │
                    ▼                │
            ┌─────────────┐          │
            │  disabled   ├──────────┘
            └─────────────┘

active ──(ends_at passes)──► expired   (trial/subscription lapse)
active ──(refund / ToS)────► revoked   (ownership itself removed)

disabled is the soft, reversible toggle — support flips it back to active and everything (Roles, AppProfile, AppSettings for that app) is exactly as it was. revoked means the underlying ownership is gone; restoring access later means a brand-new Entitlement, not reinstating this one.

This is the endpoint everything else points back to

PATCH /v1/entitlements/{id}
{ "status": "disabled", "disabled_reason": "billing_dispute" }

is the entire “turn this app off for this user” feature. See API Reference → Entitlements for the full endpoint list, and Workflows → Admin turns an app off for a user for the end-to-end sequence including the Audit Event and webhook it triggers.

Example

{
  "id": "ent_01JAG6R2N7HX0K9T4V5W6Y7Z8A",
  "user_id": "usr_01JAG3Z9X8QS3F6K2M4N5P6R7S",
  "organization_id": null,
  "application_id": "app_timetrack",
  "status": "active",
  "source": "purchase",
  "order_id": "ord_01JAG5D1C2E3F4G5H6J7K8L9M0",
  "starts_at": "2026-01-14T18:05:00Z",
  "ends_at": null,
  "disabled_reason": null
}

Back to top

Substratal Apps Platform API — living specification. This site is the system of record; see git history for how it has changed over time.

This site uses Just the Docs, a documentation theme for Jekyll.