Busymate AI

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

Busymate AI recognizes your signed-in customers without sharing an account database. So the assistant can trust who is asking, your API signs a short-lived proof for each signed-in customer (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 and refreshIdentity. bro then serves that customer's history and account tools — only theirs.

Two identities

  • Your team signs in to the Console with their own accounts to manage the workspace.
  • Your customers never get a platform account. Each launch carries a short-lived proof your product signed; its subject is your unchanging customer id. The claims prove who is chatting — they are not an account database.

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 provider 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 that requires your own signed-in session and returns 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": "<your-tenant-id>", // Busymate AI's registered tenant claim
      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("https://YOUR-PRODUCT-DOMAIN (set when you register the provider)")
    .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 and must match ^[A-Za-z0-9_-]{32,200}$; the response echoes it.
  • 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. Every launch consumes the pair 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, or null for a signed-out one. Call refreshIdentity() after login, logout, token rotation and every account switch — it reloads the correct visitor or signed-in history. Never keep a token or nonce in storage, cookies or component state. The Console's Integration section renders the full embed snippet with your values.

5. 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, referrer or logs. The destination strips the fragment before the exchange. The hosted-handoff snippet in Integration shows the exact form; see also White-label SDK → Customer identity.

6. Acceptance checklist

Setup progress is evidence of configuration; it does not prove the flow works. Run this list before you call sign-in done:

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

[ ] Signed out: getIdentity returns null and the assistant remains anonymous.
[ ] Login without reloading the host page: call refreshIdentity(); 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 Busymate AI 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 refreshIdentity(); account data is unavailable and the frame is anonymous.
[ ] Login again as the same account: call refreshIdentity(); that user's identified history returns.
[ ] Switch to a second account: call refreshIdentity(); it cannot see the first user's history or data.

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

Pitfalls

  • An email, phone number or session id as sub. Use the unchanging internal id.
  • A token or nonce kept in localStorage, a cookie or React state.
  • A proof older than the registered max age, or a missing kid.
  • Answering getIdentity for a signed-out customer with anything but null.

Verify

  1. Signed out: the assistant is a visitor session.
  2. Log in without reloading the page and call refreshIdentity(): the frame is identified.
  3. The same customer on a second device sees the same history.
  4. A second account cannot see the first account'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.

Can visitors still chat?

Yes, when guest access is on. Signed-out visitors get answers and guidance; account tools need a signed-in customer.