--- title: "Anmeldung und Identität" description: "Erkennen Sie Ihre angemeldeten Kundinnen und Kunden und lassen Sie Ihre eigenen Tools in deren Namen agieren: Widget, vollständige Chat-Seite, Apps und MCP, Schritt für Schritt." last_updated: "2026-10-30T01:08:36+02:00" --- # Anmeldung und Identität Source: https://busymate.ai/de/docs/identity Last modified: 2026-10-30T01:08:36+02:00 Your customer is already signed in to your product. Busymate AI can know who they are, and your own tools can act as them. No second login. No "Authorize" card. > **Contract `bmai-identity` 2.0.0 · `@busymate/identity-contract` 2.0.0-beta.2.** This page is generated from the contract. Anything marked **[PLANNED]** is not live yet. Machine-readable: [busymate.ai/.well-known/bmai-integration.json](https://busymate.ai/.well-known/bmai-integration.json). | Part | Version | |---|---| | Launch token | v1 (unchanged) | | Actor token | v3 (ES256, public JWKS) per workspace. v2 (HS256, your secret) stays until it is removed | | Web SDK | `@busymate/ai-web` 0.1.1 on npm | | Verifier | `@busymate/ai-verify` 0.1.2 on npm | | Check CLI | `@busymate/identity-check` 0.1.2 on npm | | Apps | SDK 1.0.2 (iOS, Android, macOS, Windows, Linux, Electron, Tauri), bridge `busymate.bridge/2` 2.0.0 | | Per-connector customer identity | `busymate` \| `oauth` \| `none` **[PLANNED]** | ## How it works Diagram: Sequence diagram: the user is signed in on your site; the chat asks your site for identity; your backend signs a launch token; the chat verifies it and opens an identified session; a tool call reaches your MCP with an actor token that your MCP verifies. Your site vouches for the user with a short-lived **launch token**. Busymate AI verifies it and opens an **identified session**. When a question needs their account, Busymate AI calls your MCP server or API with an **actor token**, and you act as that user. No shared account database. No passwords with Busymate AI. ## Choose your setup | You have | Do | Steps | |---|---|---| | A website with the widget | Your page answers `getIdentity` | [Setup A](#setup-a-the-widget-on-your-site) | | A full chat page (`https://acme.busymate.ai` or your domain) | Your login page hands the user back | [Setup B](#setup-b-the-full-chat-page) | | An iOS, Android or desktop app | The app bridge | [Setup C](#setup-c-your-own-app) | | Your own MCP server or API | Verify the actor token | [Setup D](#setup-d-your-mcp-server-or-api) | Every setup starts with **Step 1**. Setup D removes the "Authorize" card. | | Guest | Identified | |---|---|---| | Who | Anyone | Signed in to **your** product | | History | This browser | Follows the customer | | Public tools | Yes | Yes | | Your account tools | No | Yes, as that customer | ## Step 1: Sign your user 1. Create an **ES256** key pair. Publish the public key as a JWKS at `https:///.well-known/jwks.json`. 2. Add one endpoint, body `{ nonce }`. It needs **your** session. Signed out: `401`. Signed in: `201 { token, nonce, expiresIn }` with `Cache-Control: no-store`. 3. Sign `iss` = `https://api.acme.com`, `aud` = `busymate-ai`, `sub` = your immutable id for **one person** (never an e-mail), `tenant_id` = `acme`, the request `nonce` unchanged, a unique `jti`, `iat`, and `exp` = now + 120 s. ```js import { SignJWT, importPKCS8 } from "jose"; import { randomUUID } from "node:crypto"; const key = await importPKCS8(process.env.BMAI_ES256_PRIVATE_KEY, "ES256"); app.post("/api/bmai/identity", requireYourSession, async (req, res) => { const { nonce } = req.body ?? {}; if (typeof nonce !== "string" || nonce.length < 16) return res.status(400).end(); const token = await new SignJWT({ tenant_id: "acme", nonce }) .setProtectedHeader({ alg: "ES256", kid: process.env.BMAI_KID }) .setIssuer("https://api.acme.com").setAudience("busymate-ai") .setSubject(String(req.user.id)) .setJti(randomUUID()).setIssuedAt().setExpirationTime("120s") .sign(key); res.set("Cache-Control", "no-store").status(201).json({ token, nonce, expiresIn: 120 }); }); ``` 4. Register it: **Console › Identity** (or `upsert_tenant_identity_provider`). Enter issuer, JWKS URL, audience, tenant claim, mint URL and `login_url`. Run `test_tenant_identity_provider`, then **publish**. An unpublished provider is refused. Identity edits apply without a republish. **[PLANNED]** PHP, Python and Go samples: `https://busymate.ai/sdk/v1/kit/backend/`. ## Setup A: the widget on your site Add your page origin under **Console › Integration › Allowed origins**. Three calls: `getIdentity` answers the chat, `identityChanged()` after sign-in or an account switch, `signedOut()` after logout. ```html ``` With a bundler: `npm install @busymate/ai-web` (0.1.1). `createBusymateChat({ assistant: "acme", origin: "https://acme.busymate.ai", getIdentity: mintFromEndpoint("https://api.acme.com/api/bmai/identity") })`, then `chat.identityChanged()`, `chat.switchAccount()` and `chat.logout()` on your auth events. **Signed out?** The widget shows **Sign in**: | Your page | What Sign in does | |---|---| | Registered a `sign_in` page tool | A card opens in the thread: email and code, password or one button. Nothing leaves your page. | | No tool, `login_url` set | Your login page opens in a popup or the top window, signs the user in, and returns to `return_to#bmai_token=…&bmai_nonce=…`. The widget picks it up. | | Neither | Nothing is offered. | ## Setup B: the full chat page There is no code on `https://acme.busymate.ai`, so your **login page** does the work. Register it as `login_url`. | Busymate AI sends | Your login page | |---|---| | `?return_to=&bmai_nonce=` (user tapped **Sign in**) | Signs the user in (password, email code, SSO), mints with ``, redirects to `return_to#bmai_token=&bmai_nonce=` | | the same plus `bmai_prompt=none` (silent, once per tab) | **Never shows UI.** Session: redirect as above. None: `return_to#error=login_required` | A user already signed in is recognised with no click when your login page can load in a hidden frame on the chat host: ``` Content-Security-Policy: frame-ancestors https://assistant.acme.com https://acme.busymate.ai Set-Cookie: ; Secure; SameSite=None ``` Without both, signed-in users stay guests until they tap **Sign in**. The silent check gives up after 3 seconds. You can also link straight in: `https://acme.busymate.ai/#bmai_token=&bmai_nonce=`. ## Setup C: your own app Pass `account` and `mint`. Call `accountChanged()` on login, logout and account switch. Install the bridge **before** the first load. ```swift BusymateBridge.install(on: webView, config: .init( assistant: "acme", origins: ["https://acme.busymate.ai"], account: { Auth.shared.userId }, // nil when signed out mint: { request in try await API.mintBmaiIdentity(nonce: request.nonce) } // never cached )) webView.load(URLRequest(url: URL(string: "https://acme.busymate.ai/?channel=ios")!)) ``` ```kotlin BusymateBridge.install(webView, BusymateBridge.Config( assistant = "acme", origins = listOf("https://acme.busymate.ai"), account = { auth.userId }, mint = { request, done -> api.mintBmaiIdentity(request["nonce"] as String) { done(it) } } )) webView.loadUrl("https://acme.busymate.ai/?channel=android") ``` SDK 1.0.2: iOS, Android, macOS, Windows, Linux, Electron, Tauri. Desktop uses `channel=desktop_sdk`, **never** `desktop`. React Native and Flutter use the frozen files at `https://busymate.ai/sdk/v2/2.0.0/`. Copy each channel's exact chat URL from **Console › Integration**. ## Setup D: your MCP server or API Busymate AI sends an actor token on each tool call by an identified customer. Verify it **before** your own auth middleware. ```sh npm install @busymate/ai-verify # Node 18+ and Deno. Python, PHP, Go: same repository ``` ```ts import { verifyBusymateActor, requireConfirmed, BusymateActorError } from "@busymate/ai-verify"; try { const actor = await verifyBusymateActor(req, { audience: "https://mcp.acme.com", // EXACT origin of your connector URL tenantId: "acme", connectorId: process.env.BMAI_CONNECTOR_ID!, // Console › Connections idp: process.env.BMAI_IDP, // Console › Identity tool: toolName, // the tools/call name }); const user = await users.findByBusymateIdentity(actor.idp, actor.sub); // (idp, sub) only requireConfirmed(actor, WRITE_TOOLS.has(toolName)); // money and writes // run the tool as `user`. Never read an account id from tool arguments. } catch (e) { if (e instanceof BusymateActorError) return reply(401, { "WWW-Authenticate": 'Bearer error="invalid_token"' }); throw e; } ``` Serve `GET /api/bmai/status` returning `{"actorVerifier": true, "actor": 3}`. Then Busymate AI sends token v3 in `Authorization`; until then in `X-Bmai-Actor`. Rules: map users by `(idp, sub)` only. Require `confirmed` for money and writes. Reject a repeated `jti`. Answer a bad token with `401`, never fall through to another user. Guests get no token on a v3 workspace. ### Who sees a card Set per connector in the Console. The three modes below are the replacement for today's **Signed actor token** and **Per-customer OAuth** settings. **[PLANNED]** | Customer identity | For | The signed-in customer sees | |---|---|---| | **Busymate AI token** **[PLANNED]** | Your own product | No OAuth card. With "Ask customers" ON: one confirm. If your MCP rejects the token: "temporarily unavailable", and you get an alert. | | **OAuth** **[PLANNED]** | A third party with its own OAuth | "Authorize {partner}" once | | **None** **[PLANNED]** | Public tools only | No card | **Ask customers before using their account** is one switch, default **OFF**. Busymate AI enables the token mode only after a **live actor probe** passes: it mints a test-account token for your connector origin and your server must accept it. On the same registrable domain as your sign-in it is the default, after the probe. **[PLANNED]** Today a per-customer OAuth connector shows the card once per customer, even when they are signed in. ## Sign out and identity change Call `signedOut()`, `logout()` or `accountChanged()` on every logout, or the next person sees the previous chat. On an account switch call `identityChanged()` (SDK `switchAccount()`): the same conversation upgrades in place. A signed-out customer loses account tools at once. The nonce is always the one Busymate AI gives you: never your own, never cached. ## Channels | Channel | Today | |---|---| | Widget, full chat page, iOS, Android, desktop | Identified, as above | | WhatsApp, Messenger, Telegram | Identified through your `login_url` when you switch it on (below). Off: guests only | | SMS, Instagram, Apple Messages, Slack, Teams, Discord, e-mail | Guests only. No account tools | | ChatGPT and Claude apps | Your team only, not your customers | **Channel sign-in.** Set `channels.customerSignIn` to the channels you want (`["whatsapp", "messenger", "telegram"]`) and publish. It needs a published identity provider with a `login_url`. The customer sends `sign in` (or `/login`) in the chat and opens a one-time link on your chat domain. Your `login_url` gets `return_to`, `bmai_nonce` and `bmai_client=channel`, and answers exactly as for the web. The page then shows a 6-digit code. The customer sends that code **in the same chat**, and only then is the chat linked to your customer (`sub`). After that, account tools run as that customer with no card. A link forwarded to someone else cannot link their account to the sender's chat, because the code only works in the chat that asked. `sign out` unlinks the chat. A link lasts 30 days, and you can end it sooner by disabling or unpublishing the provider. ## Verify - [ ] `GET https://api.acme.com/.well-known/jwks.json` returns your key. Mint: `401` signed out, `201` + `no-store` signed in, nonce echoed. - [ ] Widget: sign in and the chat greets you in the same conversation. Sign out and the transcript clears. - [ ] Full chat page: open it already signed in. Recognised in about 3 s, no click. - [ ] Ask for something only your account tools answer: **no card**. `list_identity_link_events` shows identified, token minted, connector accepted. - [ ] Your server rejects a wrong `aud`, an expired token and an anonymous token. Busymate AI probes you three ways: the **live actor probe**, your `GET /api/bmai/status`, and the Console readiness page. Run the same checks yourself: ```sh npx @busymate/identity-check --issuer https://api.acme.com --mint https://api.acme.com/api/bmai/identity \ --login-url https://acme.com/login --chat-origin https://acme.busymate.ai \ --mcp https://mcp.acme.com --probe-url https://mcp.acme.com/mcp --tenant-id acme ``` Exit `0` passed, `1` a check failed, `--json` for machines. Browser version: [Identity check](https://busymate.ai/developers/identity-check). ## Troubleshooting | You see | Cause | Fix | |---|---|---| | Signed in to your product, still asked to **Authorize** (reason `third_party`) | The connector is per-customer OAuth, or not treated as yours, or your MCP does not accept the actor token | Do Setup D. Make sure your MCP verifies the actor token before its auth middleware, and the connector uses Signed actor token | | The **Authorize** card keeps coming back (`partner_rejected`) | Your server answered `401` to the actor token | Check `aud` (exact origin), `tenant_id`, `connector_id`, and that the verifier runs first | | Always a guest (`provider_not_published`) | The provider is saved but not published | Publish the workspace | | `nonce_mismatch` | You made your own nonce, or cached a token | Use the nonce Busymate AI passes you. Mint a new token on every call | | `409` on launch | A `jti` or nonce was reused | A unique `jti` per token. Never cache | | `claim_invalid`, `token_expired`, `clock_skew` | Claims differ from the registration, the token is older than the maximum token age, or the server clock is off | Match the registration. Use 120 s. Sync the clock | | Full chat page: signed-in users stay guests | The login page cannot be framed, or its cookie is not `SameSite=None` | Send `frame-ancestors ` and `SameSite=None; Secure` | | The silent check shows a visible page | Your login page ignored `bmai_prompt=none` | Never render UI on `prompt=none`: redirect with `#error=login_required` | | Account tools are hidden (`asks_identity_not_conveyed`) | The connector identity mode is none | Set Signed actor token | | "Sign in before this account tool can run" | The visitor is not identified | Fix Step 1 and your setup first | | Your MCP answers `401` with `confirmation_required` | A write tool was called without `confirmed` | Nothing to fix: the customer must confirm in the chat | | The previous user's chat is visible after logout | `signedOut()` was not called | Call it on every logout | | `credential_missing` on WhatsApp, Messenger or Telegram | The chat customer is not signed in | Switch on `channels.customerSignIn` and ask the customer to send `sign in` | The `reason` in a `401` launch body and the chat's `rejected:` come from one list of codes: see the manifest. ## Example: RentADriver Chat: `assistant.rentadriver.ai`. MCP: `https://mcp.rentadriver.ai/mcp`. Issuer and mint: `https://api.rentadriver.ai`. Three changes remove the card: 1. **One person per `sub`** in the launch token, not an organization id. 2. **The verifier before the auth middleware** on `mcp.rentadriver.ai`: `verifyBusymateActor(req, { audience: "https://mcp.rentadriver.ai", tenantId, connectorId, idp, tool })`. The ids are given at cut-over. 3. **`confirmed` for write tools:** `create_delivery`, `fund_delivery`, `tip_driver`, `create_wallet_deposit`, `cancel_delivery`, `set_spending_controls`, key and webhook tools. Then Busymate AI sets the connector to the token mode. **[PLANNED]** Until then the **Authorize** card is the working fallback. ## Reference | Item | Value | |---|---| | Launch token | ES256 or RS256. `exp - iat` within the provider's maximum (30 to 900 s, use 120). Skew up to 120 s. One-time `jti` and nonce: a replay gets `409`. | | Actor token v3 | ES256, `typ: bmai-actor+jwt`, keys at `https://busymate.ai/.well-known/bmai-actor-jwks.json` (rotate every 90 days, 14-day overlap: never pin a key). `iss` = `https://busymate.ai`. Claims: `aud`, `sub`, `idp`, `tenant_id`, `connector_id`, `support_session_id`, `actor_kind: "identified"`, `confirmed`, `tool`, `iat`, `nbf`, `exp` (at most 120 s after `iat`), `jti`. Skew 10 s. | | Actor token v2 (compat) | HS256, `kid: bmai-support-v2`, `Authorization: Bearer`, at most 300 s, also sent for **anonymous** visitors (`sub = anonymous:`): check `actor_kind`. Set a secret of 32+ bytes in **Console › Connections › Actor verifier**. The connector must be **Signed actor token**, server auth not OAuth, tools **Identified**. Removed after a 90-day overlap. | | Reason codes | The full list is in the [manifest](https://busymate.ai/.well-known/bmai-integration.json). | ## Questions ### Do my customers need an account with Busymate AI? No. They sign in to your product. Your backend signs the launch token, and the subject is your own user id. ### Why does a signed-in customer still see "Authorize"? Your connector is per-customer OAuth, or your MCP does not accept the actor token yet. Do Setup D, then check Troubleshooting. ### Can the assistant read a password? No. Credentials go to your own login page or sign-in card. ## For AI coding agents Copy this block into your agent with the values above filled in. ```text GOAL: The Busymate AI chat must recognise the signed-in user and call our MCP/API as that user, with no extra login and no Authorize card. CONTRACT: bmai-identity 2.0.0 (@busymate/identity-contract 2.0.0-beta.2). Manifest: https://busymate.ai/.well-known/bmai-integration.json 1. BACKEND (language of this repo) - ES256 key pair. Serve the public key as a JWKS at /.well-known/jwks.json (kid, alg ES256, use sig). - POST , body {nonce, returnTo?}, needs OUR existing session. Signed out: 401. Else 201 {token, nonce, expiresIn:120}, header Cache-Control: no-store. - JWT claims: iss=, aud=, sub=, =, nonce=, jti=, iat=now, exp=now+120. - Never an e-mail or phone in sub. Never a shared organization id. Never cache or store a token. Never make your own nonce. 2. FRONTEND (pick what this repo is) - Plain site: define window.BusymateAI.getIdentity({nonce}) BEFORE loading /embed/v1.js (data-assistant=). Call window.BusymateAI.identityChanged() after login, rotation and account switch, and window.BusymateAI.signedOut("signed_out") after logout. - Bundler: npm install @busymate/ai-web. createBusymateChat({assistant, origin, getIdentity: mintFromEndpoint()}); chat.mount(); identityChanged() / switchAccount() / logout() on our auth events. - iOS, Android, desktop: BusymateBridge.install(... account, mint ...) BEFORE the first load; accountChanged() on every auth change. Desktop uses channel=desktop_sdk, never channel=desktop. React Native and Flutter use the frozen bridge files. - Hosted chat page or custom domain: our login page reads return_to, bmai_nonce and bmai_prompt, mints with that nonce and redirects to return_to#bmai_token=&bmai_nonce=. On bmai_prompt=none it never renders UI: no session means redirect to return_to#error=login_required. Send Content-Security-Policy frame-ancestors and a SameSite=None; Secure session cookie. 3. MCP SERVER / API - npm install @busymate/ai-verify (Python, PHP, Go files: github.com/serebano/busymate-ai-sdk-verify). - Before our own auth middleware call verifyBusymateActor(req, {audience: , tenantId, connectorId: BMAI_CONNECTOR_ID, idp: BMAI_IDP, tool: }). - Token v3: ES256, typ bmai-actor+jwt, header X-Bmai-Actor (or Authorization: Bearer when GET /api/bmai/status returns {"actorVerifier":true,"actor":3}); keys at https://busymate.ai/.well-known/bmai-actor-jwks.json; iss https://busymate.ai; exp-iat <= 120; clock skew <= 10 s. - Map the user by (idp, sub) ONLY. Never from a tool argument. Require confirmed === true (requireConfirmed) for money and write tools. Reject a repeated jti. - On a BusymateActorError answer 401 with WWW-Authenticate: Bearer error="invalid_token"; log e.reason; never fall through to another user. - Serve GET /api/bmai/status -> {"actorVerifier": true, "actor": 3}. - Compat v2 (HS256, kid bmai-support-v2, header Authorization: Bearer, exp-iat <= 300, secret set in the Console): check actor_kind === "identified"; anonymous means public data only. 4. DO NOT: make your own nonce; cache or store tokens; use channel=desktop; use OAuth mode for our own MCP; put an account id in tool arguments; ask the human for private keys. 5. TELL THE HUMAN to: register the provider and publish (Console > Identity), set the connector to Signed actor token (or ask for token v3 on the workspace), keep "Ask customers before using their account" OFF unless they want the confirm, then run: npx @busymate/identity-check --issuer --mint --login-url --chat-origin --mcp --tenant-id . ``` ## Changelog ### 2.0.0-beta.2 · 2026-10-29 - One public identity page, generated from this contract. - Actor token v3 (ES256, public JWKS) is live behind a per-workspace flag, dual-signed beside v2. Guests get no token on a flagged workspace. - `@busymate/ai-verify` and `@busymate/identity-check` are on npm. `@busymate/ai-web` is on npm. ### 2.0.0-beta.1 · 2026-10-29 - The identity contract package, the machine manifest `/.well-known/bmai-integration.json` and signed test vectors. - Launch verifier constants (algorithms, token age, required claims) now come from the contract. No behaviour change.