> 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

Use the React Native SDK [settings](#settings), [actions](#actions), and [events](#events) to customize the behavior of your AI Agent.

## Settings

`AdaMessagingView` takes the following props. Only `handle` is required.

### appScheme

Available from version 1.3.0.

`appScheme?: string;`

The custom URL scheme for your app. A link whose scheme equals this value is a deep link into your app. The SDK hands it to the operating system instead of loading it in the WebView.

On iOS the SDK hands only a user-initiated top-level link to the operating system. It refuses a script-driven or subframe link, so a subframe cannot start a deep link on its own. On Android the SDK hands any matching link to the operating system, because Android reports no user-initiation signal.

### appUrl

`appUrl?: string;`

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.

Use 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 development, expose your server through an HTTPS tunnel. Add the tunnel origin to **Allowed websites**.
The emulator or simulator reaches the tunnel origin directly. Port forwarding is not required.

The SDK injects the value into the WebView document through an injected configuration object, never through a URL. Unlike [`identityToken`](#identitytoken), which the runtime reads once, the value is delivered again on every document load. Changing the value reloads the WebView. See [Custom apps](/messaging/custom-apps/overview). Requires the Messaging runtime ([`webSdk="messaging"`](#websdk)).

### cluster

`cluster?: string;`

Specifies the region your AI Agent runs on. Pass either a short region name (for example, `maple` or `us2`) or a full cluster domain. Short names resolve to `<name>.ada.support`. Defaults to `ada.support`.

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

### deviceToken

`deviceToken?: string;`

Pass the device token at startup. See [`setDeviceToken`](#setdevicetoken) for storage and push notification guidance.

| Runtime   | Device token registration                                                                                         |
| --------- | ----------------------------------------------------------------------------------------------------------------- |
| Legacy    | At initialization, before the first message.                                                                      |
| Messaging | Once the runtime is ready. The React Native SDK excludes it from the initialization payload. Android includes it. |

The device token never appears in a URL.

Set the prop once before mounting. After the runtime is ready, call [`setDeviceToken`](#setdevicetoken) on the ref to set or rotate the device token.

### domain

`domain?: string;`

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

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

### embedVersion

`embedVersion?: string;`

Pins the remote legacy host page to a specific legacy web build for pre-release verification. Rendered as the `?__ada-embed-version=<sha>` query parameter. Leave unset for the stable rollout. Ignored when `webSdk` is not `"legacy"`.

### enableProgrammaticControl

Available from version 1.3.0.

`enableProgrammaticControl?: boolean;`

Runs the Messaging runtime with programmatic control enabled. Defaults to `true` on React Native for backward compatibility, because the pre-prop React Native behavior was unconditional. This is not parity with the native SDKs.

Programmatic control is required for [`selectQuickReply`](#selectquickreply), [`sendMessage`](#sendmessage), the two [curated bridge requests](#curated-bridge-requests) that read messages, and the gated `ada:message:sent` / `ada:message:received` events. The two message reads are `getConversation()` and `getMessages()`. `triggerPlaybook()` also requires it. The other curated bridge requests do not require it. Set `false` to opt out. Then `getConversation()` and `getMessages()` reject. `sendMessage` returns nothing. The SDK reports its failure through [`onError`](#onerror) and [`onWebViewError`](#onwebviewerror) with `kind: "bridgeError"`. The SDK does not deliver the gated events. iOS and Android default it to `false`. It applies to the Messaging runtime only and is never sent on the legacy runtime.

### endConversationCallback

`endConversationCallback?: (event: unknown) => void;`

Called when a conversation ends.

> **Note**
>
> This prop is deprecated. Use [`onEvent`](#onevent) and check for the `ada:end_conversation` key instead.

### environment

```ts
environment?:
  | { type: "production" }
  | { type: "custom"; assetsOrigin: string };
```

Selects which Ada web-hosting environment the WebView loads and the matching origin allowlists. When provided, `environment` takes precedence over `cluster` for asset resolution. Leave unset for production. Use a custom asset origin only when your Ada team directs you.

### eventCallbacks

`eventCallbacks?: Record<string, (event: unknown) => void>;`

Dictionary of event callbacks keyed by event name. Supports `"*"` as a wildcard to receive all events.

> **Note**
>
> This prop is deprecated. Use [`onEvent`](#onevent) instead for a unified event callback.

### greeting

`greeting?: string;`

Specifies a greeting response ID to trigger on load. This is useful for setting view-specific greetings across your app.

### handle

`handle: string;`

The handle for your AI Agent. This is a required field.

### headless

`headless?: boolean;`

When set to `true`, the SDK runs the Ada runtime without rendering its chat UI. The WebView stays mounted but hidden and non-interactive: it renders as an absolute-positioned, transparent 1×1 view with pointer events and accessibility disabled. `onEvent`, `onStateCache`, and every [action](#actions) keep working, so you can drive a fully native UI, such as an unread badge fed by message events.

Unmounting the component ends the live session. Pass the last `onStateCache` snapshot back as `initialState` to rehydrate on the next mount.

> **Note**
>
> Headless sessions run with no visual indicator, and a session starts as soon as the view mounts. Mount a headless view only when you intend to start or resume a session.

Headless requires the Messaging runtime ([`webSdk="messaging"`](#websdk)). The remote legacy host page ignores it. See [Headless mode](/messaging/react-native/getting-started#headless-mode) for a complete example.

### identityToken

`identityToken?: string;`

Short-lived, single-use identity token minted by your backend via `POST /v2/auth/tokens/`. Use it to start the session as a known user.

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 SDK injects the token into the WebView document before content loads. The token is never placed in a URL. The runtime reads the injected value once per document load and deletes it immediately. Blank or whitespace-only values are treated as absent.

Provide the token before mounting the view. A value set after mount only applies when the WebView reloads, and a reload needs a freshly minted token because tokens are single-use. After the runtime becomes ready, the SDK never delivers a consumed token again on later reloads. Set a newly minted token to authenticate again. Requires the Messaging runtime ([`webSdk="messaging"`](#websdk)).

If the exchange fails, [`onEvent`](#onevent) receives `ada:identity_token:error`. Inspect `reason` and correct the cause. Use a freshly minted token for the next exchange.

### initialState

`initialState?: Record<string, unknown> | null;`

Previously cached state snapshot to inject before the page loads. When provided, the chat UI renders immediately without a loading state. Capture snapshots with [`onStateCache`](#onstatecache).

The SDK filters the value to the allowlisted keys in `PERSISTABLE_STATE_KEYS` before injection, and discards the whole snapshot when its `__ada_cached_at__` timestamp is older than 10 minutes. See [State persistence exports](#state-persistence-exports).

> **Warning**
>
> Snapshots persisted by earlier package versions were unfiltered, and can contain session credentials. Run stored snapshots through `toPersistableStateSnapshot()` before you pass them here, and overwrite the stored copy with the filtered result. See [Session lifecycle and state restoration](/messaging/react-native/getting-started#session-lifecycle-and-state-restoration).

### language

`language?: string;`

Sets the initial display language. To use the same language for AI Agent replies, first turn on languages in your Ada dashboard.

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).

### loadTimeoutMs

`loadTimeoutMs?: number;`

The maximum time in milliseconds the initial WebView load may take. Defaults to `30000`, matching the iOS `webViewTimeout` and Android `loadTimeoutMillis` defaults. When the timeout elapses first, the component emits the `ada.webview.loadFailed` event and calls [`onError`](#onerror). Pass `0` or a negative value to disable the timeout.

> **Note**
>
> Earlier package versions had no load timeout. A silently stalled load now fails after 30 seconds by default.

### metaFields

`metaFields?: Record<string, string | number | boolean | null>;`

Use `metaFields` to pass information about an end user to Ada on initialization. This can be useful for tracking information about your end users, as well as personalizing their experience.

**`TSX`**

```tsx TSX
metaFields={{
  name: "Some name",
  age: 30
}}
```

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

### onError

`onError?: (error: string) => void;`

Called when an error occurs inside the WebView, including load failures and runtime errors reported by the SDK.

### onEvent

`onEvent?: (key: string, data: unknown) => void;`

Called whenever the SDK publishes an event, and for WebView lifecycle events. Subscribe to specific keys for analytics or routing. See [Events](#events) for the available keys.

**`TSX`**

```tsx TSX
onEvent={(key, data) => {
  if (key === "ada:end_conversation") {
    console.log("Conversation ended", data);
  }
}}
```

### onReady

`onReady?: () => void;`

Called when the SDK has finished initializing and is ready to accept commands. Use it to send sensitive metadata and device tokens through the ref.

### onStateCache

`onStateCache?: (state: Record<string, unknown>) => void;`

Called when the runtime sends a state cache snapshot. Store the snapshot and pass it back as [`initialState`](#initialstate) on the next mount to remove the reload spinner after the WebView restarts.

Before your callback runs, the SDK reduces the snapshot to the allowlisted keys in `PERSISTABLE_STATE_KEYS` and stamps it with `__ada_cached_at__`. Session credentials and conversation content never reach your code, and the delivered snapshot is safe to persist as is. It matches what the iOS and Android SDKs cache natively. See [State persistence exports](#state-persistence-exports).

### onWebViewError

Available from version 1.3.0.

`onWebViewError?: (error: AdaWebViewError) => void;`

The typed variant of [`onError`](#onerror). It runs alongside `onError` for WebView load and bridge failures. It carries an `AdaWebViewError` so you can tell one failure from another. This matches the Android and iOS typed error callbacks.

`AdaWebViewError` is a discriminated union with four `kind` values:

* `loadFailed`: the main document failed to load. It carries `url`, `code`, and `description`.
* `httpError`: the main document returned an HTTP error status. It carries `url`, `statusCode`, and `description`.
* `timeout`: the page did not load within the configured load timeout. It carries `timeoutMs` and `description`.
* `bridgeError`: the runtime bridge reported an error. It carries `description`.

### openWebLinksInSafari

Available from version 1.3.0.

`openWebLinksInSafari?: boolean;`

Whether the SDK hands a user-initiated top-level web link to the system browser. This applies to an http or https link to a non-Ada origin. It defaults to `true`, which preserves the established React Native behavior.

Such a link cannot become the WebView main document without destroying the runtime bridge, so the SDK opens it externally. Set `false` to suppress the handoff. The SDK then blocks the link instead of opening it. Ada runtime documents and customer-hosted subframes always stay in place.

On Android the SDK always ejects a foreign top-level document and stops its load. This prop still controls whether the SDK then hands the link to the system browser. On iOS this prop controls the same handoff. iOS gates it additionally on `navigationType`.

When Android navigation replaces the trusted main document, the SDK reports both error callbacks and attempts up to three automatic recoveries. Subsequent losses report `recovering: false`. Remount the component to recover.

### sensitiveMetaFields

`sensitiveMetaFields?: Record<string, string | number | boolean | null>;`

Sensitive metadata to pass to the AI Agent. This works like [`metaFields`](#metafields), but the values are not stored in the database, and are deleted after 24 hours.

From version 1.3.0, the SDK forwards these fields at initialization. An identified session carries them from the first message, matching iOS and Android. 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.

Set the prop once before mounting for the init-time path. Call [`setSensitiveMetaFields`](#setsensitivemetafields) on the ref for explicit control after the runtime is ready, or to change the fields later.

### sessionStorageAdapter

```ts
sessionStorageAdapter?: {
  getItem(key: string): Promise<string | null>;
  setItem(key: string, value: string): Promise<void>;
  removeItem(key: string): Promise<void>;
};
```

Available from version 1.2.0. Durable key-value storage that your app owns. When you set this prop, the SDK persists session state to the storage. On the next mount, the SDK reads the storage again when the chat runtime asks for the session state. Session state, including active live agent handoff context, then survives an app force-quit. See [Session persistence](#session-persistence).

Without the prop, the SDK behaves as in earlier versions: session state does not survive a force-quit. Development builds log a one-time warning when the runtime sends session state and no adapter is set.

The package exports the interface as `AdaSessionMirrorStorage` and adds no storage dependency. The session state contains session tokens. Use a store that stays on the device that wrote it.

> **Warning**
>
> On Android, Auto Backup and device transfer copy the adapter's storage onto a different device. Exclude the adapter's storage from Auto Backup and device transfer. On iOS, use a Keychain-backed adapter with a `ThisDeviceOnly` accessibility class. The Keychain protects the session token at rest. See [Session persistence](#session-persistence).

`react-native-keychain` stores values on the device only. Set the `ThisDeviceOnly` accessibility class so the values stay off backups.

Keychain items survive an app uninstall on iOS. A reinstall can therefore restore the previous install's session. If your app must not do that, wipe the adapter's `ada-session-mirror:`-prefixed keys on first run, or use a backup-excluded file store instead.

**`TSX`**

```tsx TSX
import * as Keychain from "react-native-keychain";

const adapter = {
  getItem: async (key: string) => {
    const result = await Keychain.getGenericPassword({ service: key });
    return result ? result.password : null;
  },
  setItem: async (key: string, value: string) => {
    await Keychain.setGenericPassword("ada", value, {
      service: key,
      accessible: Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
    });
  },
  removeItem: async (key: string) => {
    await Keychain.resetGenericPassword({ service: key });
  },
};

<AdaMessagingView handle="my-company" sessionStorageAdapter={adapter} />
```

`react-native-mmkv` is another option. Its API is synchronous, so wrap it in a thin async adapter. On iOS, exclude the store file from backup.

**`TSX`**

```tsx TSX
import { MMKV } from "react-native-mmkv";

const storage = new MMKV({ id: "ada-session" });

const adapter = {
  getItem: async (key: string) => storage.getString(key) ?? null,
  setItem: async (key: string, value: string) => storage.set(key, value),
  removeItem: async (key: string) => storage.delete(key),
};
```

`@react-native-async-storage/async-storage` also satisfies the interface. Protect the adapter's storage on each platform.

On Android, exclude the adapter's storage from Auto Backup and device transfer. Set `fullBackupContent` and `dataExtractionRules` in your app manifest. Otherwise, use a store that Android excludes from backup by default. Without an exclusion, a backup or a device transfer copies the session tokens to another device.

On iOS, AsyncStorage marks its own directory `NSURLIsExcludedFromBackupKey`, so it stays out of iCloud and encrypted backups. AsyncStorage still holds the session token in plaintext in the app container. Use a Keychain-backed adapter with a `ThisDeviceOnly` accessibility class to protect the token at rest.

The SDK catches adapter errors. A throwing adapter degrades to no persistence and never breaks chat. The SDK stores values under keys with the `ada-session-mirror:` prefix. Do not write to those keys yourself. The prop applies to both the Messaging runtime and the legacy runtime.

> **Warning**
>
> If an end user signs out of your app, call [`reset`](#reset), [`deleteHistory`](#deletehistory), or [`clearPersistedState`](#clearpersistedstate). See [Session persistence](#session-persistence).

### style

`style?: StyleProp<ViewStyle>;`

Additional style overrides for the container view. When [`headless`](#headless) is set, the hidden styling is applied after `style` and always wins.

### styles

`styles?: string | Record<string, string>;`

Style overrides for the web runtime. The accepted shape depends on the runtime:

* **Messaging runtime**: no effect. The runtime ignores the value and logs a warning.
* **Legacy runtime**: pass a CSS style override string.

Empty values are omitted.

### thirdPartyCookiesEnabled

`thirdPartyCookiesEnabled?: boolean;`

Enables third-party cookies in the WebView. Android only.

> **Note**
>
> This prop is deprecated. Cookies are managed by the shared cookie jar, and the SDK sets sensible defaults. Normally omit it.

### version

`version?: string;`

Pins the remote legacy host page to a specific legacy chat build for pre-release verification. Rendered as the `?__ada-chat-version=<sha>` query parameter. Leave unset for the stable rollout. Ignored when `webSdk` is not `"legacy"`.

### webSdk

`webSdk?: "messaging" | "legacy";`

Selects which web runtime boots inside the WebView. Defaults to `"legacy"` so package upgrades do not change end-user behavior before you intentionally cut over. Set `"messaging"` to run the Messaging runtime.

These features require the Messaging runtime:

* The [`appUrl`](#appurl), [`headless`](#headless), and [`identityToken`](#identitytoken) settings.
* The [`sendMessage`](#sendmessage) action.
* The [curated bridge requests](#curated-bridge-requests).

### zdChatterAuthCallback

`zdChatterAuthCallback?: (callback: (token: string) => void) => void;`

Use `zdChatterAuthCallback` to authenticate Zendesk Chat and Zendesk Messaging Handoffs.
For Zendesk Messaging, select `webSdk="messaging"`. Request a JWT from your server and pass it to the callback.

**`TSX`**

```tsx TSX
zdChatterAuthCallback={(callback) => {
  const token = getTokenFromAPI(); // Get a fresh JWT token from your API
  callback(token);
}}
```

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 `adaRef.current?.reset()` so the next user starts a new conversation and a new Zendesk user.

## Actions

Call actions on the `AdaMessagingView` ref. This lets you control the AI Agent without re-rendering the component. Commands sent before the runtime is ready are queued, then flushed automatically when it becomes ready.

**`TSX`**

```tsx TSX
import { useRef } from "react";
import {
  AdaMessagingView,
  type AdaMessagingViewHandle,
} from "@ada-cx/messaging-react-native";

function MyComponent() {
  const adaRef = useRef<AdaMessagingViewHandle>(null);

  return <AdaMessagingView ref={adaRef} handle="my-company" webSdk="messaging" />;
}
```

### clearPersistedState

`clearPersistedState(): Promise<void>;`

Available from version 1.2.0. Removes every key that [`sessionStorageAdapter`](#sessionstorageadapter) recorded, across all handles and all scopes. The call is not limited to the mounted handle. The live WebView session continues unchanged. Call it when an end user signs out of your app. See [Session persistence](#session-persistence).

**`TSX`**

```tsx TSX
await adaRef.current?.clearPersistedState();
```

The returned promise rejects when:

* Any adapter operation fails, including registry reads, index reads, deletes, read-backs, or registry rewrites.
* An adapter call takes longer than 3 seconds.
* The component unmounts before the wipe completes.
* An earlier adapter call never settled, so the SDK blocks later calls.
* An adapter supplied earlier is no longer available.
* A removed key still returns a value.
* The SDK cannot confirm the wipe because an outstanding storage operation could still write data.

A rejected wipe can be partial. Await the call and handle the rejection. Wait for outstanding storage operations to settle before retrying.

The promise resolves with no effect only when the component never received an adapter.

The SDK tracks the keys it writes, because the adapter interface cannot list keys. An earlier app run that used a different adapter leaves keys the SDK cannot track. To remove those, delete every key in your store that starts with `ada-session-mirror:`.

### deleteHistory

`deleteHistory(): void;`

Deletes the conversation history and resets the session. If [`sessionStorageAdapter`](#sessionstorageadapter) is set, the SDK also removes the persisted session state.

**`TSX`**

```tsx TSX
adaRef.current?.deleteHistory();
```

### reset

`reset(opts?): void;`

Starts a new session and refreshes the chat. `reset` can take an optional object that changes `greeting`, `language`, `metaFields`, and `sensitiveMetaFields` for the new session. Pass `resetChatHistory: false` to keep the existing history. If [`sessionStorageAdapter`](#sessionstorageadapter) is set, the SDK also removes the persisted session state.

**`TSX`**

```tsx TSX
adaRef.current?.reset();

// With options
adaRef.current?.reset({
  language: "fr",
  metaFields: { plan: "pro" },
  resetChatHistory: true,
});
```

### selectQuickReply

`selectQuickReply(messageId: string, index: number): void;`

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/react-native/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`     | `number` | 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.
`onEvent` receives the message at `data.message`.
Quick-reply messages have `id`, `type: "quick_replies"`, and `data.quickReplies`.

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

**`TSX`**

```tsx TSX
<AdaMessagingView
  ref={adaRef}
  handle="my-agent"
  webSdk="messaging"
  enableProgrammaticControl
  onEvent={(key, data) => {
    if (key !== "ada:message:received") return;
    const { message } = data as {
      message: {
        id: string;
        type: string;
        data?: { quickReplies?: unknown[] };
      };
    };
    if (message.type === "quick_replies" && message.data?.quickReplies?.length) {
      adaRef.current?.selectQuickReply(message.id, 0);
    }
  }}
/>
```

### sendMessage

`sendMessage(body: string): void;`

Sends a message into the conversation on behalf of the end user. Pair it with [`headless`](#headless) and [`onEvent`](#onevent) to drive a fully native chat UI.

**`TSX`**

```tsx TSX
adaRef.current?.sendMessage("Where is my order?");
```

> **Note**
>
> `sendMessage` requires the Messaging runtime ([`webSdk="messaging"`](#websdk)). The legacy runtime does not support this action.

### setDeviceToken

`setDeviceToken(token: string): void;`

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).

Call `setDeviceToken` in `onReady`.

**`TSX`**

```tsx TSX
adaRef.current?.setDeviceToken("push-token");
```

### setLanguage

`setLanguage(language: string): void;`

Changes the display language at runtime without resetting the session. Language codes use the lowercase, two-letter [ISO 639-1 language format](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).

**`TSX`**

```tsx TSX
adaRef.current?.setLanguage("fr");
```

### setMetaFields

`setMetaFields(fields: Record<string, string | number | boolean | null>): void;`

Updates [metaFields](#metafields) without resetting the session. This is useful if you need to update end-user data after the Agent has already launched.

**`TSX`**

```tsx TSX
adaRef.current?.setMetaFields({
  name: "Some name",
  age: 30,
});
```

### setSensitiveMetaFields

`setSensitiveMetaFields(fields: Record<string, string | number | boolean | null>): void;`

Updates sensitive metadata without resetting the session. This works like [setMetaFields](#setmetafields), but provides an added layer of security. Call it in `onReady`.

**`TSX`**

```tsx TSX
adaRef.current?.setSensitiveMetaFields({
  token: "your_jwt_token",
});
```

### triggerPlaybook

**`TypeScript`**

```typescript TypeScript
triggerPlaybook(
  playbookId: string,
  options?: {
    metaFields?: Record<string, string | number | boolean | null>;
    sensitiveMetaFields?: Record<string, string | number | boolean | null>;
  }
): Promise<void>;
```

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 promise resolves when the API accepts the trigger.
It rejects on refusal, invalid input, metadata write failure, or timeout.
The error includes the reason, such as `triggerPlaybook rejected: playbook_running`.

**`TSX`**

```tsx TSX
await adaRef.current?.triggerPlaybook("507f1f77bcf86cd799439011", {
  metaFields: { orderId: "order-123" },
  sensitiveMetaFields: { accountToken: "<account-token>" },
});
```

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

Available from version 1.3.0, with `triggerPlaybook` added in 1.5.0. These methods require `webSdk="messaging"`. On the Legacy runtime, every method rejects as unsupported.

The ref exposes a fixed set of methods that ask the running chat for state or drive it. Each returns a `Promise` that resolves with the requested value, or rejects. A method rejects in any of four cases:

* The running runtime build does not implement it.
* 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: one that only some runtimes support rejects with an explicit unsupported signal.

Reads return public models only. No read returns sensitive meta fields, the internal stores, or persisted session data. Writes and triggers resolve with no value.

Call these after the runtime is ready. Call them in [`onReady`](#onready). `triggerPlaybook()` and the message reads `getConversation()` and `getMessages()` require [`enableProgrammaticControl`](#enableprogrammaticcontrol), which defaults to `true`. The other reads, writes, and triggers do not. The default differs per platform. iOS and Android default it to `false`. On those platforms the same two reads reject until the host enables the flag.

| Method                                  | Returns                            | Description                                                             |
| --------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------- |
| `close()`                               | `Promise` (rejects as unsupported) | The SDK cannot close a parent-hosted webview.                           |
| `closeCampaign()`                       | `Promise<void>`                    | Dismisses the active campaign.                                          |
| `getConversation()`                     | `Promise<unknown>`                 | The public conversation model.                                          |
| `getInfo()`                             | `Promise<unknown>`                 | The public AI Agent and session info.                                   |
| `getMessages()`                         | `Promise<unknown>`                 | The public message list.                                                |
| `getMetaFields()`                       | `Promise<Record<string, unknown>>` | The non-sensitive meta fields set on the session.                       |
| `isOpen()`                              | `Promise<boolean>`                 | Whether the chat window is open.                                        |
| `open()`                                | `Promise` (rejects as unsupported) | The SDK cannot open a parent-hosted webview.                            |
| `setTheme(theme)`                       | `Promise<void>`                    | Sets the runtime theme to `"light"`, `"dark"`, or `"auto"`.             |
| `toggle()`                              | `Promise` (rejects as unsupported) | The SDK cannot toggle a parent-hosted webview.                          |
| `triggerAnswer(answerId)`               | `Promise<void>`                    | Triggers a specific answer by id.                                       |
| `triggerGreeting(handle?)`              | `Promise<void>`                    | Triggers the greeting, optionally for a specific handle.                |
| `triggerPlaybook(playbookId, options?)` | `Promise<void>`                    | Starts a Playbook. Early Access. Requires programmatic control.         |
| `triggerProactive(messageKey, params?)` | `Promise<void>`                    | Shows a proactive message by key, with optional variable substitutions. |

**`TSX`**

```tsx TSX
const conversation = await adaRef.current?.getConversation();
await adaRef.current?.setTheme("dark");
```

## Events

Subscribe to events through the [`onEvent`](#onevent) prop. The callback receives the event key and an event-specific data payload.

### SDK events

With the Messaging runtime, `onEvent` receives every event the SDK publishes. Common keys include:

| Key                          | Description                                                                                                                                                                                                                                                               |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ada:agent:joined`           | A human agent joined the conversation.                                                                                                                                                                                                                                    |
| `ada:agent:left`             | A human agent left the conversation.                                                                                                                                                                                                                                      |
| `ada:campaigns:dismissed`    | The end user dismissed a proactive message.                                                                                                                                                                                                                               |
| `ada:campaigns:engaged`      | The end user engaged with a proactive campaign.                                                                                                                                                                                                                           |
| `ada:campaigns:opened`       | The chat opened after a proactive message appeared.                                                                                                                                                                                                                       |
| `ada:campaigns:shown`        | A proactive message appeared.                                                                                                                                                                                                                                             |
| `ada:close_chat`             | The end user explicitly closed the chat with the close control, or confirmed ending the conversation. It is not a signal that the chat UI is gone: after an End Chat confirm the UI stays on screen for any survey, and the event fires even when the chat could not end. |
| `ada:connection:change`      | The realtime connection state changed.                                                                                                                                                                                                                                    |
| `ada:conversation:change`    | The active conversation changed.                                                                                                                                                                                                                                          |
| `ada:conversation:message`   | A message was added to the conversation. The payload does not include the message body.                                                                                                                                                                                   |
| `ada:csat_submitted`         | The end user submitted a satisfaction survey.                                                                                                                                                                                                                             |
| `ada:end_conversation`       | The conversation ended.                                                                                                                                                                                                                                                   |
| `ada:identity_token:error`   | The identity token exchange failed. The payload contains a `reason` string. Inspect `reason` and correct the cause. Use a freshly minted token for the next exchange.                                                                                                     |
| `ada:identity_token:expired` | The identity token expired before the exchange. The SDK also reports `ada:identity_token:error`.                                                                                                                                                                          |
| `ada:message:received`       | The AI Agent or a human agent sent a message. The payload includes the message body.                                                                                                                                                                                      |
| `ada:message:sent`           | The end user sent a message. The payload includes the message body.                                                                                                                                                                                                       |
| `ada:typing:start`           | A typing indicator started.                                                                                                                                                                                                                                               |
| `ada:typing:stop`            | A typing indicator stopped.                                                                                                                                                                                                                                               |

`onEvent` also receives custom events from your AI Agent under their configured event names.

> **Warning**
>
> `ada:message:sent` and `ada:message:received` carry full message bodies, including any personal information end users type. Do not forward these payloads to analytics or logging tools unless your product and security teams have accepted that exposure.

> **Note**
>
> `ada:message:sent` and `ada:message:received` are delivered only when [`enableProgrammaticControl`](#enableprogrammaticcontrol) is on. The React Native SDK defaults this prop to `true`. The native app is the programmatic driver. The iOS and Android SDKs default it to `false`. Set it to `false` to opt the app out of these gated events on React Native.

### WebView lifecycle events

The component also reports its own lifecycle and the session mirror through `onEvent`:

| Key                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ada.webview.contentProcessTerminated` | The WebView content process ends after two recoveries in a row. The count resets after the page stays loaded for 60 s. Reported once per terminal state, even if the load already failed. The payload includes only `url`, when known.                                                                                                                                                                                                                                                                                                                 |
| `ada.webview.loaded`                   | The WebView finished its initial load. The payload includes the loaded `url`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `ada.webview.loadFailed`               | The initial load failed, including when [`loadTimeoutMs`](#loadtimeoutms) elapsed first. The payload includes `url`, `code`, `statusCode`, and `error`. Also reported through `onError`.                                                                                                                                                                                                                                                                                                                                                               |
| `ada.webview.subresourceLoadFailed`    | A runtime subresource failed to load after the initial load. Also reported through `onError`.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `ada.webview.mainDocumentLost`         | The main frame left the trusted Ada document, so the chat bridge stopped working. The payload includes `url` and `recovering`. `recovering` is `true` when the SDK reloads the trusted document. Also reported through `onError` and `onWebViewError`. See [Android recovery limits](#openweblinksinsafari).                                                                                                                                                                                                                                           |
| `ada.sessionMirror.diagnostic`         | The SDK could not 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-quarantined`, `adapter-removeItem-failed`, `adapter-setItem-failed`, `adapter-unavailable`, `entries-not-allowlisted`, `guard-origin-unavailable`, `legacy-rollout-unresolved`, `nonce-mismatch`, `origin-mismatch`, `scope-mismatch`. |

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

## Session persistence

The SDK stores session state inside the WebView's storage. On Android, that storage does not survive a force-quit. On iOS, the OS can also discard it.

From version 1.2.0, pass [`sessionStorageAdapter`](#sessionstorageadapter) to persist session state in storage that your app owns. On the next launch, the chat runtime asks the SDK for the persisted session state, and the SDK reads your adapter to answer. Session state, including active live agent handoff context, then survives an app force-quit. Without the adapter, the SDK does not persist session state, and it behaves as in earlier versions.

Session persistence follows these rules:

* Session state persists only under the `normal` persistence mode. The `session` and `private` persistence modes clear the session state, so the SDK writes no session state to the adapter in these modes.
* If you supply an [`identityToken`](#identitytoken) when a session starts or resets, the identity exchange runs first. The resolved session replaces any persisted session state.
* The SDK never persists identity tokens to disk.
* [`reset`](#reset) and [`deleteHistory`](#deletehistory) remove the persisted session state. [`clearPersistedState`](#clearpersistedstate) removes it without a session reset.
* The SDK shows no prompts or permission dialogs to end users, and it needs no new app permissions.
* The persisted session state contains session tokens, so it must stay on the device that wrote it. On Android, exclude the adapter's storage from Auto Backup and device transfer. Otherwise a backup can copy a session to another device.
* On iOS, AsyncStorage keeps its own directory out of iCloud and encrypted backups, so a backup does not copy the session. AsyncStorage still stores the token in plaintext in the app container. See [`sessionStorageAdapter`](#sessionstorageadapter) for the Keychain-backed alternative.

The [`onStateCache`](#onstatecache) snapshot is a separate mechanism. It carries branding state only, and it removes the loading state after a restart. It never contains session state.

> **Warning**
>
> **Call `reset()` when an end user signs out of your app.** Because session state survives app restarts, the next user of the device can otherwise resume the previous user's conversation. Call [`reset`](#reset), [`deleteHistory`](#deletehistory), or [`clearPersistedState`](#clearpersistedstate) in your sign-out flow.

## State persistence exports

The package exports the pieces of the state persistence contract described in [Session lifecycle and state restoration](/messaging/react-native/getting-started#session-lifecycle-and-state-restoration).

### PERSISTABLE_STATE_KEYS

`PERSISTABLE_STATE_KEYS: readonly string[]`

The allowlist of snapshot keys the SDK keeps: `advancedColorsEnabled`, `allowedProtocols`, `button`, `chatEnabled`, `fallbackUi`, `features`, `intro`, `proactiveConversations`, `textOverAccentColor`, `tintColor`, and `__ada_cached_at__`. It matches the allowlist the iOS and Android SDKs persist natively.

### STATE_CACHE_TTL_MS

`STATE_CACHE_TTL_MS = 600000`

The snapshot expiry in milliseconds (10 minutes). An [`initialState`](#initialstate) snapshot with an older `__ada_cached_at__` stamp is discarded instead of injected.

### toPersistableStateSnapshot

`toPersistableStateSnapshot(state: Record<string, unknown>): Record<string, unknown>`

Reduces a snapshot to the allowlisted keys, and stamps `__ada_cached_at__` when the stamp is missing. [`onStateCache`](#onstatecache) already applies this filter. Use it to sanitize snapshots that earlier package versions persisted unfiltered, before you pass them as [`initialState`](#initialstate).