Actors
Who the agent is talking to — the end user on the other side of a session, keyed by whatever your application already calls them.
Overview
An actor is the identity a generation is attributed to. It carries the persona instructions composed into that turn's prompt and, when you ask for one, a memory container that follows the actor from session to session.
An actor is keyed by your own external_id: a user id, an account number, a
hashed email — whatever your system already uses.
POST /v1/projects/{project_id}/actors is
idempotent on that key, so the first thing your backend does when a new user
appears can be this call. No read first, and a retry returns the actor you
already have rather than a second one.
Pass the id you get back as actor_id when you open a
session, and every turn in that session is attributed to that
person.
Actors are for traffic that arrives through this API. A conversation that arrives through a channel resolves to an address instead, and the address mints and owns the actor it speaks as. You can read those actors here, but you erase one through its address — see Two ways in, one identity.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Actors.
Data Model
Actor
| Field | Type | Description |
|---|---|---|
id | string, nullable | Public actor ID. |
project_id | string, nullable | The owning project. |
external_id | string, nullable | Your own key for this person. |
name | string, nullable | A human-friendly name, for reading a transcript later. |
instructions | string, nullable | Persona instructions composed into the agent's prompt for this actor's turns. |
has_memory | boolean | Whether a memory container is linked, carrying what the agent remembers across sessions. |
created_at | string (date-time), nullable | |
updated_at | string (date-time), nullable |
Key Concepts
external_id is the idempotency key
Creating an actor converges on external_id: the first call creates one and
answers 201, and every later call with the same key returns that same actor
with 200. You never have to check first, and a retried request cannot
produce a duplicate.
That also means you never need to store naturali's id if you would rather
not — GET /v1/projects/{project_id}/actors?external_id=…
resolves your own key to an actor without creating anything.
external_id is not updatable. It is the value your references converge on,
so moving it would orphan every id you hold while freeing the old key for
someone else.
Two ways in, one identity
Both entry points end at the same kind of identity, but each owns the actors it creates:
| Channel traffic | This API | |
|---|---|---|
| Identity resource | Address | Actor |
| Keyed by | The channel identifier (whatsapp:dm:<phone>) | Your external_id |
| Actor minted by | The address, lazily on first message | POST …/actors |
| Erased through | DELETE /v1/projects/{project_id}/addresses/{identifier} | DELETE /v1/projects/{project_id}/actors/{actor_id} |
Two rules keep them from colliding. An external_id may not start with a
channel prefix (whatsapp:, discord:, …) or address: — those name actors
that belong to an address, and claiming one would put a single identity under
two owners. And
DELETE /v1/projects/{project_id}/actors/{actor_id}
answers 409 for an actor an address owns: erase it through the address, which
removes its conversations too.
Reads are not restricted either way. Resolving the actor_id you see on a
channel conversation is exactly what
GET /v1/projects/{project_id}/actors/{actor_id}
is for.
One human can still be two actors
Someone who talks to your agent on WhatsApp and in your app has two identities: one the address minted, one you created. Nothing joins them, so memory does not carry across and erasing one does not erase the other.
That is deliberate. Without a graph saying two identities are the same person, naturali does not know that they are and does not claim to — the same reason address erasure is a promise about one address rather than about a human everywhere.
Erasure is per identity
DELETE /v1/projects/{project_id}/actors/{actor_id}
removes the actor and the sessions it holds. It is the erasure path for an end
user who reached you through this API, and it is narrower than "erase this
human everywhere" for the reason above.
Examples
Identify a user the first time your backend sees them — safe to call on every sign-in, not just the first:
- CLI
- SDK
- curl
naturali create-actor \
--project-id proj_V1StGXR8Z5jdHi6B \
--external-id user_42 \
--name Ana
const { data: actor } = await naturali.actors.createActor({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: { external_id: 'user_42', name: 'Ana' },
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/actors \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "external_id": "user_42", "name": "Ana" }'
Give the actor a persona and a memory that follows it across sessions:
- CLI
- SDK
- curl
naturali create-actor \
--project-id proj_V1StGXR8Z5jdHi6B \
--external-id user_42 \
--name Ana \
--instructions 'Speaks Portuguese. Prefers short answers.' \
--memory
const { data: actor } = await naturali.actors.createActor({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
external_id: 'user_42',
name: 'Ana',
instructions: 'Speaks Portuguese. Prefers short answers.',
memory: true,
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/actors \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "user_42",
"name": "Ana",
"instructions": "Speaks Portuguese. Prefers short answers.",
"memory": true
}'
Attribute a session to the actor, so every turn is recorded against that person:
- CLI
- SDK
- curl
naturali create-session \
--project-id proj_V1StGXR8Z5jdHi6B \
--agent-id agent_V1StGXR8Z5jdHi6B \
--actor-id actor_V1StGXR8Z5jdHi6B
const { data: session } = await naturali.sessions.createSession({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
agent_id: 'agent_V1StGXR8Z5jdHi6B',
},
body: { actor_id: 'actor_V1StGXR8Z5jdHi6B' },
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/agents/agent_V1StGXR8Z5jdHi6B/sessions \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "actor_id": "actor_V1StGXR8Z5jdHi6B" }'
Resolve your own key to an actor without creating one:
- CLI
- SDK
- curl
naturali list-actors \
--project-id proj_V1StGXR8Z5jdHi6B \
--external-id user_42
const { data: page } = await naturali.actors.listActors({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { external_id: 'user_42' },
});
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/actors?external_id=user_42" \
-H "Authorization: Bearer $NATURALI_API_KEY"
Erase an actor and the sessions it holds:
- CLI
- SDK
- curl
naturali delete-actor \
--project-id proj_V1StGXR8Z5jdHi6B \
--actor-id actor_V1StGXR8Z5jdHi6B
await naturali.actors.deleteActor({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
actor_id: 'actor_V1StGXR8Z5jdHi6B',
},
});
curl -X DELETE https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/actors/actor_V1StGXR8Z5jdHi6B \
-H "Authorization: Bearer $NATURALI_API_KEY"