Ir al contenido
Busymate AI

Esta guía está actualmente en inglés.

Documentación

Inicio de sesión e identidad

En esta página
<!-- GENERATED by scripts/identity-contract/gen.mjs --write from packages/identity-contract. Do not hand-edit. -->

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.

PartVersion
Launch tokenv1 (unchanged)
Actor tokenv3 (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
AppsSDK 1.0.2 (iOS, Android, macOS, Windows, Linux, Electron, Tauri), bridge busymate.bridge/2 2.0.0
Per-connector customer identitybusymate | oauth | none [PLANNED]

How it works

User Your site or app Your backend Busymate AI chat Busymate AI Your MCP 1. already signed in 2. getIdentity(nonce) 3. mint(nonce) 4. signed launch token 5. token + nonce 6. verify with your JWKS identified session 7. asks about their account 8. tool call + actor token 9. verify, act as sub 10. result 11. answer, no extra login

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 haveDoSteps
A website with the widgetYour page answers getIdentitySetup A
A full chat page (https://acme.busymate.ai or your domain)Your login page hands the user backSetup B
An iOS, Android or desktop appThe app bridgeSetup C
Your own MCP server or APIVerify the actor tokenSetup D

Every setup starts with Step 1. Setup D removes the "Authorize" card.

GuestIdentified
WhoAnyoneSigned in to your product
HistoryThis browserFollows the customer
Public toolsYesYes
Your account toolsNoYes, as that customer

Step 1: Sign your user

  1. Create an ES256 key pair. Publish the public key as a JWKS at https://<issuer>/.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 });
});
  1. 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
<script>
  window.BusymateAI = {
    getIdentity: async ({ nonce }) => {
      const r = await fetch("https://api.acme.com/api/bmai/identity", {
        method: "POST", credentials: "include", cache: "no-store",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ nonce }),
      });
      if (r.status === 401 || r.status === 403) return null;   // signed out
      if (!r.ok) throw new Error("identity mint failed " + r.status);
      const id = await r.json();
      if (id.nonce !== nonce) throw new Error("nonce mismatch");
      return { token: id.token, nonce: id.nonce };             // never cache
    },
  };
  // after YOUR login or account switch: window.BusymateAI.identityChanged();
  // after YOUR logout:                  window.BusymateAI.signedOut("signed_out");
</script>
<script src="https://acme.busymate.ai/embed/v1.js" data-assistant="acme" async></script>

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 pageWhat Sign in does
Registered a sign_in page toolA card opens in the thread: email and code, password or one button. Nothing leaves your page.
No tool, login_url setYour 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.
NeitherNothing 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 sendsYour login page
?return_to=<chat>&bmai_nonce=<n> (user tapped Sign in)Signs the user in (password, email code, SSO), mints with <n>, redirects to return_to#bmai_token=<token>&bmai_nonce=<n>
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:

text
Content-Security-Policy: frame-ancestors https://assistant.acme.com https://acme.busymate.ai
Set-Cookie: <your session>; 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=<t>&bmai_nonce=<n>.

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 identityForThe signed-in customer sees
Busymate AI token [PLANNED]Your own productNo 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 onlyNo 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

ChannelToday
Widget, full chat page, iOS, Android, desktopIdentified, as above
WhatsApp, Messenger, TelegramIdentified through your login_url when you switch it on (below). Off: guests only
SMS, Instagram, Apple Messages, Slack, Teams, Discord, e-mailGuests only. No account tools
ChatGPT and Claude appsYour 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.

Troubleshooting

You seeCauseFix
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 tokenDo 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 tokenCheck 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 publishedPublish the workspace
nonce_mismatchYou made your own nonce, or cached a tokenUse the nonce Busymate AI passes you. Mint a new token on every call
409 on launchA jti or nonce was reusedA unique jti per token. Never cache
claim_invalid, token_expired, clock_skewClaims differ from the registration, the token is older than the maximum token age, or the server clock is offMatch the registration. Use 120 s. Sync the clock
Full chat page: signed-in users stay guestsThe login page cannot be framed, or its cookie is not SameSite=NoneSend frame-ancestors <chat origins> and SameSite=None; Secure
The silent check shows a visible pageYour login page ignored bmai_prompt=noneNever render UI on prompt=none: redirect with #error=login_required
Account tools are hidden (asks_identity_not_conveyed)The connector identity mode is noneSet Signed actor token
"Sign in before this account tool can run"The visitor is not identifiedFix Step 1 and your setup first
Your MCP answers 401 with confirmation_requiredA write tool was called without confirmedNothing to fix: the customer must confirm in the chat
The previous user's chat is visible after logoutsignedOut() was not calledCall it on every logout
credential_missing on WhatsApp, Messenger or TelegramThe chat customer is not signed inSwitch on channels.customerSignIn and ask the customer to send sign in

The reason in a 401 launch body and the chat's rejected:<code> 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

ItemValue
Launch tokenES256 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 v3ES256, 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:<session>): 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 codesThe full list is in the manifest.

Questions

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 <issuer>/.well-known/jwks.json (kid, alg ES256, use sig).
   - POST <mint path>, body {nonce, returnTo?}, needs OUR existing session. Signed out: 401. Else 201 {token, nonce, expiresIn:120}, header Cache-Control: no-store.
   - JWT claims: iss=<issuer>, aud=<audience>, sub=<immutable per-person user id>, <tenant claim>=<tenant>, nonce=<request nonce unchanged>, jti=<uuid>, 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 <chat origin>/embed/v1.js (data-assistant=<slug>). 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(<mint url>)}); 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=<token>&bmai_nonce=<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 <chat origins> 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: <exact connector origin>, tenantId, connectorId: BMAI_CONNECTOR_ID, idp: BMAI_IDP, tool: <tools/call name>}).
   - 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 <issuer> --mint <mint url> --login-url <login url> --chat-origin <chat origin> --mcp <connector origin> --tenant-id <tenant>.

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.

Preguntas

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.