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

# Developer guide

For [Messaging identity tokens](/messaging/identity/getting-started), your backend creates an end user or looks one up by `external_id`. Pass the resulting identity token to the SDK; the conversation creation flow below remains specific to custom channels.

This guide shows how to use the End Users API alongside the Conversations API to build context-aware, authenticated custom channel integrations. It covers setting user context before the first AI Agent turn, passing sensitive metadata securely, and common integration patterns.

> **Info**
>
> The `POST /v2/end-users/` → `POST /v2/conversations/` flow described in this guide is for **custom channel (Conversations API) integrations only**. For legacy Chat, use the Chat SDK [`setMetaFields()`](/chat/web/sdk-api-reference#setmetafields) and [`setSensitiveMetaFields()`](/chat/web/sdk-api-reference#setsensitivemetafields) instead.

## Channel eligibility

| Feature                                                                  | Available channels                          |
| ------------------------------------------------------------------------ | ------------------------------------------- |
| Pre-greeting context (`POST /v2/end-users/` → `POST /v2/conversations/`) | Custom channels only                        |
| Secure metadata on `POST /v2/end-users/`                                 | Backend end-user creation                   |
| Secure metadata on `PATCH /v2/end-users/:id`                             | All channels (endpoint is channel-agnostic) |

* **Native chat**: The Chat SDK provides [`setSensitiveMetaFields()`](/chat/web/sdk-api-reference#setsensitivemetafields) as the primary path. `PATCH /v2/end-users/:id` with `sensitive_metadata` is also available if you have the `end_user_id`.
* **Social and email channels**: `PATCH /v2/end-users/:id` with `sensitive_metadata` is the only API pathway. Obtain the `end_user_id` from a [`v1.end_user.created`](/reference/end-users/end-user-created-webhook) or [`v1.conversation.created`](/reference/conversations/conversation-created-webhook) webhook event.

## Before you begin

* An [API token](/reference/introduction/authentication)
* A [custom channel](/reference/conversations/create-channel) configured in the Ada dashboard
* A [webhook endpoint](/reference/webhooks/overview) (optional, for receiving events)

## Multi-language support from the first turn

This example creates an end user with a language preference and metadata before starting a conversation. The AI Agent receives the correct language and context from the greeting onward.

#### Create the end user with language and metadata

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/end-users/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": {
      "first_name": "Maria",
      "last_name": "Santos",
      "language": "pt-BR",
      "metadata": {
        "region": "latam",
        "plan_type": "enterprise"
      }
    }
  }'
```

The response includes the `end_user_id`:

```json
{
  "end_user_id": "67a8b2c1d3f4e5a6b7c8d9e0",
  "profile": {
    "first_name": "Maria",
    "last_name": "Santos",
    "language": "pt-BR",
    "metadata": {
      "region": "latam",
      "plan_type": "enterprise"
    },
    ...
  },
  "created_at": "2026-03-31T10:30:00.000Z",
  "updated_at": "2026-03-31T10:30:00.000Z"
}
```

#### Start a conversation with the end user

Pass the `end_user_id` from the previous step. The AI Agent associates the conversation with this end user, and the greeting fires with the correct language, locale-aware knowledge base lookups, and personalized content.

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/conversations/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "YOUR_CHANNEL_ID",
    "end_user_id": "67a8b2c1d3f4e5a6b7c8d9e0"
  }'
```

The AI Agent now has full user context from the first turn. Language is set to `pt-BR`, knowledge base lookups use the correct locale, and metadata values like `region` and `plan_type` are available as metavariables in Playbooks, Actions, and article rules.

## Authenticated Actions without re-login

This example passes an auth token securely at user creation time so the AI Agent can run authenticated Actions (such as account lookups or order changes) without prompting the end user to log in again.

#### Create the end user with sensitive metadata

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/end-users/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": {
      "language": "en-US",
      "metadata": {
        "account_tier": "premium"
      },
      "sensitive_metadata": {
        "fields": {
          "auth_token": "eyJhbGciOiJIUzI1NiIs...",
          "session_id": "sess_abc123"
        }
      }
    }
  }'
```

The `sensitive_metadata` values are encrypted at rest, redacted from the dashboard, excluded from LLM context, and automatically deleted after 24 hours. They do not appear in the response body.

#### Start a conversation

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/conversations/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "YOUR_CHANNEL_ID",
    "end_user_id": "END_USER_ID_FROM_STEP_1"
  }'
```

The AI Agent can now use the `auth_token` to execute authenticated Actions on behalf of the end user.

MCP tools that act as the end user can use it too, when the value is a token the MCP server accepts for that end user. On the MCP server connection, enter the field name (`auth_token` in this example) under **Customer token field name**, and the tool runs without the authorization step in the conversation. See [Authentication and channels](/docs/automation/tools/mcp-tools/authentication-and-channels).

#### Refresh the token mid-conversation (if needed)

If the token expires during the conversation, update it with a PATCH request:

```bash
curl -X PATCH https://EXAMPLE.ada.support/api/v2/end-users/END_USER_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": {
      "sensitive_metadata": {
        "fields": {
          "auth_token": "eyJhbGciOiJSUzI1NiIs..."
        }
      }
    }
  }'
```

The updated token is immediately available to the AI Agent for subsequent Actions.

## Campaign-driven personalization

This example sets campaign context on an end user so the AI Agent can personalize the greeting and route the conversation based on the campaign that brought the user in.

#### Create the end user with campaign metadata

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/end-users/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": {
      "metadata": {
        "campaign_id": "spring_2026_promo",
        "offer_code": "SAVE20",
        "source_channel": "email"
      }
    }
  }'
```

#### Start a conversation

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/conversations/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "YOUR_CHANNEL_ID",
    "end_user_id": "END_USER_ID_FROM_STEP_1"
  }'
```

The metavariables `campaign_id`, `offer_code`, and `source_channel` are available from the greeting onward. Use them in Playbooks to route to a campaign-specific flow, in article rules to surface relevant content, or in Actions to apply the offer.

## Identify end users with a stable external ID

This example uses the `external_id` field to reference end users by an identifier from another system (for example a CRM contact ID) across conversations. The same identifier can be used for lookup, create, and update.

> **Info**
>
> The conversation creation example below applies to custom channels. For Messaging, use the end-user lookup with [identity tokens](/messaging/identity/getting-started) and start the session through the SDK.

#### Look up the end user by external ID

```bash
curl -X GET "https://EXAMPLE.ada.support/api/v2/end-users/?external_id=user-12345" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

A `200` response returns the existing end user with `end_user_id` and profile data already populated. A `404` response means no end user is mapped to that `external_id` yet.

#### Create the end user if no mapping exists

If the lookup returned `404`, create the end user and set `external_id` in the same request:

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/end-users/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "user-12345",
    "profile": {
      "language": "en-US",
      "metadata": {
        "plan_type": "enterprise"
      }
    }
  }'
```

`POST /v2/end-users/` is idempotent when `external_id` is supplied: if the value is already mapped, the existing end user is returned with `200`; otherwise a new end user is returned with `201`. This makes the call safe to retry.

#### Start a conversation

Use the `end_user_id` from the lookup or create response:

```bash
curl -X POST https://EXAMPLE.ada.support/api/v2/conversations/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "YOUR_CHANNEL_ID",
    "end_user_id": "END_USER_ID_FROM_STEP_1_OR_2"
  }'
```

#### Update or clear the external_id (optional)

Set a new `external_id` or clear an existing one with `PATCH /v2/end-users/{end_user_id}`. Passing `null` removes the mapping and makes the value available for reuse.

```bash
curl -X PATCH https://EXAMPLE.ada.support/api/v2/end-users/END_USER_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": {},
    "external_id": null
  }'
```

`profile` is required on `PATCH /v2/end-users/{end_user_id}` requests; pass an empty object when you only need to set or clear `external_id`.

If the new `external_id` is already assigned to a different end user on the same AI Agent, `PATCH` returns `409 Conflict`.

### Validation rules

* Maximum length: 36 characters.
* The `<` and `>` characters are not allowed.
* Values are stored and matched case-insensitively (`ABC-123` and `abc-123` resolve to the same end user).
* `external_id` is unique per AI Agent — one value maps to exactly one end user.
* Empty values, values longer than 36 characters, and values containing `<` or `>` are rejected with `400 Bad Request` on `GET`, `POST`, and `PATCH`.

## Security considerations

### `sensitive_metadata` vs. `metadata`

|                          | `metadata`                          | `sensitive_metadata`                               |
| ------------------------ | ----------------------------------- | -------------------------------------------------- |
| **Encryption**           | Not encrypted                       | Encrypted at rest                                  |
| **Dashboard visibility** | Visible                             | Redacted                                           |
| **LLM context**          | Included                            | Excluded                                           |
| **TTL**                  | Persists until overwritten          | Automatically deleted after 24 hours               |
| **API responses**        | Returned in GET and PATCH responses | Never returned (write-only)                        |
| **Use cases**            | Region, plan type, preferences      | Auth tokens, session IDs, PII (name, phone, email) |

Use `sensitive_metadata` for any value that should not persist long-term or be visible to operators and LLMs. Use standard `metadata` for non-sensitive context that should be readable and persistent.

### End-user scoped storage

`sensitive_metadata` values are stored at the end-user level, not the conversation level. If an end user has multiple active conversations, a value set in one conversation is accessible in all of them.

For the most common integration pattern (one end user per conversation), this is not observable. For integrations where a single end user has concurrent conversations across channels, be aware that sensitive values propagate to all active conversations for that end user.

### Write-only behavior

`sensitive_metadata` values are never returned in API responses. If your integration needs to reference a token it previously set, store it on your side. Ada stores the value only for use by the AI Agent during the conversation.

## FAQ

#### Can I use pre-greeting context with native chat?

No. The `POST /v2/end-users/` → `POST /v2/conversations/` flow is for custom channel integrations only. For legacy Chat, use the Chat SDK [`setMetaFields()`](/chat/web/sdk-api-reference#setmetafields) to set user context before the conversation starts.

#### Can I use pre-greeting context with social channels like WhatsApp?

Only if your WhatsApp integration uses the Conversations API (making it a custom channel). Social channels managed through Ada's native integrations (such as SunCo-based WhatsApp) do not support this flow because the end user initiates the conversation and there is no opportunity to call `POST /v2/conversations/`.

#### Can I use secure metadata on native chat, social, or email channels?

Yes. `PATCH /v2/end-users/:id` with `sensitive_metadata` is channel-agnostic and works for any end user as long as you have the `end_user_id`. For legacy Chat, the Chat SDK [`setSensitiveMetaFields()`](/chat/web/sdk-api-reference#setsensitivemetafields) is the primary path, but the API also works. For social and email channels, the API is the only pathway.

#### What happens if I put sensitive values in standard metadata instead of sensitive_metadata?

Values in standard `metadata` are stored unencrypted, visible in the dashboard, included in LLM context, and returned in API responses. Always use `sensitive_metadata` for auth tokens, session IDs, and personally identifiable information.

#### What happens to end users created with POST that never start a conversation?

End users created through `POST /v2/end-users/` that are not associated with a conversation within 24 hours of creation are automatically deleted. If your integration encounters an error between creating the end user and starting a conversation, reuse the same `end_user_id` for the retry rather than creating a new one. End users are not billed (billing is per conversation, not per end user).

#### Is POST /v2/end-users/ idempotent?

Only when `external_id` is supplied. A `POST` with an `external_id` that is already mapped returns the existing end user with `200`; a new user returns `201`. Without `external_id`, each `POST` creates a new end user — store and reuse the returned `end_user_id` to avoid duplicates.

#### What happens if I omit the language field on POST?

If no `language` is provided in the profile, the end user's language defaults to the AI Agent's configured default language (typically English). If you explicitly provide a language value, that value is used.

#### Does end-user metadata carry over across conversations?

End-user metadata (set via `profile.metadata`) persists across conversations for the same end user. Conversation metadata (set on the conversation object) does not carry over. If you need fresh metadata for each conversation, update the end user via `PATCH /v2/end-users/:id` before starting a new conversation.

#### How do I remove a sensitive metavariable?

Set the key to `null` in a `PATCH /v2/end-users/:id` request:

```json
{
  "profile": {
    "sensitive_metadata": {
      "fields": {
        "auth_token": null
      }
    }
  }
}
```

#### How do I clear an external_id?

Send a `PATCH /v2/end-users/{end_user_id}` request with `external_id` set to `null`. `profile` is required on PATCH requests, so include an empty `profile` object when you only need to clear the `external_id`. This removes the mapping and frees the value to be assigned to a new end user.

```json
{
  "profile": {},
  "external_id": null
}
```

#### What happens to an external_id if the end user is auto-deleted after 24 hours?

End users created via `POST /v2/end-users/` that are not associated with a conversation within 24 hours are automatically deleted. When this happens, the `external_id` mapping is removed along with the end user, so the value is reusable in a new `POST /v2/end-users/` request.