User tokens
Tell the assistant who the user is, and what they may do, from your own backend.
The SDK runs in a browser the user controls, so nothing it says about itself can be believed. Instead, your backend signs a short-lived token for the signed-in user, and the runtime reads their identity and permissions from its verified claims, and from nothing else.
The token
A JWT, signed with HS256 using the app's secret key: the whole sk_…
string, exactly as the dashboard showed it.
| Claim | Required | Meaning |
|---|---|---|
sub | yes | Your id for the user. It appears in the audit log and the usage feed. |
exp | yes | When it expires, in Unix seconds. |
permissions | no | The user's permission strings, matched against each action's permissions. Omitted means none. |
role | no | Accepted, but grants nothing. Permissions alone decide what the user may do. |
iat | no | When it was issued. If present, the lifetime is measured from it. |
A token may live one hour at most. Longer is refused, so a leaked token stops working before it is useful. Fifteen minutes is a good default. The SDK asks for a fresh one each time it connects, and again when the runtime says the current one has expired, so a short lifetime costs nothing.
Signing it
Mint it from your own session, with the user's real permissions, on every request. Never cache one token for everybody.
import jwt from "jsonwebtoken";
const token = jwt.sign(
{ sub: user.id, permissions: user.permissions },
process.env.MIHE_SECRET_KEY!,
{ algorithm: "HS256", expiresIn: "15m" },
);import os, time, jwt # PyJWT
token = jwt.encode(
{"sub": user.id, "permissions": user.permissions, "exp": int(time.time()) + 900},
os.environ["MIHE_SECRET_KEY"],
algorithm="HS256",
)Serve it from a route the SDK can call, and hand that route to
getUserToken:
<AssistantProvider
getUserToken={() => fetch("/api/mihe-token").then((r) => r.text())}
// …
/>The route must only answer for a signed-in user. It is how the assistant knows who is asking.
Keep the secret key on the server
Anyone holding the secret key can sign a token for any user, with any
permissions. Keep it in your backend's environment. Never put it in a browser
bundle or a NEXT_PUBLIC_-style variable. A session opened from a browser with
it is refused.
When a token is refused
The session is refused with one of these codes, and its message appears as lastError on useAssistant() and in the widget:
| Code | Cause |
|---|---|
invalid_user_token | Not signed with this app's secret key. Check you are using the key for this app. |
expired_user_token | Expired. Mid-session the SDK fetches a new one by itself; if it happens on connecting, check your server's clock and that your route signs a fresh token each time. |
user_token_lifetime_too_long | Valid for more than an hour. Sign shorter-lived tokens. |
user_token_missing_expiry | No exp claim. |
user_token_missing_subject | No sub claim. |
malformed_user_token | Not a JWT, or not HS256. |