getEntitlements() answers the first, for the team the
current session is acting inside.
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
@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 onesetActiveCustomer() 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 anexternal_idcould 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 offalse, because a page full offalsewould 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.