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

# Getting started

This guide mints an identity token from your backend and wires it into each Messaging SDK. Read the [identity tokens overview](/messaging/identity/overview) first for the flow and the security model.

## Before you start

* You need an Ada [API key](/reference/introduction/authentication) for your backend. The key must never reach a browser or mobile app.
* You need the Ada `end_user_id` of the person to identify. Create end users, or look them up by your own `external_id`, with the [End Users API](/reference/end-users/overview).

## Optional handoff details

Only your server can send `verified_email` and `name` when it mints an identity token.
Use an email address whose ownership your system has verified.

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

Verified email linking needs an `external_id` on the end user.
Create or look up the end user with an `external_id` through the End Users API.
Without an `external_id`, Zendesk Messaging handoffs ignore the Ada identity token, `verified_email`, and `name`.

When the mint request includes `verified_email`, Zendesk Messaging handoffs use it to link to or create your Zendesk end user.
Ada uses this order:

1. Use the Zendesk end user with the same `external_id` as the Platform API identity mapping.
2. Otherwise, link exactly one Zendesk end user with a matching primary email.
3. If no match exists, create a Zendesk end user with the name and verified email.

Ada uses `name` from the mint request first, then the end user's profile `first_name` and `last_name`.
Ada trims the profile names and joins them with a space.
See [Authenticate end users](/docs/handoffs/zendesk/zendesk-messaging#authenticate-end-users) for the full matching rules.

Ada carries the values encrypted in the token.

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.

Session refresh does not extend this window.
Without `verified_email` or `name`, mint, exchange, and handoff keep their existing behavior.

If Sunshine initialization fails, Ada keeps the values for a retry.
A new exchange replaces the previous linking values, including an exchange on another device.
Expired sessions cannot supply these values.

When the exchanged token has decrypted claims, Ada updates `jwt_email` and `jwt_name` for display.
A missing or empty value clears its previous display copy only if that copy still matches the value Ada wrote.
Ada updates `name` only when it is empty or still equals the previous token's name. It never writes `email` from these claims.
When Ada clears the linking values, including when it detects expiry, it also clears display copies that still match the values Ada wrote.
Without claims, exchange clears the previous claims and display copies that still match the values Ada wrote.
It does not use the profile name for display.
These display copies do not supply identity.

For validation, name fallback, and dashboard behavior, see [Verified handoff details](/messaging/identity/overview#verified-handoff-details).

## Step 1: Mint a token from your backend

Call [`POST /v2/auth/tokens/`](/reference/auth-tokens/overview) with the end user's id. Authenticate with your API key as a `Bearer` token:

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/auth/tokens/ \
  --header 'Authorization: Bearer {YOUR_API_TOKEN}' \
  --header 'Content-Type: application/json' \
  --data '{
    "end_user_id": "66b1a2c3d4e5f6a7b8c9d0e1",
    "verified_email": "Ada.Lovelace@example.com",
    "name": "Ada Lovelace"
  }'
```

A successful response returns `201` with the token:

```json
{
  "token": "eyJhbGciOi...",
  "type": "identity",
  "expires_in": 900
}
```

`expires_in` is in seconds. The token expires 15 minutes after minting and can be exchanged exactly once.

| Status | Meaning                                                                                       |
| ------ | --------------------------------------------------------------------------------------------- |
| `201`  | Token minted.                                                                                 |
| `400`  | `end_user_id` is invalid, or `verified_email` or `name` fails validation. Ada stores nothing. |
| `404`  | No end user exists with that id.                                                              |
| `503`  | Token minting is not available for this AI Agent. Contact your Ada team.                      |

Mint on demand, when your client is about to start or reset a chat session. Good mint points are sign-in and page load, always on your backend. Do not mint tokens in advance or cache them. Requests are rate limited per AI Agent; the limits are sized for one mint per end user opening chat. The [Auth Tokens API reference](/reference/auth-tokens/overview) documents the endpoint schema, error codes, and rate limits.

## Step 2: Pass the token to the Web SDK

Deliver the token to your page over HTTPS, then pass it as `identityToken`:

```ts
import { loadAdaMessaging } from "@ada-cx/messaging-sdk";

const token = await fetchAdaIdentityToken(); // your backend call

const ada = await loadAdaMessaging({
  handle: "<your-handle>",
  identityToken: token,
});
```

The same setting works with `adaEmbed.start()` on the [module-script path](/messaging/web/getting-started#embed-with-a-module-script), and on `window.adaSettings` for pages that still configure through the global.

The SDK exchanges the token when the end user engages with the chat, before it restores any saved conversation state. An end user who never opens the chat never triggers the exchange.

## Step 3: Handle exchange failures

Subscribe to the identity events on your host page:

```ts
await ada.subscribeEvent("ada:identity_token:error", (data) => {
  // The exchange failed. The session continues anonymously.
});

await ada.subscribeEvent("ada:identity_token:expired", (data) => {
  // The server rejected the token and its expiry had elapsed.
  // Mint a fresh token and pass it through reset() to retry.
});
```

A failed exchange does not block the conversation. The session falls back to the normal anonymous path, so the end user can still chat.

> **Note**
>
> Every exchange attempt consumes the token, even when the attempt fails afterward. To retry identification, mint a fresh token. Never reuse one.

## Step 4: Re-identify or switch users

Pass a new token to `reset()` when the signed-in user changes, or to retry after an expired token:

```ts
const token = await fetchAdaIdentityToken();
await ada.reset({ identityToken: token });
```

Rules:

* A full `reset()` without a replacement token discards any token that has not finished exchanging. The old identified session cannot be re-adopted.
* `reset({ resetChatHistory: false })` preserves the current session, and with it any pending token.
* When the end user signs out of your product, call `reset()` so the next session starts anonymously.

## Mobile SDKs

Each mobile SDK accepts the token at initialization and requires the Messaging runtime. The SDK injects the token into the WebView before any page script runs. The token never appears in a URL, and the runtime deletes it from the page after one read. A remount needs a freshly minted token.

### iOS

**`Swift`**

```swift Swift
let adaWebHost = AdaWebHost(
    handle: "<your-handle>",
    eventCallbacks: [
        "ada:identity_token:error": { event in /* continue anonymously */ },
        "ada:identity_token:expired": { event in /* mint a fresh token, then reset */ },
    ],
    webSdk: .messaging,
    identityToken: token
)
```

Set `identityToken` at initialization. See the [iOS SDK reference](/messaging/ios/sdk-api-reference#identitytoken) for details.

### Android

**`Kotlin`**

```kotlin Kotlin
val settings = AdaMessagingView.Settings.Builder("<your-handle>")
    .webSdk(AdaWebSdk.Messaging)
    .identityToken(token)
    .build()
adaView.initialize(settings)

adaView.addEventCallback("ada:identity_token:error") { event -> /* continue anonymously */ }
```

See the [Android SDK reference](/messaging/android/sdk-api-reference#identitytoken) for details.

### React Native

**`TSX`**

```tsx TSX
const token = await fetchAdaIdentityToken(); // your backend call

<AdaMessagingView
  handle="<your-handle>"
  webSdk="messaging"
  identityToken={token}
  onEvent={(key) => {
    if (key === "ada:identity_token:error") {
      // continue anonymously, or remount with a fresh token
    }
  }}
/>
```

Fetch the token before you mount the view. A token set after mount only applies on the next document load. See the [React Native guide](/messaging/react-native/getting-started#secure-identity-tokens) for details.

## Identify one user across two devices

One identity gives one conversation on every device and browser. This walkthrough signs the same end user in on a laptop browser and on a phone app.

### Device 1: the laptop browser

1. The end user signs in to your site. Your backend verifies them with your normal logic.
2. Your backend mints a token for their `end_user_id`, as in [Step 1](#step-1-mint-a-token-from-your-backend), and delivers it to the page.
3. The page passes the token as `identityToken`:

```ts
// Minted by your backend during page load
const tokenA = await fetchAdaIdentityToken();

const ada = await loadAdaMessaging({
  handle: "<your-handle>",
  identityToken: tokenA,
});
```

When the end user opens the chat, the SDK exchanges the token and the identified conversation starts. Suppose they send "Where is my order?" and then close the laptop.

### Device 2: the phone app

1. The end user opens your mobile app and signs in. Your backend verifies them again.
2. Your backend mints a second token for the same `end_user_id`. The first token is spent, so this device needs its own mint.
3. The app passes the new token at initialization. React Native is shown here. iOS uses the [`identityToken` initializer parameter](/messaging/ios/sdk-api-reference#identitytoken), and Android uses [`Settings.Builder.identityToken`](/messaging/android/sdk-api-reference#identitytoken).

**`TSX`**

```tsx TSX
// A separate token, minted by your backend at app sign-in
const tokenB = await fetchAdaIdentityToken();

<AdaMessagingView
  handle="<your-handle>"
  webSdk="messaging"
  identityToken={tokenB}
/>
```

The exchange resolves to the same end user, and the phone restores the same conversation before the chat opens. "Where is my order?" and the AI Agent's reply are already in the transcript. While both devices stay open, live updates flow to both over the same realtime channel. The phone's exchange does not sign the laptop out; both hold live identified sessions.

> **Warning**
>
> Never send the laptop's token to the phone. Tokens are single use, so the server rejects a replayed token and that session falls back to anonymous. Mint a fresh token on your backend for every device, at sign-in or page-load time.

This flow is verified end-to-end against a live environment on every change: two devices, two freshly minted tokens, one shared conversation.

## Security checklist

* Mint only from your backend, with your API key kept server-side.
* Deliver tokens to clients over HTTPS only.
* Never place a token in a URL, query parameter, or deep link.
* Never log tokens on the server or the client.
* Mint a fresh token per attempt. Tokens are single use and expire in 15 minutes.