Recognise one user across a channel and your app
By the end of this tutorial you will have one end user who is the same actor on WhatsApp and in your own web chat — proven by their WhatsApp conversation reading back your actor's id.
Four steps:
- Create the actor for your user.
- Open your web chat as that actor.
- Link the WhatsApp number.
- Prove it.
Every call is shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.
Prerequisites
-
A credential. A
nat_sk_…API key exported asNATURALI_TOKEN, and your client set up — the CLI, the SDK or plaincurl:export NATURALI_TOKEN=nat_sk_...export NATURALI_API=https://api.naturali.ai/v1 # curl examples only -
A channel answered by an agent, with the project and agent ids exported. Connecting WhatsApp by link connects a number; Connect a Discord channel works the same way with a Discord DM.
export PROJECT=proj_V1StGXR8Z5jdHi6Bexport AGENT=agent_V1StGXR8Z5jdHi6Bexport NATURALI_PROJECT_ID=$PROJECT # lets the CLI omit --project-id -
A webhook subscribed to
message.received, pointing at your backend — created through the API, or declared beside the channel as awebhookin a formation.
1. Create the actor for your user
Use your own user id as external_id. Tool calls in this actor's sessions carry
it as X-Naturali-Context-actor_external_id, set from the actor rather than
from anything a caller sends, so your tools know which user is talking.
- CLI
- SDK
- curl
naturali create-actor --name Ana --external-id user_42
const { data: actor } = await naturali.actors.createActor({
path: { project_id: process.env.PROJECT! },
body: { name: 'Ana', external_id: 'user_42' },
});
curl -X POST "$NATURALI_API/projects/$PROJECT/actors" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Ana", "external_id": "user_42" }'
{ "id": "actor_V1StGXR8Z5jdHi6B", "name": "Ana", "external_id": "user_42" }
export ACTOR=actor_V1StGXR8Z5jdHi6B
2. Open your web chat as that actor
Your web chat is a session that names the actor.
- CLI
- SDK
- curl
naturali create-session --agent-id "$AGENT" --actor-id "$ACTOR" --auto-generate true
const { data: session } = await naturali.sessions.createSession({
path: { project_id: process.env.PROJECT! },
body: {
agent_id: process.env.AGENT!,
actor_id: process.env.ACTOR!,
auto_generate: true,
},
});
curl -X POST "$NATURALI_API/projects/$PROJECT/sessions" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{ \"agent_id\": \"$AGENT\", \"actor_id\": \"$ACTOR\", \"auto_generate\": true }"
{ "id": "sess_V1StGXR8Z5jdHi6B", "agent_id": "agent_V1StGXR8Z5jdHi6B", "actor_id": "actor_V1StGXR8Z5jdHi6B" }
3. Link the WhatsApp number
Show the signed-in user a link that sends a one-time code from their phone,
such as https://wa.me/5511900000000?text=LINK-4F7K2Q. When your webhook
receives a message.received whose content is a code you issued, bind the
address it came from — the event's identifier, whatsapp:dm:5511999998888
— to that user's actor.
The code is the only proof the number belongs to the user. Make it single-use and short-lived, and bind it to the user who asked for it.
- CLI
- SDK
- curl
naturali set-address \
--identifier whatsapp:dm:5511999998888 \
--actor-id "$ACTOR"
await naturali.channels.setAddress({
path: {
project_id: process.env.PROJECT!,
identifier: 'whatsapp:dm:5511999998888',
},
body: { actor_id: process.env.ACTOR! },
});
curl -X PUT "$NATURALI_API/projects/$PROJECT/addresses/whatsapp%3Adm%3A5511999998888" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{ \"actor_id\": \"$ACTOR\" }"
{ "identifier": "whatsapp:dm:5511999998888", "actor_id": "actor_V1StGXR8Z5jdHi6B", "action": null }
The conversation the code arrived on is closed, since a session's actor is fixed; the next message opens one as your actor.
4. Prove it
Send any message from the phone, then read the address's conversations.
- CLI
- SDK
- curl
naturali list-address-conversations --identifier whatsapp:dm:5511999998888
const { data: page } = await naturali.channels.listAddressConversations({
path: {
project_id: process.env.PROJECT!,
identifier: 'whatsapp:dm:5511999998888',
},
});
curl "$NATURALI_API/projects/$PROJECT/addresses/whatsapp%3Adm%3A5511999998888/conversations" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"data": [
{
"identifier": "whatsapp:dm:5511999998888",
"actor_id": "actor_V1StGXR8Z5jdHi6B",
"session_id": "sess_8kQ2mZp4Rt7Lx1Vb"
}
],
"next_cursor": null
}
actor_id is the actor your web chat uses: one user, two transports.
What's next
- Cap spend per end user — one quota now covers both transports.
- Addresses — rebinding, releasing and erasure.