Developer guide

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.

The POST /v2/end-users/POST /v2/conversations/ flow described in this guide is for custom channel (Conversations API) integrations only. For native chat, use the Chat SDK setMetaFields() and setSensitiveMetaFields() instead.

Channel eligibility

FeatureAvailable channels
Pre-greeting context (POST /v2/end-users/POST /v2/conversations/)Custom channels only
Secure metadata on POST /v2/end-users/Custom channels only
Secure metadata on PATCH /v2/end-users/:idAll channels (endpoint is channel-agnostic)
  • Native chat: The Chat SDK provides 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 or v1.conversation.created webhook event.

Before you begin

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.

1

Create the end user with language and metadata

$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:

1{
2 "end_user_id": "67a8b2c1d3f4e5a6b7c8d9e0",
3 "profile": {
4 "first_name": "Maria",
5 "last_name": "Santos",
6 "language": "pt-BR",
7 "metadata": {
8 "region": "latam",
9 "plan_type": "enterprise"
10 },
11 ...
12 },
13 "created_at": "2026-03-31T10:30:00.000Z",
14 "updated_at": "2026-03-31T10:30:00.000Z"
15}
2

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.

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

1

Create the end user with sensitive metadata

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

2

Start a conversation

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

3

Refresh the token mid-conversation (if needed)

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

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

1

Create the end user with campaign metadata

$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"
> }
> }
> }'
2

Start a conversation

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

external_id is available for custom channel (Conversations API) integrations only. Native chat, social, and voice channels are out of scope.
1

Look up the end user by external ID

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

2

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:

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

3

Start a conversation

Use the end_user_id from the lookup or create response:

$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"
> }'
4

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.

$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

metadatasensitive_metadata
EncryptionNot encryptedEncrypted at rest
Dashboard visibilityVisibleRedacted
LLM contextIncludedExcluded
TTLPersists until overwrittenAutomatically deleted after 24 hours
API responsesReturned in GET and PATCH responsesNever returned (write-only)
Use casesRegion, plan type, preferencesAuth 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

No. The POST /v2/end-users/POST /v2/conversations/ flow is for custom channel integrations only. For native chat, use the Chat SDK setMetaFields() to set user context before the conversation starts.

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

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 native chat, the Chat SDK setSensitiveMetaFields() is the primary path, but the API also works. For social and email channels, the API is the only pathway.

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.

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

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.

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.

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.

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

1{
2 "profile": {
3 "sensitive_metadata": {
4 "fields": {
5 "auth_token": null
6 }
7 }
8 }
9}

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.

1{
2 "profile": {},
3 "external_id": null
4}

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.