Entitlements
The on/off switch for a user’s access to an app. See Domain Model → Entitlements for the full field reference and status lifecycle.
- Endpoints
GET /v1/users/{id}/entitlementsPOST /v1/users/{id}/entitlementsGET /v1/entitlementsGET /v1/entitlements/{id}PATCH /v1/entitlements/{id}— the toggleDELETE /v1/entitlements/{id}- Errors specific to this resource
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/users/{id}/entitlements |
List a user’s entitlements — this is what the hub dashboard renders as “your apps.” |
POST |
/v1/users/{id}/entitlements |
Grant access to an app (admin_grant, or internal use by the billing webhook handler for purchase). |
GET |
/v1/entitlements |
Admin/support view. List across all users, filterable by application_id, status, user_id, source. The “who has app X enabled” query. |
GET |
/v1/entitlements/{id} |
Fetch one entitlement by its own ID. |
PATCH |
/v1/entitlements/{id} |
Change status. This is the on/off toggle. |
DELETE |
/v1/entitlements/{id} |
Hard-remove a grant made in error. Distinct from setting status: revoked — see below. |
GET /v1/users/{id}/entitlements
// Response — 200
{
"data": [
{
"id": "ent_01JAG6R2N7HX0K9T4V5W6Y7Z8A",
"application_id": "app_timetrack",
"status": "active",
"source": "purchase",
"order_id": "ord_01JAG5D1C2E3F4G5H6J7K8L9M0",
"starts_at": "2026-01-14T18:05:00Z",
"ends_at": null
},
{
"id": "ent_01JAG9F4Q1W2E3R4T5Y6U7I8O9",
"application_id": "app_invoicer",
"status": "disabled",
"source": "purchase",
"order_id": "ord_01JAG8B3N4M5K6J7H8G9F0D1S2",
"starts_at": "2026-03-02T09:00:00Z",
"ends_at": null
},
{
"id": "ent_01JAGD4E5F6G7H8J9K0L1M2N3O",
"application_id": "app_payroll",
"status": "active",
"source": "org_seat",
"order_id": null,
"starts_at": "2026-04-02T10:00:00Z",
"ends_at": null,
"granted_via": { "organization_id": "org_01JAFZ8Y7X6W5V4U3T2S1R0Q9P", "member_decision": "included" }
}
],
"page": { "next_cursor": null, "has_more": false }
}
granted_via appears only on an org_seat-sourced row — see Domain Model → Entitlements → Attribution for what member_decision (included/excluded) means and why it’s computed per member rather than stored on the org-wide grant itself.
POST /v1/users/{id}/entitlements
// Request
{
"application_id": "app_invoicer",
"source": "admin_grant"
}
// Response — 201
{
"id": "ent_01JAGA1B2C3D4E5F6G7H8J9K0L",
"application_id": "app_invoicer",
"status": "active",
"source": "admin_grant",
"order_id": null,
"starts_at": "2026-10-03T12:00:00Z",
"ends_at": null
}
Requires Idempotency-Key — see Conventions → Idempotency. Requires a platform role with entitlements.manage for source: admin_grant; the billing webhook handler calls this same endpoint internally (via a service API key scoped to entitlements.manage) with source: "purchase" and order_id set, keyed on the order ID so a retried webhook never double-grants.
GET /v1/entitlements
GET /v1/entitlements?application_id=app_invoicer&status=active
// Response — 200
{
"data": [
{
"id": "ent_01JAG6R2N7HX0K9T4V5W6Y7Z8A",
"user_id": "usr_01JAG3Z9X8QS3F6K2M4N5P6R7S",
"application_id": "app_invoicer",
"status": "active",
"source": "purchase",
"order_id": "ord_01JAG5D1C2E3F4G5H6J7K8L9M0",
"starts_at": "2026-01-14T18:05:00Z",
"ends_at": null
}
],
"page": { "next_cursor": null, "has_more": false }
}
Requires a platform role with entitlements.manage. This is the endpoint a support dashboard calls to answer “who currently has app X” or “show me every disabled entitlement from the last billing dispute” — the per-user list above doesn’t support that cross-user query.
GET /v1/entitlements/{id}
// Response — 200
{
"id": "ent_01JAG6R2N7HX0K9T4V5W6Y7Z8A",
"user_id": "usr_01JAG3Z9X8QS3F6K2M4N5P6R7S",
"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
}
Self (for one’s own entitlement), or a platform role with entitlements.manage.
PATCH /v1/entitlements/{id} — the toggle
// Request — turn off
{ "status": "disabled", "disabled_reason": "billing_dispute" }
// Response — 200
{ "id": "ent_01JAG9F4Q1W2E3R4T5Y6U7I8O9", "status": "disabled", "disabled_reason": "billing_dispute", "...": "..." }
// Request — turn back on
{ "status": "active", "disabled_reason": null }
// Request — on an org-wide grant, narrow who it applies to
{ "member_scope": "denylist", "member_overrides": ["usr_01JAG3Z9X8QS3F6K2M4N5P6R7S"] }
Requires a platform role with entitlements.manage (e.g. support or superadmin — see Roles & Permissions), or — for an org_seat row specifically — org-admin standing on that Organization (see Organizations → Delegated admin). Writes an Audit Event and emits entitlement.disabled / entitlement.granted on a status transition. Changing only member_scope/member_overrides (no status change) is a distinct, narrower write — it emits entitlement.member_scope_changed instead, since no member’s resolved access path was necessarily added or removed by this call alone; see Domain Model → Orders & Audit → Action catalog.
status transitions are not unrestricted — revoked is terminal; you cannot PATCH a revoked entitlement back to active. Grant a new one instead. See Domain Model → Entitlements → Status transitions.
DELETE /v1/entitlements/{id}
Requires a platform role with entitlements.manage. Removes the record entirely — reserved for correcting a grant made by mistake (wrong user, wrong app, duplicate), where no Audit trail of a real access change should persist. For every other case — a real purchase ending, a real admin decision to cut off access — use PATCH with status: revoked or disabled instead, so the history survives in the Audit log.
This is enforced server-side, not left to caller discipline: rejected with 409/code: "entitlement_order_linked" whenever order_id is set, regardless of the calling key’s restrict_destructive setting — an order-linked Entitlement is never hard-deletable by anyone, because it’s never “a mistake” in the sense this endpoint exists for; it’s a real financial record. This is a universal gate on top of, not a replacement for, API Keys → Agent keys’s restrict_destructive check — a restrict_destructive: true key is blocked from calling this endpoint at all; a restrict_destructive: false or service-use key is still blocked from this one specific case.
Errors specific to this resource
| Code | When |
|---|---|
entitlement_already_exists |
A POST would create a duplicate active grant for the same (user_id, application_id). |
invalid_status_transition |
e.g. attempting to reactivate a revoked entitlement. |
entitlement_not_found |
{id} doesn’t resolve. |
entitlement_order_linked |
A DELETE targets an Entitlement with order_id set — use PATCH with status: revoked instead. |