Skip to content
AI Assistant

Guides

Recognize signed-in customers

Let the assistant trust who is signed in: publish your public key, have your API sign a short-lived proof, and wire getIdentity and refreshIdentity.

JWKSLaunch token120 sNonceUsed onceLoginLogoutSwitch
On this page

AI Assistant recognizes your signed-in customers without sharing an account database. Your API signs a short-lived proof for each one (a launch token — an ES256 JWT valid for 120 seconds with a one-time nonce), you publish the matching public key (JWKS) as the workspace's identity provider, and the widget calls getIdentity, identityChanged, and signedOut. Your mate then serves that customer's history and account tools — only theirs.

Two identities

  • Your team signs in to the Console with their own accounts.
  • Your customers never get a platform account. Each launch carries a proof your product signed, whose subject is your unchanging customer id.

1. Keys

Create an ES256 key pair. Publish the public key at https://yourdomain/.well-known/jwks.json with a kid; keep the private key on your server. ES256 is the reference algorithm; the allowed list is part of the registration.

2. Register the provider

Open Console → Identity — or call upsert_tenant_identity_provider — and enter:

FieldValue
Issueryour origin, for example https://yourdomain
JWKS URLhttps://yourdomain/.well-known/jwks.json
Audiencebusymate-ai
Workspace claimtenant_id, equal to your workspace id
Subject claimsub — the unchanging internal customer id
Max proof ageat most 120 seconds
Mint endpointthe URL of the endpoint from step 3

Save the draft, run the checks, publish. An incomplete sign-in setup blocks the release.

3. The mint endpoint

Your API exposes one endpoint requiring your own signed-in session, returning a freshly signed proof for that customer:

typescript
import { SignJWT, importJWK } from "jose";

// POST https://YOUR-PRODUCT-DOMAIN/api/bmai/identity — requires YOUR OWN logged-in product session.
app.post("/api/bmai/identity", requireSession, async (req, res) => {
  // Fresh on EVERY mint. Never persist the launch token/nonce in localStorage,
  // sessionStorage, cookies, React state, or module state: every assistant
  // launch consumes this pair exactly once.
  const nonce = typeof req.body?.nonce === "string" ? req.body.nonce : "";
  if (!/^[A-Za-z0-9_-]{32,200}$/.test(nonce)) return res.status(400).json({ error: "invalid_nonce" });
  const key = await importJWK(JSON.parse(process.env.AI_LAUNCH_PRIVATE_JWK), "ES256");
  const token = await new SignJWT({
      tenant_id: process.env.BMAI_TENANT_ID, // the tenant id shown in Console → Identity
      nonce,                               // equals the sibling field below
      name: req.user.displayName,          // optional low-sensitivity display claim
    })
    .setProtectedHeader({ alg: "ES256", kid: process.env.AI_LAUNCH_KEY_ID })
    .setIssuer(process.env.BMAI_ISSUER) // the Issuer you registered in Console → Identity
    .setAudience("busymate-ai")
    .setSubject(req.user.id)               // IMMUTABLE internal account id; never email/phone/session id
    .setJti(crypto.randomUUID())           // one-time (replay-protected)
    .setIssuedAt()
    .setExpirationTime("120s")               // <= registered max age (120s)
    .sign(key);
  res.set("Cache-Control", "no-store");
  res.status(201).json({ token, nonce, expiresIn: 120 });
});
  • The nonce arrives from the widget, must match ^[A-Za-z0-9_-]{32,200}$, and is echoed back.
  • Claims: iss, aud, sub, your workspace claim, nonce, a one-time jti, iat, exp within the registered max age.
  • Respond 201 with { token, nonce, expiresIn } and Cache-Control: no-store. Each pair is consumed exactly once.

4. Wire the widget

Define window.BusymateAI.getIdentity before the embed script loads. It returns a fresh { token, nonce } for a signed-in customer and null for a signed-out one. Call identityChanged() after login, token rotation and every account switch, which upgrades the live conversation in place, and signedOut() on logout, which ends the session and clears the transcript. Never keep a token or nonce in storage, cookies or state. Console → Integration renders the full embed snippet with your values.

5. Pick your platform

New native apps use official SDK identity v2. For web and compatibility transports, https://busymate.ai/sdk/v1/kit/ supplies identityChanged() and signedOut() bindings for the platform rows below.

You are buildingInstallationGuide
A website, any frameworkkit/web/busymate-identity.jsthis page
iOS (WKWebView)Official Swift package, distribution 1.0.1iOS SDK
Android (WebView)Official Gradle module, distribution 1.0.0Android SDK
Electron · Tauri · WebView2kit/desktop/preload.jsDesktop
React Nativekit/react-native/busymateIdentity.jsReact Native
Flutterkit/flutter/busymate_identity.dartFlutter
Your APIkit/backend/{node,php,python,go}Backend signing

An app WebView loading your OWN page is a fifth shape. The assistant asks that page, not your app; see Fix a signed-in customer who shows as a guest.

Three rules apply everywhere, and missing any is silent: register the bridge before the page loads, mint fresh on every ask (a proof is single-use, so a cached one is refused), and signal both directions (an unsignalled sign-out leaves a conversation in front of the next person at that device). Third-party cookie policy has no bearing on any of it.

6. Full-page open

For a hosted page instead of an embed, mint the same pair and open your address with the token and nonce in the URL fragment, never the query string; the hosted-handoff snippet in Integration shows the form.

The redirect handoff your own login page must complete

When a signed-out visitor clicks Sign in on your hosted page, the platform sends them to your registered loginUrl with return_to and a one-time bmai_nonce. On that same request your login page must complete the customer's sign-in, mint the pair with the received bmai_nonce, and redirect to the exact return_to with #bmai_token=<token>&bmai_nonce=<nonce> in the fragment. A login page that ignores return_to leaves the chat a guest; Console → Integration → Identity flags it.

7. Sign in inside the chat

A sign_in tool, as a page tool or on your connected server, signs a visitor in without leaving the conversation and re-mints the session in place. Without it, a signed-out visitor gets the redirect above, which is why loginUrl matters even if you add the tool later. See Let your users sign in from the chat.

8. Let the diagnostics sign in as a test account

Your mate can run its full diagnostics as one of your own test accounts (Console → Connections → Test account, or verify_tenant_private_tools), which needs a sign-in without a browser. Add the machine arm to your mint endpoint, or publish a sign_in tool: Sign identity tokens from your backend has both.

9. Acceptance checklist

Setup progress proves configuration, not that the flow works. Console → Identity → "Test identified launch" fetches your JWKS, checks a key exists for every registered algorithm, pushes an unsigned probe through the live verifier (which must refuse it), and reports whether the provider is published. Each failure names its reason, and the same check gates the release. Paste a real token your endpoint minted, with its nonce, to prove the last step; test_tenant_identity_provider is the MCP twin.

text
REQUIRED AUTH + HISTORY ACCEPTANCE — AI Assistant / your mate
Setup progress is configuration evidence; it does not prove this workflow.

[ ] Console -> Identity -> "Test identified launch" is GREEN on your provider row.
    (It fetches your JWKS and checks a key exists for every algorithm you
    registered, then pushes a deliberately unsigned probe through the same
    verifier the live launch uses and requires it to be REFUSED, and confirms the
    provider is in the draft AND the published revision. Do not eyeball the form.)
[ ] Signed out: getIdentity returns null and the assistant remains anonymous.
[ ] Login without reloading the host page: call identityChanged(); the frame becomes identified.
[ ] Every getIdentity call returns a different nonce AND JWT jti. No launch token or nonce is persisted.
[ ] The subject is the same immutable internal AI Assistant account id across sessions/devices — never email, phone, browser id, or session id.
[ ] Send an identified message; refresh the host page; the same conversation reappear.
[ ] That refresh produces no launch_replayed, invalid_token, or silent anonymous fallback.
[ ] Logout: call signedOut(); account data is unavailable and the frame is anonymous.
[ ] Login again as the same account: call identityChanged(); that user's identified history returns.
[ ] Switch to a second account: call identityChanged(); it cannot see the first user's history or data.

Do not rely on a post-message-only identify() flow. Use identityChanged() after login,
access-token/session rotation, and every account switch; use signedOut() after logout.

10. Check your own integration

"My customer shows up as a guest" is the symptom every mistake here produces. The Identity check grades your page's lifecycle, the CLI grades your backend, and both exit 0 passed, 1 failed, 2 could not be observed. In the Console, Identity health shows the same cells against real traffic. Fix a signed-in customer who shows as a guest walks each symptom to its cause.

Verify

  1. Signed out: the assistant is a visitor session.
  2. Log in without reloading and call identityChanged(): the frame is identified.
  3. The same customer on a second device sees the same history.
  4. A second account cannot see the first's history or data.

Next

Questions

Do you store my customers' accounts?

No. Each launch carries a proof your product signed; the subject is your id. Conversations are keyed to that subject inside your workspace.

Why does the proof expire in 120 seconds?

It is a launch proof, not a session. A fresh one is minted per launch and used once, so a leaked proof is useless within moments.

Which algorithm must I use?

ES256 is the reference. The algorithms your provider accepts are part of its registration, checked against your JWKS.