Integration guide · protocol v1

Put your AI experience everywhere.

Build and manage branded AI experiences connected to your users, knowledge, data, and tools. This guide uses support as one production template—not as the platform boundary.

Web iOS Android Desktop

Architecture in 60 seconds

Your app remains the identity and account-data authority. The chat receives a short-lived proof of who is present; it uses a separately scoped actor when calling your MCP tools.

Your appExisting session
1
Identity endpoint2-minute assertion
2
AI runtimeTenant policy
3
Your MCPSelf-scoped tools

Sync the customer already signed into your product.

Create one authenticated backend endpoint. It signs a short-lived ES256 assertion with your stable customer subject and a few safe display claims. Never expose the private key or your product session to the browser or chat.

server/support-identity.ts
import { SignJWT, importJWK } from "jose";

app.post("/api/bmai/identity", requireSession, async (req, res) => {
  const nonce = crypto.randomUUID();
  const key = await importJWK(process.env.BMAI_PRIVATE_JWK, "ES256");
  const token = await new SignJWT({
    tenant_id: process.env.BMAI_TENANT_ID,
    name: req.user.displayName,       // low-sensitivity context only
    account_status: req.user.status,
  })
    .setProtectedHeader({ alg: "ES256", kid: "bmai-2026-01" })
    .setIssuer("https://yourdomain.com")
    .setAudience("busymate-ai")
    .setSubject(req.user.id)          // your stable customer id
    .setJti(crypto.randomUUID())
    .setIssuedAt()
    .setNotBefore("-5s")
    .setExpirationTime("2m")
    .sign(key);

  res.json({ token, nonce, expiresIn: 120 });
});
Keep sensitive data live. Put balances, devices, settings and mutations behind self-scoped MCP tools. Claims are orientation context, not permission.

One script for an embedded launcher.

The SDK asks your backend for identity when needed, works for guests, refreshes after login/logout, opens external links outside the frame, and does not depend on third-party cookies.

app.html
<script>
  window.BusymateAI = {
    getIdentity: async () => {
      const response = await fetch("/api/bmai/identity", {
        method: "POST",
        credentials: "include",
      });
      if (!response.ok) return null; // guest chat still works
      return response.json();       // { token, nonce }
    },
  };
</script>
<script
  src="https://ai.yourdomain.com/embed/v1.js"
  data-assistant="your-assistant"
  data-label="Help"
  async
></script>

Open a full-page tenant or custom domain

Use the fragment handoff when a signed-in user opens your-tenant.busymate.ai or your mapped custom domain. The destination clears it before exchange.

open-support.ts
const identity = await fetch("/api/bmai/identity", {
  method: "POST",
  credentials: "include",
}).then((response) => response.json());

const fragment = new URLSearchParams({
  bmai_token: identity.token,
  bmai_nonce: identity.nonce,
});

// One-time identity never appears in the HTTP request or referrer.
location.href =
  "https://your-tenant.busymate.ai/#" + fragment;

Use WKWebView with a narrow message bridge.

Allow only the three versioned messages: identity request, identity response and external URL. Mint identity through the app's existing authenticated API client.

SupportBridge.swift
final class SupportBridge: NSObject, WKScriptMessageHandler {
  let webView: WKWebView

  func userContentController(_ controller: WKUserContentController,
                             didReceive message: WKScriptMessage) {
    guard message.name == "SupportChat",
          let body = message.body as? [String: Any],
          body["type"] as? String == "support.chat.v1.identity_request"
    else { return }

    Task {
      let identity = try await api.supportLaunchIdentity()
      let payload: [String: Any] = [
        "type": "support.chat.v1.identity",
        "token": identity.token,
        "nonce": identity.nonce,
      ]
      let data = try JSONSerialization.data(withJSONObject: payload)
      let json = String(decoding: data, as: UTF8.self)
      await webView.evaluateJavaScript("window.postMessage(\(json), '*')")
    }
  }
}

Use WebView with one allowlisted interface.

Keep navigation on your support origin, send outside links to the system browser and expose no general-purpose native methods.

SupportBridge.kt
class SupportBridge(private val webView: WebView) {
  @JavascriptInterface
  fun postMessage(raw: String) {
    val message = JSONObject(raw)
    if (message.optString("type") != "support.chat.v1.identity_request") return

    lifecycleScope.launch {
      val identity = api.mintSupportLaunchIdentity()
      val response = JSONObject()
        .put("type", "support.chat.v1.identity")
        .put("token", identity.token)
        .put("nonce", identity.nonce)
      webView.evaluateJavascript(
        "window.postMessage(${JSONObject.quote(response.toString())}, '*')", null
      )
    }
  }
}

webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(SupportBridge(webView), "SupportChatNative")

The same protocol fits Electron, Tauri and native shells.

Use an isolated WebView, deny new in-view windows, open safe URLs externally and answer identity requests through the privileged host layer.

support-window.ts
// Electron — the same contract works in Tauri or a native WebView.
const support = new BrowserWindow({
  webPreferences: {
    preload: join(__dirname, "support-preload.js"),
    contextIsolation: true,
    sandbox: true,
  },
});

support.webContents.setWindowOpenHandler(({ url }) => {
  if (url.startsWith("https://")) shell.openExternal(url);
  return { action: "deny" };
});

support.loadURL("https://your-tenant.busymate.ai/?channel=desktop");
ipcMain.handle("support:identity", () => api.mintSupportLaunchIdentity());

Give the assistant tools without giving it your customer database.

Expose complete schemas during discovery. Classify every tool as public, identified or delegated. Recheck authorization on every call, derive the customer from the signed actor and require confirmation for writes.

delegated tools/call
// tools/list may be public; every tools/call is authorized again.
{
  "jsonrpc": "2.0",
  "id": 12,
  "method": "tools/call",
  "params": {
    "name": "update_my_profile",
    "arguments": { "display_name": "Ada" }
  }
}

// Authorization: Bearer <short-lived delegated actor>
// Never accept account_id in arguments. Derive the customer from the actor.

Configure escalation as product policy.

Trigger rules

Customer request, sentiment, tool failure, billing risk, VIP tier, keywords or any tenant event.

Operator delivery

Shared inbox, assignment, presence, SLA, browser notification, email, Telegram or webhook.

Live takeover

Watch the stream, join with context, reply in place and suspend autonomous writes while human-active.

Smart audit

Rank repeated problems, missing knowledge, failed paths and automation candidates with source evidence.

Keep your own user base and authentication.

Configure the tenant login and account URLs in Console. A guest choosing Sign in leaves the frame, authenticates on your existing product, and returns to the exact originating tenant or artifact URL. Your backend then supplies a one-time identity assertion; Busymate AI never receives the user's password or your signing key.

Writes remain self-scoped. Account-aware MCP tools derive the customer from the delegated actor, recheck authorization on every call, and request confirmation for configured changes.

Production launch checklist

  1. Create tenant branding, SEO, domains and suggestions.
  2. Register identity issuer, JWKS, audience, claims and token age.
  3. Connect MCP, classify tools and prove self-scoped reads plus confirmed writes.
  4. Configure human triggers, staff channels, hours and SLA.
  5. Test guests, signed-in users, logout/account switch, replay denial and tenant substitution.
  6. Verify hosted, embed, iOS, Android, desktop, accessibility and external links.
  7. Publish only the verified revision and monitor connector/domain health.
Ready to configure a tenant?The admin integration kit generates every surface contract from one revision.
Open the platform