Skip to main content
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.
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

In a Next app, import the hook from @userkit/nextjs — the same hook, and in proxy mode 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 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.