Skip to content
AI Assistant

Guides

In-app AI support for iOS and Android

Show your assistant's chat inside your iOS or Android app, pass sign-in through your own API, and verify on a real device.

YOUR APPCHANNEL?channel=iosWeb viewBRIDGEWeb viewBusymateAIIdentityOpen linkClose
On this page

Your app gets AI Assistant support by showing your assistant's chat inside a WKWebView (iOS) or a WebView (Android) with channel=ios or channel=android. Your own API vouches for signed-in customers through a small bridge, so they are recognized. If your app runs a proxy or VPN, keep the chat host out of it. There is no SDK binary — two source files you copy.

1. Load the hosted chat

  1. The app loads the hosted chat: https://<your-host>/?channel=ios (or channel=android). Your own address is the entry point; the platform keeps /support/<slug> as a fallback.
  2. The page asks the native side for identity through a small bridge. A signed-in customer gets an identified session; a signed-out one is anonymous.
  3. External links open outside the WebView; the chat stays inside your app.

The bridge is what matters; the channel is the hint. A bare https://<your-host>/ with no channel still detects your installed bridge and asks it, so a signed-in customer is still recognized. Pin the channel too — it names your app (not a browser), files the conversation under the right host, sets the layout, and grounds what the assistant says about signing in.

Neither installed is still anonymous. With no bridge there is nobody to ask, so every customer gets an anonymous session regardless of how signed in they are to your app. Install the bridge; identity is what makes the assistant theirs.

Native bridge messages — both platforms answer the same three:

MessagePayloadYou do
busymate.ai.v1.identity_requestanswer with busymate.ai.v1.identity { token, nonce }
busymate.ai.v1.open_url / auth_request{ url }open it outside the WebView
busymate.ai.v1.closedismiss the sheet

2. Bridge on iOS

Load the URL in a WKWebView and add a script message handler named BusymateAI. The reference source is served at /sdk/v1/ios/BusymateAI.swift.

swift
// SDK source: https://busymate.ai/sdk/v1/ios/BusymateAI.swift
// Load https://your-assistant.busymate.ai/?channel=ios in a WKWebView.
final class AssistantBridge: NSObject, WKScriptMessageHandler {
  let webView: WKWebView

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

    Task { // mint through YOUR authenticated API — never a key in the app
      let identity = try await api.mintLaunchIdentity()
      let payload: [String: Any] = [
        "type": "busymate.ai.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), '*')")
    }
  }
}

3. Bridge on Android

Load the URL in a WebView, enable JavaScript, and register the bridge as BusymateAINative. Same messages (above), same identity answer. The reference source is served at /sdk/v1/android/BusymateAI.kt.

kotlin
// SDK source: https://busymate.ai/sdk/v1/android/BusymateAI.kt
// Load https://your-assistant.busymate.ai/?channel=android in a WebView.
class AssistantBridge(private val webView: WebView) {
  @JavascriptInterface
  fun postMessage(raw: String) {
    val message = JSONObject(raw)
    if (message.optString("type") != "busymate.ai.v1.identity_request") return

    lifecycleScope.launch { // mint through YOUR authenticated API client
      val identity = api.mintLaunchIdentity()
      val response = JSONObject()
        .put("type", "busymate.ai.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(AssistantBridge(webView), "BusymateAINative")
// SupportChatNative + support.chat.v1.* remain accepted for shipped apps.

4. Mint identity

  • The token is a short-lived sign-in proof (an ES256 launch token) your API mints for the signed-in customer — see Recognize signed-in customers.
  • It lives at most 120 seconds and carries a one-time nonce and jti. Mint a fresh pair for every request; never cache one.
  • The signing key stays on your server. The app calls your authenticated API; it never holds a key.
  • Signed out: answer with no identity. The session stays anonymous, with guest access if you allow it.

5. Signed out, or not wired yet

Anonymous is a supported state, not a broken one — with guest access on, the assistant answers product questions for anyone. What it will NOT do in your app is send your customer to a web login page: inside an app, sign-in is handed to the chat by the app, so there is no page for them to use. It says plainly that this chat has not been given their account yet and that the app has to pass their sign-in through, then helps with everything that does not need an account. The in-chat Sign in control appears only where it can finish — a browser, an iframe embed, or an app that installed the bridge — so your customers are never pointed at a control that goes nowhere.

6. Match your app

The hosted chat follows the device's light or dark setting and the customer's language. The name, colors and welcome copy come from your workspace branding, so the sheet reads as part of your app.

7. The keyboard is handled for you

When a customer taps the input, the composer stays directly above the on-screen keyboard and the conversation scrolls above it — on iOS Safari, Android Chrome, and inside both WebViews. You do not have to size anything or pass a keyboard height in.

The page does this two ways at once, so it holds wherever your WebView lands. Where the engine supports it, the page asks for the layout viewport itself to shrink (interactive-widget: resizes-content) and the chat is simply a keyboard shorter. Where it does not — notably WKWebView — the page measures the keyboard from visualViewport and subtracts it. The two cannot double-count: where the first applies, the second measures zero.

Two things on your side are worth knowing:

  • Do not add your own keyboard padding around the WebView. The page already clears the home-indicator inset while the keyboard is up; an extra native inset shows as a gap under the composer.
  • Android: leave the chat activity on the default adjustResize. adjustPan is also handled (the measured path covers it), but adjustResize gives the smoothest result.

If you embed the chat as a cross-origin iframe on a web page rather than in a WebView, the embed/v1.js loader does the same job from the host side: while a keyboard is up it fits the widget panel to the host page's visual viewport, because a child frame cannot observe the host's keyboard at all. Nothing to configure.

8. Keep it out of your proxy

Exclude your own process — or at least the chat host — from the tunnel. Otherwise the WebView's traffic loops through your own capture and the sheet stalls.

Real example

Our own DevTools iOS and Android apps show their own workspace this way: a Support tab, a WebView, the bridge above, identity minted by the DevTools API for signed-in customers. It is Rolling out through TestFlight and the internal Play track — follow the changelog. The integration is written up as a public page: DevTools integration.

Verify

On a device, not a simulator:

  1. Signed out, open Support. Guest chat answers.
  2. Sign in and reopen. The greeting identifies the customer; history is theirs.
  3. Tap an external link. It opens in the system browser.
  4. Log out. The next identity request returns nothing; the session is anonymous again.
  5. Write "talk to a human". The request reaches the Inbox.

Next

Questions

Do I need a native SDK?

No. A WebView plus the bridge source file for your platform. Both files are served from the platform and versioned under /sdk/v1/.

Where does the identity token come from?

From your own API, for your own signed-in customer. The app never holds a signing key; it forwards what your API minted.

Does the chat match my app's look?

Branding is your workspace's — name, colors, welcome copy. Light and dark follow the device.

Can I ship this before identity is wired?

Yes. Without a bridge answer the chat runs anonymously. Add identity when your API is ready; the page starts asking for it on the next open.