Roxels/ docs
concepts

Identity and resumption

A Roxels participant isn't anonymous. When you tell us who they are — even by a string only you can interpret — we can pick up where they left off, accumulate context across visits, and deliver outputs that reference them in your system.

This page consolidates the identity story. It's scattered across the embed, the conversations API, and the persons API; here it is in one place.

The two identifiers you'll see

Every participant Roxels knows about has two identifiers:

  • external_id — A string you provide. Stable across visits. Your user id, your tenant id, your email — whatever you key users on internally.
  • person.id — A string Roxels provides. Stable across visits. Returned by the API.

You can use either to look someone up in our API, but for everything client-side, use external_id. It's the value you already have; we never expose person.id to the browser.

Where you pass it

external_id flows through every entry point:

// Embed init (script tag autoinit)
<script
  src="https://app.roxels.ai/embed.js"
  data-template-key="rk-..."
  data-external-id="user_123"
></script>
 
// Embed init (manual)
Roxels.init({ templateKey: "rk-...", externalId: "user_123" });
 
// Programmatic start
Roxels.start({ templateKey: "rk-...", externalId: "user_123" });
 
// Set after init (e.g. on sign-in)
Roxels.setPerson({ name: "Ada Lovelace", externalId: "user_123" });
 
// Server-side conversation creation
POST /v1/interviews
{
  "template_id": "tpl_...",
  "persons": [{ "external_id": "user_123", "name": "Ada Lovelace" }]
}

In all cases, Roxels looks up an existing person by external_id in your org. If found, the new conversation attaches to them. If not, a new person is created.

What it gets you

Cross-conversation context

The person record accumulates. The third conversation a user has knows about the second and the first — the agent can reference what was discussed previously, the captured data carries forward, the user feels recognized.

Session resumption

If the template has resumption enabled (a setting in the template's embed config), the participant's next visit picks up where they left off instead of starting a new conversation. Same transcript, same captured data, same goal state, mid-sentence if necessary.

This is the right default for long-running conversations the user might want to come back to (onboarding flows that take multiple sessions, support cases that need a follow-up).

Output identification

Webhooks and frontend callbacks include external_id in the payload. Your downstream systems can match the conversation back to your user without any extra mapping.

Audit and observability

In the dashboard, conversations are grouped by person. You can see "all conversations Ada has had" in one place.

When you don't pass external_id

If you don't pass an external_id, Roxels creates an anonymous person for the session. The conversation runs normally. But:

  • No resumption — the next visit starts fresh.
  • No accumulation — no cross-conversation context.
  • Outputs include a system-generated person id, not yours.

For one-shot anonymous conversations (a public demo, a marketing survey), this is fine. For anything where the participant might come back, pass external_id.

Trust model

The browser is not a trusted environment. Anyone with the embed key can pass any external_id they want. So:

  • external_id identifies, but it does not authenticate. If your system needs to know who someone is in a trusted way, don't rely on the value the browser sent.
  • For sensitive context, create the session server-side. Use the Conversations API to create the session with verified context, then pass session_id (not external_id) to the embed. The user can't tamper with context you put on the session server-side.

The right rule of thumb: external_id is fine for personalization and resumption. It's not fine as a sole authentication mechanism for actions the agent will take on behalf of the user.

Server-side identity verification

When the participant must be authenticated:

[Your backend]                              [Browser]              [Roxels]
     │                                          │                      │
     │  user is signed in                       │                      │
     │ ────────────────────────────────────────► │                      │
     │                                          │                      │
     │  POST /v1/interviews                     │                      │
     │    persons: [{ external_id, name }]      │                      │
     │    context: { plan, role, … }            │                      │
     │ ────────────────────────────────────────────────────────────────►│
     │                                          │                      │
     │  ◄────────────────────────────────────────────────────────────── │
     │  { session_id }                          │                      │
     │                                          │                      │
     │  pass session_id to the page              │                      │
     │ ────────────────────────────────────────► │                      │
     │                                          │                      │
     │                                          │  Roxels.start({       │
     │                                          │    sessionId,         │
     │                                          │    display: …         │
     │                                          │  })                   │
     │                                          │ ────────────────────► │

The user's identity, plan, role — anything sensitive — is set on the session by your backend. The browser receives only the session_id. The agent has access to everything; the user can't change any of it.

This is the pattern for support flows, account-management conversations, anything where the agent acts in the user's name.

Multiple users in one browser

If the same browser is used by different participants (a shared tablet, a kiosk, a household device), make sure external_id is set correctly for each:

// User signs in
Roxels.setPerson({ name: "Ada", externalId: "user_ada" });
 
// User signs out, different user signs in
Roxels.setPerson({ name: "Bob", externalId: "user_bob" });
 
// Now Roxels.start(...) starts a conversation as Bob.

Don't rely on the previous person's identity carrying over after a sign-out.

Deletion

If a user asks to be forgotten, two surfaces:

  • The conversations. Each conversation has a delete endpoint (contact support@roxels.ai for bulk deletion).
  • The person record. Currently deleted via support (a self-service endpoint is on the roadmap).

When you delete a person, future conversations starting with that external_id create a fresh person — no history carries over.

Working with external_id across environments

A common pattern is: in production, external_id is your real user id; in staging, it's a test id; in development, it's whatever a developer happened to type.

If you ever import production data into staging (don't!), or run the same template against multiple environments, use prefixes:

Roxels.start({
  templateKey: "rk-...",
  externalId: `${env}:${userId}`, // e.g. "prod:user_123"
});

This keeps environment data cleanly separated in Roxels.