> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ada.cx/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ada.cx/_mcp/server.

# SDK API reference

With the Android SDK, you can customize the behavior of your AI Agent by configuring initial settings, either in XML or programmatically using the `AdaMessagingView.Settings` class.

**`XML`**

```xml XML
<!-- Example 1: XML -->
<cx.ada.messaging.widget.AdaMessagingView
    android:id="@+id/ada_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:ada_handle="my-handle"
    app:ada_language="en"
    app:ada_greeting="5c59aaabd8269e0339979014" />
```

**`Kotlin`**

```kotlin Kotlin
// Example 2: Kotlin
val settings = AdaMessagingView.Settings.Builder("my-handle")
    .greeting("5c59aaabd8269e0339979014")
    .language("en")
    .metaFields(
        AdaMessagingView.MetaFields.Builder()
            .setField("name", "John")
            .setField("age", 20)
            .setField("authorized", true),
    )
    .sensitiveMetaFields(
        AdaMessagingView.MetaFields.Builder()
            .setField("authToken", "secure-session-token"),
    )
    .build()
```

## Settings

The Ada Android SDK supports the following settings on `AdaMessagingView.Settings.Builder`. Settings that also have an XML attribute list it next to the builder method.

### acceptThirdPartyCookies

`app:ada_accept_third_party_cookies="true"` or `.acceptThirdPartyCookies(true)`

Allows the WebView to accept third-party cookies. Defaults to `false`. Only set this if an Ada integration you use requires it.

### appUrl

`.appUrl("https://apps.example.com/ada-app/")`

Overrides the app frame URL that the Messaging runtime mounts.

> **Note**
>
> Your handle's **Allowed websites** list controls custom apps. In your dashboard, go to **Channels** > **Messaging**. Before your transition, use **Channels** > **Chat**. Add your app's origin. If the list does not allow the origin, the runtime drops the URL with a logged warning and mounts the default app.

The value must be an absolute `https:` URL. The runtime drops invalid values with a logged warning and mounts the default app instead. If the custom app fails to load or complete its handshake, the runtime falls back to the default app.

For local development against an emulator, expose your development server through an `https:` tunnel, and add the tunnel origin to Allowed websites. The dashboard accepts only `https://` entries. On the production and pre-production environments, the WebView's host page is an Ada asset origin, not a loopback page. The web SDK's implicit loopback allowance therefore does not apply there. The emulator reaches the tunnel origin directly, so no port forwarding is needed.

The exception is the local asset-server environment ([`AdaEnvironment.Local`](#adaenvironment)), used for SDK development. Its WebView host page is `https://localhost:4900`, a loopback origin, so the implicit loopback allowance applies and a loopback `appUrl` needs no entry. The emulator's `10.0.2.2` host alias is not a loopback host, so the runtime rejects it as an `appUrl`. A host page served from that alias also loses the implicit allowance, for the same reason. Instead, forward the port so the emulator reaches your development server at `http://localhost:5173`:

```sh
adb reverse tcp:5173 tcp:5173
```

The SDK delivers the value to the web runtime through an injected configuration object, not through the WebView URL. Unlike [`identityToken`](#identitytoken), which is delivered once, the value is delivered again on every document load. See [Custom apps](/messaging/custom-apps/overview). Ignored on the Legacy runtime.

### cluster

`app:ada_cluster="ca"` or `.cluster("ca")`

Specifies the cluster your AI Agent runs on. Set this only if your Agent is hosted on a non-default cluster. If the Agent is on the default cluster, leave this unset.

> **Info**
>
> Do not change this value unless instructed by your Ada team.

### deviceToken

`app:ada_device_token="fcm-device-token"` or `.deviceToken("fcm-device-token")`

Pass the FCM device token at startup. Registration timing depends on the platform and runtime:

| Platform             | Runtime              | Device token registration                    |
| -------------------- | -------------------- | -------------------------------------------- |
| Android              | Legacy and Messaging | At initialization, before the first message. |
| iOS and React Native | Messaging            | Once the runtime is ready.                   |

See [`setDeviceToken`](#setdevicetoken) for storage, push notification guidance, and updates after initialization.

### domain

`.domain("acme")`

The Ada domain your AI Agent is served from. Applies to the Legacy runtime only. The Messaging runtime resolves its host from [`cluster`](#cluster) and [`environment`](#environment).

> **Info**
>
> Do not set this value unless instructed by your Ada team.

### embedVersion

`.embedVersion("<build-sha>")`

Pins the Legacy runtime to a specific embed build for pre-release validation. Leave blank for the stable rollout. Ignored on the Messaging runtime.

> **Info**
>
> Only use this when the Ada team gives you a build to validate.

### enableProgrammaticControl

`.enableProgrammaticControl(true)`

Opts your app into the Messaging runtime's programmatic control surface. Defaults to `false`. When set to `true`, your app can:

* Call [`selectQuickReply`](#selectquickreply), [`sendMessage`](#sendmessage) and [`triggerPlaybook`](#triggerplaybook).
* Receive the `ada:message:sent` and `ada:message:received` events through [`addEventCallback`](#addeventcallback).

While the flag is `false`, the runtime rejects `selectQuickReply`, `sendMessage` and `triggerPlaybook` with a `ProgrammaticControlNotEnabled` error. It does not deliver those events to your callbacks.

The SDK forwards this setting only to Messaging. Use `webSdk(AdaWebSdk.Messaging)` and `enableProgrammaticControl(true)` for supported programmatic sends and message-body events.

> **Warning**
>
> Turning this on delivers full message bodies to your native code. The `ada:message:sent` and `ada:message:received` events carry the conversation transcript, including any PII your end users type. Have your product and security teams accept that exposure explicitly before you enable it.

### environment

`.environment(AdaEnvironment.Production)`

Selects the Ada deployment environment. Defaults to production. See the [AdaEnvironment](#adaenvironment) type for the available values. Most apps never set this.

### greeting

`app:ada_greeting="5c59aaabd8269e0339979014"` or `.greeting("5c59aaabd8269e0339979014")`

Customizes the greeting message that new end users see. The value is the ID of the Answer you want to use. You can find the ID in the URL of the corresponding Answer in the dashboard.

> **Note**
>
> This setting only applies if you're using a **scripted AI Agent**.

> **Note**
>
> The plural forms `.greetings(...)` and `app:ada_greetings` are accepted as compatibility aliases. Use the singular `greeting` form in new code.

### handle

`app:ada_handle="my-handle"` or `AdaMessagingView.Settings.Builder("my-handle")`

The handle for your AI Agent. This is a required field, passed as the `Settings.Builder` constructor argument.

### headless

`.headless(true)`

Runs the Messaging runtime without its default UI. Defaults to `false`. The runtime still boots, connects, and emits events, and programmatic commands such as [`sendMessage`](#sendmessage) still work. Only the rendered conversation surface is suppressed.

Use this when your app renders its own conversation UI, or when it only consumes conversation events. Pair it with the [`AdaMessagingView.headless`](#headless-mode) factory to run the view without attaching it to a layout, and with [`enableProgrammaticControl`](#enableprogrammaticcontrol) when you need `sendMessage`.

Ignored on the Legacy runtime. See [Headless mode](#headless-mode).

> **Note**
>
> Headless sessions run with no visual indicator to the end user. The conversation session is created the same way as a visible session. Treat enabling headless as a meaningful trust decision for your app.

### identityToken

`.identityToken(token)`

Authenticates the end user with a one-shot identity token minted by your backend. The Messaging runtime exchanges the token for an authenticated session during startup.

Your server can supply optional `verified_email` and `name` values when it mints the token.
Ada links only when the mint request includes `verified_email`.
Without either field, mint, exchange, and handoff keep their existing behavior.
Ada stores the values encrypted on the session's end user.
Ada uses them only in the first 7 days after the exchange.
Ada deletes them after the first Zendesk Messaging handoff uses them and Sunshine initialization succeeds.
If no handoff uses them, Ada deletes them at the next handoff or token exchange after the 7 days end.

The flow is:

1. Your backend requests a short-lived, single-use JWT for the signed-in end user from Ada's `POST /v2/auth/tokens/` endpoint.
2. Your app passes that token to `.identityToken(...)` when it builds the settings.
3. The runtime performs the exchange before the conversation starts.

**`Kotlin`**

```kotlin Kotlin
val settings = AdaMessagingView.Settings.Builder("my-handle")
    .webSdk(AdaWebSdk.Messaging)
    .identityToken(tokenFromYourBackend)
    .build()
```

Each token is single-use and expires quickly. Mint a fresh token from your backend for each initialization. Do not reuse tokens, and do not mint them on the device.

The SDK hands the token to the runtime through a secure native channel. The token never appears in a URL, and the SDK never persists it to disk.

Settings passed through `AdaMessagingActivity.EXTRA_SETTINGS` or `AdaMessagingDialog.ARGUMENT_SETTINGS` lose the token after process death or repeated unparceling. A persisted session may still resume. Supply a fresh token when a new identity exchange is required.

Exchange failures surface as the `ada:identity_token:error` and `ada:identity_token:expired` events through [`addEventCallback`](#addeventcallback).

Ignored on the Legacy runtime.

### language

`app:ada_language="en"` or `.language("en")`

Sets the initial display language. To use the same language for AI Agent replies, first turn languages on in your Ada dashboard. Go to **Customization** > **Languages**. See [Support multiple languages in the same AI Agent](/docs/setup/languages/about-multilingual-support) for more information.

With the Messaging runtime, this setting applies on every WebView launch and takes precedence over a saved language choice. A language picker or [`setLanguage`](#setlanguage) change during that launch is still saved when session persistence is enabled. This setting takes precedence again on the next launch. Omit this setting to allow a saved choice to restore.

Later language updates from the Agent or another widget sharing the session can change the display language. With session persistence enabled, the widget saves its current display language.

When activation restores an existing session, it synchronizes the configured language if the Agent supports it. If the reply language changes, other open widgets using the same session apply the update live, even during a conversation.

If the Agent does not support the display language, or the language update fails, the display language can differ from the Agent's reply language.
An unsupported configured value can still replace a supported saved choice when session persistence is enabled.

> **Note**
>
> Language codes use the [ISO 639-1 language format](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).

### loadTimeoutMillis

`app:ada_load_timeout="10000"` or `.loadTimeoutMillis(10000)`

Sets the WebView load timeout in milliseconds. Defaults to `30000`. When the timeout elapses before the conversation loads, the SDK invokes [`webViewLoadingErrorCallback`](#webviewloadingerrorcallback).

### metaFields

`app:ada_metaFields="@raw/meta_fields"` or `.metaFields(builder)`

Passes information about an end user to Ada. Use this to track information about your end users, and to personalize their experience. For example, you may wish to track the `phone_number` and `name` for conversation attribution.

**`Kotlin`**

```kotlin Kotlin
.metaFields(
    AdaMessagingView.MetaFields.Builder()
        .setField("name", "John")
        .setField("age", 20)
        .setField("authorized", true),
)
```

To set metaFields in XML, create a JSON file in the `res/raw` directory, and reference it from the view declaration.

To change these values after initialization, use the [setMetaFields](#setmetafields) action.

### sensitiveMetaFields

`app:ada_sensitiveMetaFields="@raw/sensitive_meta_fields"` or `.sensitiveMetaFields(builder)`

Passes sensitive information about an end user. This works like [metaFields](#metafields), but the values are not stored in the database, and are deleted after 24 hours.

The SDK forwards these fields at initialization, so an identified session carries them from the first message. On the Legacy runtime they ride the start config. On the Messaging runtime they ride a document-start injection. The values never appear in a URL.

To change these values after initialization, use the [setSensitiveMetaFields](#setsensitivemetafields) action.

### styles

`app:ada_styles="..."` or `.styles("...")`

Passes style overrides to the conversation. The value's meaning depends on the runtime:

* **Legacy runtime**: a CSS style override string, forwarded to the conversation frame.
* **Messaging runtime**: no effect. The runtime ignores the value and logs a warning.

### version

`.version("<build-sha>")`

Pins the Legacy runtime's conversation UI to a specific build for pre-release validation. Leave blank for the stable rollout. Ignored on the Messaging runtime.

> **Info**
>
> Only use this when the Ada team gives you a build to validate.

### webSdk

`app:ada_web_sdk="legacy|messaging"` or `.webSdk(AdaWebSdk.Messaging)`

Selects the web runtime the SDK loads. Defaults to `AdaWebSdk.Legacy`, which preserves the behavior of Ada's existing Chat SDK. Set `AdaWebSdk.Messaging` to use the next-generation Messaging runtime.

The [enableProgrammaticControl](#enableprogrammaticcontrol), [headless](#headless), and [identityToken](#identitytoken) settings apply to the Messaging runtime only. The [selectQuickReply](#selectquickreply) action and the [sendMessage](#sendmessage) action's programmatic gate also require that runtime.

## Callback properties

Set these properties directly on your `AdaMessagingView` instance.

### filePickerCallback

Fires when an end user asks to attach a file. The `FilePickerCallback` argument describes the request and resolves it.

| Member                                    | Description                                                                                                                                                                                         |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `acceptTypes: List<String>`               | MIME types the chat accepts. A file extension Android cannot map stays as written. An empty list means any type.                                                                                    |
| `allowMultiple: Boolean`                  | The chat accepts more than one file.                                                                                                                                                                |
| `createChooserIntent(context): Intent`    | The system chooser for this request, with the camera when the chat accepts images. For a capture-only request with a camera available, the camera intent itself.                                    |
| `isCaptureEnabled: Boolean`               | The chat asks for a new photo from the camera.                                                                                                                                                      |
| `onChooserResult(resultCode, data)`       | Resolves the request from the chooser's activity result. It passes only `content://` files from other apps, so a picker app cannot return a `file://` path or a file from your app's own providers. |
| `onFilesTaken(uris)` / `onFileTaken(uri)` | Resolves the request with your own picker's result. To cancel, call `onFileTaken(null)` or `onFilesTaken(emptyList())`.                                                                             |

Resolve each request once, including when the launch fails and when your Activity is destroyed. The chat ignores later file picks while a request is open. Return `true` to signal that your app handles the picker.

**`Kotlin`**

```kotlin Kotlin
adaView.filePickerCallback = { request ->
    pendingFileRequest = request
    try {
        filePicker.launch(request.createChooserIntent(this))
    } catch (e: ActivityNotFoundException) {
        request.onFileTaken(null)
        pendingFileRequest = null
    }
    true
}
```

`AdaMessagingActivity` and `AdaMessagingDialog` handle file selection for you.

### webViewErrorCallback

Fires whenever [`webViewLoadingErrorCallback`](#webviewloadingerrorcallback) fires, with a typed [`AdaWebViewError`](#adawebviewerror) that describes the reason. When both callbacks are set, both fire.

**`Kotlin`**

```kotlin Kotlin
adaView.webViewErrorCallback = { error ->
    when (error) {
        is AdaWebViewError.Timeout -> showRetry()
        is AdaWebViewError.HttpError -> log(error.statusCode)
        is AdaWebViewError.LoadFailed -> log(error.description)
        is AdaWebViewError.BridgeError -> log(error.error)
    }
}
```

### webViewLoadingErrorCallback

Fires when the conversation fails to load, including when [`loadTimeoutMillis`](#loadtimeoutmillis) elapses. To learn why the load failed, set [`webViewErrorCallback`](#webviewerrorcallback) instead, or in addition.

**`Kotlin`**

```kotlin Kotlin
adaView.webViewLoadingErrorCallback = {
    // Show your own error state or retry.
}
```

### zdChatterAuthAsyncCallback

Use this variant of [`zdChatterAuthCallback`](#zdchatterauthcallback) to fetch JWTs over the network for Zendesk Chat and Zendesk Messaging Handoffs.
The SDK invokes your callback with a `respond` function.
Call `respond(token)` from any thread when your request completes. Call `respond(null)` if no token is available.
For Zendesk Messaging, authentication failure lets the Handoff proceed with an anonymous Sunshine user when no other identity mapping applies.

**`Kotlin`**

```kotlin Kotlin
adaView.zdChatterAuthAsyncCallback = { respond ->
    fetchTokenFromApi { token ->
        respond(token)
    }
}
```

The SDK accepts one answer per request and drops late answers for a superseded request. When both callbacks are set, `zdChatterAuthAsyncCallback` takes precedence.

### zdChatterAuthCallback

Use `zdChatterAuthCallback` to authenticate Zendesk Chat and Zendesk Messaging Handoffs with a JWT from your server.

**`Kotlin`**

```kotlin Kotlin
adaView.zdChatterAuthCallback = {
    getToken() // this function returns String
}
```

The SDK invokes this callback on the main thread, so it must return quickly. To fetch the token over the network, use [`zdChatterAuthAsyncCallback`](#zdchatterauthasynccallback) instead.

For Zendesk Messaging, return your Zendesk Messaging authentication JWT. Sign it with a Zendesk Messaging signing key using HS256.
Include the signing key ID in the `kid` header. Include the `scope: "user"`, `external_id`, and `exp` claims.
Set `exp` to an expiry time in Unix seconds. Keep the signing secret on your server.

The `external_id` must be a non-empty string of at most 255 characters.
It must not contain `/`, `?`, `#`, `%`, whitespace, or control characters.
Send the required `external_id` claim and the optional `name`, `email`, and `email_verified` claims.

Verified email linking requires the Messaging SDK.

> **Warning**
>
> Ada links to or creates a Zendesk user from the email.
> A wrong email links the conversation to another person's Zendesk user.
>
> [Read all risks](/docs/handoffs/zendesk/zendesk-messaging#authenticate-end-users)

Ada uses `name` and `email` for display and linking only when the JWT has an `email` with boolean `email_verified: true`.
Without a verified email, Ada uses only `external_id`.
With a verified email, Ada writes META `jwt_name` and `jwt_email`, as it does for Zendesk Chat.
Ada updates `name` only when it is empty or still equals the previous JWT's name.
Ada never writes META `email` from these claims.
This preserves the email that your page or a **Capture** block sets.

With a verified email, Ada first uses a Zendesk end user with the same `external_id` as the JWT.
With `email_verified: true`, Ada next matches exactly one Zendesk end user by primary email.
Ada trims whitespace and converts the email to lowercase before matching.
Ada uses the matched user's external ID or sets it to the JWT's `external_id` if missing.
When Ada sets a missing external ID on the Zendesk user, Zendesk keeps that link for later conversations.

If a verified email has no match, Ada creates a Zendesk end user with the JWT's name and email.
If the JWT has no name, Ada uses the email as the name.

If Ada cannot select or create a Zendesk end user, Ada uses the JWT's `external_id`.

Ada uses the JWT's email to match or create a Zendesk user only when `email_verified` is `true`.
Ada never links agents or admins, or a match that is not exact.
See [Authenticate end users](/docs/handoffs/zendesk/zendesk-messaging#authenticate-end-users) for the full matching rules.

The SDK requests one token at startup, including when both Zendesk platforms are configured.
Zendesk Messaging authentication does not require the Zendesk Chat feature. Existing Zendesk Chat callbacks need no changes.

After successful verification, the server stores the identity and its validity window on the end user record.
Each successful startup check renews the window for 24 hours.
Browser metadata cannot supply or change these values.

If no valid token or signing secret is available, the Handoff uses an anonymous Sunshine user unless another identity mapping applies.
When a user signs out, call `adaView.reset()` so the next user starts a new conversation and a new Zendesk user.

The Android SDK invokes this callback on the Messaging runtime only. On the Legacy runtime, a Zendesk Chat handoff proceeds unauthenticated.

## Actions

Use the actions below in conjunction with settings to customize the behavior of your AI Agent in an Android app.

> **Note**
>
> The following actions apply to `AdaMessagingView`. To call actions on `AdaMessagingActivity` or `AdaMessagingDialog`, see [AdaMessagingActivity and AdaMessagingDialog](#adamessagingactivity-and-adamessagingdialog).

Actions called before the runtime reports ready are queued, then run once the `sdk.ready` event fires.

### addEventCallback

`addEventCallback(eventName: String = "*", callback: (event: Event) -> Unit)`

Subscribes to conversation events. Pass an exact event key to receive only that event, or omit the key to receive every forwarded event through the `"*"` catch-all.

**`Kotlin`**

```kotlin Kotlin
adaView.addEventCallback("ada:message:received") { event ->
    // A message from the AI Agent arrived.
}

adaView.addEventCallback { event ->
    Log.d("Ada", "event=${event.eventName}")
}
```

Useful event keys include:

* `ada:identity_token:error` and `ada:identity_token:expired` report [identityToken](#identitytoken) exchange failures.
* `ada:message:received` and `ada:message:sent` fire per message on the Messaging runtime, and require [enableProgrammaticControl](#enableprogrammaticcontrol).
* `ada.bridge.error` reports a native bridge error.
* `ada.webview.loaded`, `ada.webview.loadFailed`, `ada.webview.subresourceLoadFailed`, and `ada.webview.mainDocumentLost` report the WebView's own lifecycle. See [WebView lifecycle events](#webview-lifecycle-events).
* `ada.sessionMirror.diagnostic` reports a degraded [session mirror](#session-persistence). See [WebView lifecycle events](#webview-lifecycle-events).
* `sdk.ready` fires when the runtime is ready for commands.

Remove subscriptions with `removeEventCallback(callback)` or `removeEventCallbacks(eventName)`.

### addSdkEventCallback

`addSdkEventCallback(callback: (key: String, data: String?) -> Unit)`

Subscribes to every event with its raw key and JSON payload. Use this callback for complete Messaging and lifecycle payloads, including the `message` from `ada:message:received`. Use this instead of [`addEventCallback`](#addeventcallback) when you need the unparsed payload. Remove subscriptions with `removeSdkEventCallback(callback)`.

### clearPersistedState

`adaView.clearPersistedState()`

Removes the SDK's natively persisted state. This includes the cached branding snapshot that smooths the next launch. From version 1.2.0, it also includes the persisted session mirror. From version 1.6.0, it also deletes the camera photos the file chooser saved. Call it when an end user signs out. The next session starts without rehydrated state. See [Session persistence](#session-persistence).

The method returns no result. It runs the wipe in the background. It does not block the calling thread, so it is safe to call on the main thread. To confirm the wipe, use [`clearPersistedStateDurably`](#clearpersistedstatedurably).

The call works before [`initialize`](#initialize). It does not touch the web runtime's own storage. To clear the conversation itself, use [`deleteHistory`](#deletehistory) or [`reset`](#reset).

The dialog drops `clearPersistedState` before `onViewCreated` and after `onDestroyView`. `show()` is asynchronous.
Clear native state through `AdaMessagingView` when needed independently. Clear the live conversation separately. Run durable clearing off the main thread.

### clearPersistedStateDurably

`adaView.clearPersistedStateDurably(): Boolean`

Removes the same natively persisted state as [`clearPersistedState`](#clearpersistedstate), and returns the result. The method returns `false` when the SDK could not delete the camera photos or could not confirm the session-mirror wipe on disk. If the method returns `false`, call it again.

This method blocks the calling thread. It runs the wipe in the background, and the caller waits. The wait ends when the wipe completes or a timeout of about 3 seconds elapses. Call it off the main thread. Use it when your sign-out flow must confirm the wipe before it continues.

### deleteHistory

`adaView.deleteHistory()`

Deletes the end user's conversation history from storage. It also deletes the camera photos the file chooser saved. When the end user opens a new conversation, the SDK creates a new end-user record.

### downloadTranscript

`adaView.downloadTranscript(fileName: String, data: String)`

Saves a transcript to the device's Downloads folder. `data` is a base64 URI with the prefix `data:text/plain; charset=utf-8;base64,`.
The method shows a toast and returns no result. It reports no failure to your app.

### initialize

`adaView.initialize(settings)`

Initializes the view with an `AdaMessagingView.Settings` object and starts loading the conversation. Required when the view was created programmatically, or when the XML declaration has no `ada_handle` attribute.

### reset

`adaView.reset()`

Creates a new end-user session and refreshes the conversation. It also deletes the camera photos the file chooser saved.

Reset can also take optional parameters to apply to the new session: `language`, `greetings`, `metaFields`, `sensitiveMetaFields`, and `resetChatHistory`:

**`Kotlin`**

```kotlin Kotlin
adaView.reset(
    language = "en",
    metaFields = AdaMessagingView.MetaFields.Builder()
        .setField("plan", "pro"),
    sensitiveMetaFields = AdaMessagingView.MetaFields.Builder()
        .setField("authToken", "secure-session-token"),
    resetChatHistory = true,
)
```

### selectQuickReply

`adaView.selectQuickReply(messageId: String, index: Int)`

Selects a quick reply through the same send path as a tap in the built-in UI.
Requires the Messaging runtime and [`enableProgrammaticControl(true)`](/messaging/android/sdk-api-reference#enableprogrammaticcontrol).
Calls queue until the runtime is ready. The method returns no result. The legacy runtime does not support it.

| Parameter   | Type     | Description                                                                      |
| ----------- | -------- | -------------------------------------------------------------------------------- |
| `messageId` | `String` | The non-empty message `id` from `ada:message:received`.                          |
| `index`     | `Int`    | A non-negative integer index into `message.data.quickReplies`, starting at zero. |

Selection starts engagement and activates the chat, like `sendMessage`.
The runtime starts if needed. In visible mode, it opens a closed chat or reloads the timeout fallback.
The selected user message carries `origin: "programmatic"` in `getMessages()` and `ada:message:sent`.

`SEND_FAILED` means the server confirmed rejection with HTTP 4xx.
The runtime withdraws that answer and releases the block in the app display state and host selection checks.
The host can select the same message again if it remains current and passes the rate limit and other checks.
`SEND_UNCONFIRMED` means HTTP 5xx, a transport error, or another unconfirmed outcome.
The block stays answered because the selection may already have taken effect.
Another selection of that message rejects with `ALREADY_ANSWERED`.

`SEND_DROPPED` means the runtime could not start the send.
`INVALID_QUICK_REPLY` means the stored chip has no required target or usable label or value.
If the conversation or session changes during the request, the runtime treats the selection as accepted, as `sendMessage` does.
It does not apply the send result to the replacement state or publish `ada:message:sent` for that request.
See the [Web rejection codes](/messaging/web/sdk-api-reference#selectquickreply) for all selection checks.

The host sends only the message ID and index. The runtime reads the reply fields from its own transcript.
`addSdkEventCallback` receives the JSON payload, which contains `message`.
Use this callback to read the complete message. `addEventCallback` exposes only the event name and `user_data`.
Quick-reply messages have `id`, `type: "quick_replies"`, and `data.quickReplies`.

This example reads the received replies and selects the first reply.

**`Kotlin`**

```kotlin Kotlin
adaView.addSdkEventCallback { key, rawData ->
    if (key != "ada:message:received" || rawData == null) return@addSdkEventCallback
    val message = org.json.JSONObject(rawData).optJSONObject("message")
        ?: return@addSdkEventCallback
    if (message.optString("type") != "quick_replies") return@addSdkEventCallback
    val quickReplies = message.optJSONObject("data")?.optJSONArray("quickReplies")
        ?: return@addSdkEventCallback
    if (quickReplies.length() > 0) {
        adaView.selectQuickReply(message.getString("id"), 0)
    }
}
```

### sendMessage

`adaView.sendMessage(message: String)`

Sends a message into the conversation on the end user's behalf, as if they had typed it. Calls queue until the runtime reports `sdk.ready`, so you can call it right after [`initialize`](#initialize).

**`Kotlin`**

```kotlin Kotlin
adaView.sendMessage("I need help with my order")
```

Use `webSdk(AdaWebSdk.Messaging)` and [`enableProgrammaticControl(true)`](#enableprogrammaticcontrol) for supported programmatic sends. Without programmatic control, Messaging rejects the send with `ProgrammaticControlNotEnabled`.

### setDeviceToken

`adaView.setDeviceToken(token: String)`

Stores or updates the device token with the end user's sensitive metadata. To send push notifications, use webhooks and your push provider. See [Push notifications](/docs/channels/messaging/push-notifications).

### setLanguage

`adaView.setLanguage(language: String)`

Changes the conversation language programmatically. Use this action, rather than the `language` setting, to change the language without clearing the conversation history. Language codes must use a lowercase, two-letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code.

**`Kotlin`**

```kotlin Kotlin
adaView.setLanguage("fr")
```

Before using `setLanguage`, you must turn languages on in your Ada dashboard. Calls made before the runtime reports `sdk.ready` are queued and dispatched when it becomes ready, so you can call it right after [`initialize`](#initialize).

### setMetaFields

Changes your [metaFields](#metafields) while the conversation is open:

**`Kotlin`**

```kotlin Kotlin
adaView.setMetaFields(
    AdaMessagingView.MetaFields.Builder()
        .setField("name", "John")
        .setField("age", 20)
        .setField("authorized", true),
)
```

### setSensitiveMetaFields

Changes your [sensitiveMetaFields](#sensitivemetafields) while the conversation is open:

**`Kotlin`**

```kotlin Kotlin
adaView.setSensitiveMetaFields(
    AdaMessagingView.MetaFields.Builder()
        .setField("authToken", "rotated-session-token"),
)
```

> **Note**
>
> `Map<String, String>` overloads for `setMetaFields`, `setSensitiveMetaFields`, and `reset` still exist for backward compatibility, but are deprecated. Use `MetaFields.Builder` for all new code.

### triggerPlaybook

**`Kotlin`**

```kotlin Kotlin
fun triggerPlaybook(
    playbookId: String,
    metaFields: Map<String, Any?>? = null,
    sensitiveMetaFields: Map<String, Any?>? = null,
    callback: (AdaBridgeInterface.AdaBridgeResponse) -> Unit = {}
)
```

Starts a specific Playbook in the current conversation.

> **Note**
>
> This method is in Early Access.
> It requires [`enableProgrammaticControl`](#enableprogrammaticcontrol) and an active v2 Playbook marked as startable from the SDK.
> Set `startable_from_sdk: true` through the dashboard API, or ask the Ada team to mark the Playbook.
> This Playbook setting defaults to `false`.

Pass the Playbook ID as a 24-character hexadecimal string.
The SDK sends `metaFields` and `sensitiveMetaFields` with the trigger.
For a valid Playbook ID within the rate limit, the API stores metadata before it checks Playbook eligibility.
It also applies a supported `language`.
It also binds the push device token when push notifications are enabled.
These updates persist even when a later check refuses the trigger. A rate-limited request stores nothing.

If the API returns `language_not_supported`, it keeps the current language and skips push device binding and Playbook activation.
Other metadata and email fields remain stored under the same rules as the metadata setters.

If either metadata field has a non-null value that is not an object, the API returns HTTP 409 with `failure_reason: "invalid_metadata"`.
The API stores no metadata and does not activate the Playbook for this refusal.

Use strings, numbers, booleans, or null values for metadata, as with [`setMetaFields`](#setmetafields).
The Playbook must allow standalone use and pass its availability rules for the end user.
The API refuses requests during a live handoff or when another Playbook is running.
Each end user can make three valid-ID attempts with valid metadata objects per minute per AI Agent.

The callback receives `Success` with null when the API accepts the trigger.
It receives `Failure` on refusal, invalid input, metadata write failure, or timeout.
The failure includes the reason, such as `triggerPlaybook rejected: playbook_running`.
`AdaMessagingDialog` forwards this method with the same signature.

**`Kotlin`**

```kotlin Kotlin
adaView.triggerPlaybook(
    "507f1f77bcf86cd799439011",
    metaFields = mapOf("orderId" to "order-123"),
    sensitiveMetaFields = mapOf("accountToken" to "<account-token>"),
) { response ->
    if (response is AdaBridgeInterface.AdaBridgeResponse.Failure) {
        println(response)
    }
}
```

HTTP 503 with `activation_indeterminate` means the API cannot confirm whether the Playbook started.
The rejection does not mean the Playbook did not start.
Timeouts and network errors can also leave the result unknown. Do not retry automatically.
The Legacy runtime reports this method as unsupported.

## Curated bridge requests

`AdaMessagingView` exposes a fixed set of methods that ask the running chat for state or drive it. Each method takes a callback. The callback receives one `AdaBridgeInterface.AdaBridgeResponse`. That response is `Success`, `Unsupported`, or `Failure`.

These methods require the Messaging runtime. Set `webSdk(AdaWebSdk.Messaging)`. On the default Legacy runtime, every method returns `Unsupported`.

A method returns `Unsupported` when the running runtime build does not implement it. A method returns `Failure` in any of three cases:

* The runtime returns an error, such as a bad argument.
* No answer arrives before the request times out.
* The runtime sends a malformed response that carries none of a result, an error, or an unsupported outcome.

A method never silently does nothing.

Reads return public models only. No read returns sensitive meta fields, the internal stores, or persisted session data. Writes and triggers succeed with a null result.

Calls made before the runtime reports `sdk.ready` are queued, then run once it becomes ready. Reads take a required callback. Writes and triggers take an optional callback. `triggerPlaybook` and the message reads `getConversation` and `getMessages` require [`enableProgrammaticControl`](#enableprogrammaticcontrol). That setting defaults to `false`. The other reads, writes, and triggers do not. The default differs per platform. React Native defaults it to `true`. There the same two reads resolve without the flag.

| Method                                                                   | Result on the Messaging runtime | Description                                                             |
| ------------------------------------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------- |
| `close(callback)`                                                        | `Unsupported`                   | The SDK cannot close a parent-hosted webview.                           |
| `closeCampaign(callback)`                                                | `Success` with null             | Dismisses the active campaign.                                          |
| `getConversation(callback)`                                              | `Success` with the model        | The public conversation model.                                          |
| `getInfo(callback)`                                                      | `Success` with the model        | The public AI Agent and session info.                                   |
| `getMessages(callback)`                                                  | `Success` with the model        | The public message list.                                                |
| `getMetaFields(callback)`                                                | `Success` with the model        | The non-sensitive meta fields set on the session.                       |
| `isOpen(callback)`                                                       | `Success` with the value        | Whether the chat window is open.                                        |
| `open(callback)`                                                         | `Unsupported`                   | The SDK cannot open a parent-hosted webview.                            |
| `setTheme(theme, callback)`                                              | `Success` with null             | Sets the runtime theme to `"light"`, `"dark"`, or `"auto"`.             |
| `toggle(callback)`                                                       | `Unsupported`                   | The SDK cannot toggle a parent-hosted webview.                          |
| `triggerAnswer(answerId, callback)`                                      | `Success` with null             | Triggers a specific answer by id.                                       |
| `triggerGreeting(handle, callback)`                                      | `Success` with null             | Triggers the greeting, optionally for a specific handle.                |
| `triggerPlaybook(playbookId, metaFields, sensitiveMetaFields, callback)` | `Success` with null             | Starts a Playbook. Early Access. Requires programmatic control.         |
| `triggerProactive(messageKey, params, callback)`                         | `Success` with null             | Shows a proactive message by key, with optional variable substitutions. |

**`Kotlin`**

```kotlin Kotlin
adaView.getConversation { response ->
    if (response is AdaBridgeInterface.AdaBridgeResponse.Success) {
        // Use the public conversation model.
    }
}

adaView.setTheme("dark")
```

## WebView lifecycle events

The SDK synthesizes events for the WebView's own lifecycle and for the session mirror. It delivers them through [`addEventCallback`](#addeventcallback) and [`addSdkEventCallback`](#addsdkeventcallback), alongside the runtime's events:

| Key                                    | Fires when                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ada.webview.contentProcessTerminated` | The WebView content process ends on Android API 26 or later. Reported once per terminal state. The payload includes only `url`, when known.                                                                                                                                                                                                                                                                                                             |
| `ada.webview.loaded`                   | The WebView finishes loading its page. The payload includes the loaded `url`.                                                                                                                                                                                                                                                                                                                                                                           |
| `ada.webview.loadFailed`               | The initial load fails: a main-frame resource error, an HTTP status of 400 or higher, a load timeout, or a URL build failure. The payload includes `url`, `code`, `statusCode`, and `error`. [`webViewLoadingErrorCallback`](#webviewloadingerrorcallback) and [`webViewErrorCallback`](#webviewerrorcallback) also fire.                                                                                                                               |
| `ada.webview.subresourceLoadFailed`    | A resource inside the page fails to load after the main document: a subresource error or an HTTP status of 400 or higher. Reported once per URL per page load. The payload includes `url`, `code`, `statusCode`, and `error`. The error callbacks do not fire, because the main frame is intact.                                                                                                                                                        |
| `ada.webview.mainDocumentLost`         | The main frame leaves the trusted Ada document, so the chat bridge stops working. The payload includes `url` and `recovering`. `recovering` is `true` when the SDK reloads the trusted document. [`webViewLoadingErrorCallback`](#webviewloadingerrorcallback) and [`webViewErrorCallback`](#webviewerrorcallback) also fire.                                                                                                                           |
| `ada.sessionMirror.diagnostic`         | The SDK cannot complete a [session mirror](#session-persistence) operation. The payload includes one `reason` string and no session data. The SDK reports each reason once per loaded document, except `adapter-removeItem-failed`, which it reports every time. Reasons: `adapter-getItem-failed`, `adapter-removeItem-failed`, `adapter-setItem-failed`, `entries-not-allowlisted`, `legacy-rollout-unresolved`, `origin-mismatch`, `scope-mismatch`. |

On `ada.webview.contentProcessTerminated`, create a new `AdaMessagingView`.

## Headless mode

Headless mode runs the Messaging runtime without visible conversation UI. Your app receives conversation events and sends messages through native code. Use it to build native surfaces such as unread badges, notification banners, or a custom composer.

Two pieces combine into headless operation:

1. The [`headless`](#headless) setting tells the runtime to suppress its default UI. The runtime still boots, connects, and emits events.
2. The `AdaMessagingView.headless(context, settings)` factory constructs the view without a layout tree. The view starts loading immediately instead of waiting for window attachment.

You can also place a normal `AdaMessagingView` in your layout at zero size. The factory exists so you do not have to.

### The headless factory

`AdaMessagingView.headless(context: Context, settings: Settings): AdaMessagingView`

Builds a view that runs without joining the layout tree. The factory forces `headless = true` on the settings it receives. It requires `webSdk(AdaWebSdk.Messaging)`, and throws `IllegalArgumentException` when the settings target the Legacy runtime.

### Example: drive a native unread badge

**`Kotlin`**

```kotlin Kotlin
val settings = AdaMessagingView.Settings.Builder("my-handle")
    .webSdk(AdaWebSdk.Messaging)
    .enableProgrammaticControl(true)
    .identityToken(tokenFromYourBackend)
    .build()

val ada = AdaMessagingView.headless(context, settings)

ada.addEventCallback("ada:message:received") { event ->
    unreadBadge.increment()
}
ada.addEventCallback("ada:message:sent") { event ->
    unreadBadge.clear()
}

ada.sendMessage("I need help with my order")
```

`sendMessage` calls queue until the runtime reports `sdk.ready`, so you can call it right after the factory returns.

### Constraints

* **Messaging runtime only.** The factory rejects `AdaWebSdk.Legacy` with an `IllegalArgumentException`.
* **`sendMessage` needs programmatic control.** The runtime rejects the send with `ProgrammaticControlNotEnabled` unless you set [`enableProgrammaticControl(true)`](#enableprogrammaticcontrol).
* **No background execution.** The WebView runs only while your process lives. Use [webhooks and your push provider](/docs/channels/messaging/push-notifications) to send push notifications while your app is closed.
* **One conversation session per view.** Do not create a headless view and a visible view for the same end user at the same time. Destroy the headless view before you show a visible view for the same end user.

### Lifecycle you still own

An unattached WebView does not receive lifecycle callbacks from the framework. Forward them yourself:

* Call `ada.onResume()` when your owning screen or app comes to the foreground.
* Call `ada.onPause()` when it leaves the foreground. This pauses timers and reduces battery use.
* Call `ada.destroy()` when you are done with the view. Remove your event callbacks first.

Tie these calls to a `LifecycleObserver` on the owning activity, fragment, or process lifecycle.

### Showing UI later

A headless view keeps its headless setting after attachment. Ada's conversation UI does not appear.
To show Ada's UI, destroy the headless view and initialize a non-headless `AdaMessagingView`.

## AdaMessagingActivity and AdaMessagingDialog

To call actions on `AdaMessagingActivity`, call `getAdaView()` to obtain the `AdaMessagingView` instance. The method is public, so any code that holds the activity instance can call it. It is available from `onCreate` onward. For example, from a subclass:

**`Kotlin`**

```kotlin Kotlin
class MyCustomActivity : AdaMessagingActivity() {
    override fun onResume() {
        super.onResume()
        val adaView = getAdaView()
        adaView.setMetaFields(
            AdaMessagingView.MetaFields.Builder()
                .setField("name", "John"),
        )
    }
}
```

`AdaMessagingDialog` forwards the common actions directly: `clearPersistedState`, `deleteHistory`, `downloadTranscript`, `reset`, `selectQuickReply`, `sendMessage`, `setDeviceToken`, `setLanguage`, `setMetaFields`, and `setSensitiveMetaFields`, plus the subscription methods `addEventCallback`, `removeEventCallbacks`, `addSdkEventCallback`, and `removeSdkEventCallback`. It also forwards the [curated bridge requests](#curated-bridge-requests), `removeEventCallback`, and the `zdChatterAuthCallback` property.
`open` and `close` do not show or dismiss the dialog. Use `show()` and `dismiss()` for that.

The dialog does not expose these members:

* `clearPersistedStateDurably`
* `webViewErrorCallback`
* `webViewLoadingErrorCallback`
* `zdChatterAuthAsyncCallback`

If you need them, use `AdaMessagingView` or `AdaMessagingActivity` with `getAdaView()`.

Obtain the dialog instance from the fragment manager:

**`Kotlin`**

```kotlin Kotlin
val adaDialog = supportFragmentManager
    .findFragmentByTag(AdaMessagingDialog.TAG) as AdaMessagingDialog
adaDialog.setMetaFields(
    AdaMessagingView.MetaFields.Builder()
        .setField("name", "John"),
)
adaDialog.reset()
```

You can register event callbacks on the dialog before it is shown. The dialog buffers them and attaches them when its view is created. The dialog drops commands before `onViewCreated` and after `onDestroyView`. `show()` is asynchronous. Curated requests return `Failure("Ada view is not attached")` when the view is absent.

## Session persistence

From version 1.2.0, session state persists across an app force-quit. Session state includes the end-user token and any active live agent handoff context. If an end user force-quits the app during a conversation or a live agent handoff, the conversation continues when they reopen the app.

The web runtime stores session state in the WebView's `localStorage`, as before. From version 1.2.0, the SDK also stores a session mirror in app-private `SharedPreferences`. On Android, `localStorage` is not guaranteed to survive a force-quit. When that happens, the SDK restores the session state from the session mirror.

The session mirror is app-private, and the SDK stores the values in plain form. The SDK never persists identity tokens to disk.

The SDK ships backup-exclusion rules as the resources `@xml/ada_messaging_backup_rules` and `@xml/ada_messaging_data_extraction_rules`. The SDK sets no `android:fullBackupContent` or `android:dataExtractionRules` attribute for them. The manifest merger treats a library value for those attributes as a conflict with an app that declares its own. That conflict fails the app build.

Your app must wire the exclusion itself. If your app declares no backup rules, reference the SDK rule resources on your `<application>` element:

**`XML`**

```xml XML
<application
    android:fullBackupContent="@xml/ada_messaging_backup_rules"
    android:dataExtractionRules="@xml/ada_messaging_data_extraction_rules">
```

If your app has its own rule files, add the same exclusion to them instead:

**`XML`**

```xml XML
<!-- fullBackupContent (pre-API 31) -->
<exclude domain="sharedpref" path="cx.ada.messaging.session-mirror.xml" />

<!-- dataExtractionRules (API 31+), in both <cloud-backup> and <device-transfer> -->
<exclude domain="sharedpref" path="cx.ada.messaging.session-mirror.xml" />
```

This step excludes the session mirror from Android Auto Backup and device transfer. Without this step, a device backup can copy session state to the cloud and restore it on another device.

Session persistence follows your AI Agent's persistence setting:

* The `normal` persistence mode writes the session mirror. The SDK stores session state natively.
* The `session` and `private` persistence modes clear the session mirror. The SDK writes no session mirror in these modes.

If you supply an [`identityToken`](#identitytoken) at initialization, the token takes precedence. The SDK performs the identity exchange first, and the resolved session replaces any previously persisted session state. To authenticate a new user after sign-out, call `initialize` again with new settings that carry a fresh token.

> **Warning**
>
> **When an end user signs out, clear the live conversation with [`reset`](#reset) or [`deleteHistory`](#deletehistory).** Clear native state through `AdaMessagingView` when needed independently. [`clearPersistedState`](#clearpersistedstate) alone does not clear the live conversation. Run durable clearing off the main thread. Sessions now survive app restarts. If you skip this step, the next user of the device can resume the previous user's conversation.

The native side also caches a small allowlist of non-sensitive branding state, with a 10 minute expiry, to smooth the next launch. [`clearPersistedState`](#clearpersistedstate) removes the branding cache, the session mirror, and the camera photos the file chooser saved.

## Types

### AdaEnvironment

Sealed class that selects the Ada deployment environment. Pass it to [`environment`](#environment).

| Value                                 | Description                                                                                                                         |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `AdaEnvironment.Custom(assetsOrigin)` | Loads the Messaging runtime from your CDN origin, such as `https://my-cdn.example.com`. Use it only when your Ada team directs you. |
| `AdaEnvironment.Production`           | Ada's production environment. The default.                                                                                          |
| `AdaEnvironment.Preprod(branch)`      | Ada's pre-production environment, for validation the Ada team directs.                                                              |
| `AdaEnvironment.Local(port, host)`    | A local development server, for SDK development.                                                                                    |

### AdaWebSdk

Enum that selects the web runtime. Pass it to [`webSdk`](#websdk).

| Value                 | Description                             |
| --------------------- | --------------------------------------- |
| `AdaWebSdk.Legacy`    | The existing Chat runtime. The default. |
| `AdaWebSdk.Messaging` | The next-generation Messaging runtime.  |

### AdaWebViewError

Sealed class delivered to [`webViewErrorCallback`](#webviewerrorcallback).

| Case                                      | Description                                                                                     |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `BridgeError(error)`                      | The native bridge reported an error before the runtime became ready.                            |
| `HttpError(url, statusCode)`              | The main document responded with an HTTP error status.                                          |
| `LoadFailed(url, errorCode, description)` | The main document failed to load. `url` is `null` when the SDK could not build the WebView URL. |
| `Timeout(timeoutMillis)`                  | The page did not finish loading within [`loadTimeoutMillis`](#loadtimeoutmillis).               |

### Event

Data class delivered to [`addEventCallback`](#addeventcallback) subscribers.

| Field               | Type          | Description                                                                                                                                               |
| ------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chatterTranscript` | `String?`     | The conversation transcript, when the event carries one.                                                                                                  |
| `eventName`         | `String?`     | The event key, for example `ada:message:received`.                                                                                                        |
| `userData`          | `JSONObject?` | Contains the payload's `user_data` object, when present. Use [`addSdkEventCallback`](#addsdkeventcallback) for complete Messaging and lifecycle payloads. |

### MetaFields.Builder

Builder for [`metaFields`](#metafields) and [`sensitiveMetaFields`](#sensitivemetafields) values. `setField(key, value)` accepts `String`, `Boolean`, `Int`, `Long`, `Float`, and `Double` values.

**`Kotlin`**

```kotlin Kotlin
AdaMessagingView.MetaFields.Builder()
    .setField("name", "John")
    .setField("age", 20)
    .setField("authorized", true)
```