> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Entitlements in the SDK

> What the signed-in team is entitled to, and why an unknown answer is not a refusal.

An entitlement is what the **plan** allows, and it is a different question from
what a **person** is allowed. `getEntitlements()` answers the first, for the team the
current session is acting inside.

```ts theme={null}
const grant = await userkit.getEntitlements();

const seats = grant.features["seats"];
const roomForOneMore = seats?.unlimited || (seats?.limit ?? 0) > usedSeats;
```

```json theme={null}
{
  "customer_id": "9f2c…",
  "plan": "pro",
  "resolved": true,
  "features": {
    "seats": { "kind": "metered", "enabled": true, "limit": 25, "unlimited": false, "included": null },
    "projects": { "kind": "metered", "enabled": true, "limit": null, "unlimited": true, "included": null },
    "ai_credits": { "kind": "credit", "enabled": true, "limit": null, "unlimited": false, "included": 5000 },
    "sso": { "kind": "boolean", "enabled": false, "limit": null, "unlimited": false, "included": null }
  }
}
```

The features are **your** catalogue — the plans you sell, not ours. `kind` is a
plain string rather than a union for that reason: a kind you add must not need a
release of the SDK before a client can read it.

`limit` is a ceiling to compare usage against; `included` is a per-period grant to
add to a balance, and it is only ever set for the `credit` kind. They are two
fields because they are two questions.

**"No ceiling" travels as `unlimited: true`, never as an absent `limit`.** A
feature with `limit: null` and `unlimited: false` has no ceiling *recorded*, which
is a ceiling of zero rather than permission for an infinite one — a magic number
on the wire is one every client has to be told about, and a bare `null` is one
every client gets to interpret. Every feature in your catalogue appears here,
including the ones the current plan does not carry: those arrive `enabled: false`,
which is the answer rather than a gap.

## In React

```tsx theme={null}
import { useEntitlements } from "@userkit/react";

function SeatsPanel() {
  const grant = useEntitlements();
  if (!grant) return <Spinner />;          // not yet, or not knowable
  return grant.features["sso"]?.enabled ? <SsoSettings /> : <UpgradePrompt />;
}
```

In a Next app, import the hook from `@userkit/nextjs` — the same hook, and in
[proxy mode](/en/customer-auth/session-tokens) the read is forwarded by the handlers,
with the session in an httpOnly cookie on your own origin. The team is still resolved
and never named: `X-Customer-Id` rides through the handlers and identifies without
ever authorizing.

It follows the session: signing out clears it, and switching teams refetches for
the team the next request will act inside.

## The team is resolved, never named

The grant belongs to the customer the session resolved — the one
`setActiveCustomer()` named, or the contact's oldest membership when none was.
There is no parameter that names a customer, which is what makes a pasted id a
404 instead of somebody else's plan.

## Three answers worth branching on

* **404** — the session belongs to no team. An anonymous visitor gets this, and it
  is the true answer rather than a refusal: it is what tells a UI to offer
  creating a team.
* **403 `unverified_session`** — an identified but unverified session. What a paid
  account includes is not handed to an assertion anybody who knows an
  `external_id` could make from a browser.
* **503 `entitlements_unavailable`** — nothing is being refused. The grant could
  not be resolved right now, so ask again. The API answers no body at all rather
  than one full of `false`, because a page full of `false` would make the decision
  for you, silently, in the direction that closes your product during our outage.

`useEntitlements()` renders all three as `null`, which is the same rendering as
"still loading" on purpose: both mean *nothing to gate on yet*. **Never treat
`null` as a grant of nothing** — gate on `grant?.features[key].enabled` and let
`null` mean "not yet".

## This is the UI's gate, not the enforcement

A browser can be told anything. Read the same grant on your own backend — with a
server key, `GET /v1/customers/{id}/entitlements`, alongside the
[session token](/en/customer-auth/session-tokens) that proves who is asking — and
enforce it there. What `getEntitlements()` buys is a screen that does not offer a
button the plan will refuse.
