> ## Documentation Index
> Fetch the complete documentation index at: https://docs.animam.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authenticated visitors (behind your login)

> Run the agent inside your logged-in area: your backend signs who the visitor is, your backend serves their data, and the agent answers with it — without Animam ever touching your authentication.

The widget works behind a login exactly as it does on a public page — it is a script tag either way. The real question is what the agent *knows* about the person who is logged in.

The answer is a division of labour: **Animam never authenticates anyone.** It receives an identity your system has already proven, then asks your system for that person's data. You stay the authority on both.

## Three layers, adopt them in order

| Layer                              | What it gives you                                             | Plan    |
| ---------------------------------- | ------------------------------------------------------------- | ------- |
| **Identity** — visitor JWT         | The agent knows *who* is talking, across sessions and devices | Starter |
| **Data** — context webhook         | The agent knows *their record*: orders, balance, case status  | Starter |
| **Action** — verified actions, MCP | The agent *does* something in their account                   | Builder |

Each layer stands alone. Identity alone already gives you memory that follows the person rather than the browser.

***

## 1. Get your signing key

```bash theme={null}
curl -X POST https://api.animam.ai/tenants/YOUR_SLUG/visitor-auth \
  -H "Cookie: animam_session=…"
```

```json theme={null}
{ "signingKey": "3f9a…", "usage": { "algorithm": "HS256", "…": "…" } }
```

<Warning>
  The key is returned **once** and never readable again — store it in your backend secrets. Regenerating it invalidates every token already issued. It must never reach the browser: it signs identities, so anyone holding it can impersonate any of your users.
</Warning>

## 2. Sign a token for the logged-in user

Do it server-side, when you render the page. Nothing here is Animam-specific — it is a plain HS256 JWT.

```js theme={null}
import jwt from 'jsonwebtoken'

app.get('/account', requireLogin, (req, res) => {
  const visitorToken = jwt.sign(
    {
      sub: req.user.id,            // required — your stable internal id
      email: req.user.email,       // optional
      name: req.user.displayName,  // optional
      phone: req.user.phone,       // optional
    },
    process.env.ANIMAM_VISITOR_SIGNING_KEY,
    { algorithm: 'HS256', expiresIn: '1h' }
  )
  res.render('account', { visitorToken })
})
```

Use your **internal id** as `sub`, not the email: it is what ties the visitor to a stable record even when they change address.

Animam checks the signature, the presence of `sub` and `iat`, and the expiry — defaulting to `iat + 24h` when you omit `exp`. An invalid or expired token is not an error: the visitor is simply treated as anonymous, and the conversation continues.

<Note>
  The token sits in your page's HTML, so treat it as visible to any script on that page: keep the claims minimal and the lifetime short. It also cannot be revoked — logging out does not invalidate a token that is still within its `exp`. An hour is a good default.
</Note>

## 3. Hand it to the widget

```html theme={null}
<script
  src="https://cdn.animam.ai/widget.js"
  data-tenant="YOUR_SLUG"
  data-visitor-token="{{ visitorToken }}"
></script>
```

The widget sends it as the `X-Visitor-Token` header on every message. Animam verifies it and resolves — or creates — the matching `VisitorUser`, keyed on `sub` (or `email` when it matches an existing record). From there, everything the agent remembers about that person follows them across browsers and devices.

Restrict `allowedDomains` on your tenant to your own origins while you are at it: the widget then refuses to answer from anywhere else.

## 4. Serve the visitor's record

Point the tenant at an endpoint you own:

```bash theme={null}
curl -X PATCH https://api.animam.ai/tenants/YOUR_SLUG \
  -H "X-API-Key: $ANIMAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "visitorContextUrl": "https://your-backend.example/animam/context" }'
```

On **every message** from an identified visitor, Animam POSTs to it:

```json theme={null}
POST https://your-backend.example/animam/context
X-Animam-Signature: <hmac-sha256(raw body, your signing key), hex>
Content-Type: application/json

{
  "email": "user@example.com",
  "externalId": "user-4821",
  "visitorId": "9c1f…",
  "timestamp": "2026-07-25T14:21:00.000Z"
}
```

Verify the signature over the **raw body**, then answer with free text — you decide what the agent is allowed to know:

```js theme={null}
import { createHmac, timingSafeEqual } from 'crypto'

app.post('/animam/context', express.raw({ type: 'application/json' }), async (req, res) => {
  const expected = createHmac('sha256', process.env.ANIMAM_VISITOR_SIGNING_KEY)
    .update(req.body)                       // Buffer — the exact bytes sent
    .digest('hex')
  const got = req.get('X-Animam-Signature') || ''
  if (got.length !== expected.length || !timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
    return res.status(401).end()
  }

  const { externalId } = JSON.parse(req.body)
  const user = await db.users.findByIdOrNull(externalId)
  if (!user) return res.json({ context: null })

  const orders = await db.orders.recentFor(user.id, 3)
  res.json({
    context: [
      `Customer since ${user.createdAt.getFullYear()}, ${user.plan} plan.`,
      `Recent orders: ${orders.map((o) => `#${o.ref} (${o.status})`).join(', ')}.`,
      user.openTicket ? `Open ticket: ${user.openTicket.subject}.` : null,
    ].filter(Boolean).join('\n'),
  })
})
```

The text is injected into the agent's prompt for that conversation.

<Warning>
  Scope the query to the identified visitor, and to them alone. Animam relays what you return — it cannot tell one customer's data from another's. Send only what you would accept the agent saying out loud, and never tokens or credentials.
</Warning>

### Optional: a second factor

If reading the record is not enough proof for a sensitive request, return a `challenge` alongside the context:

```json theme={null}
{
  "context": "Account holder, 2 open contracts.",
  "challenge": {
    "type": "sms_code",
    "prompt": "I've sent a code to the mobile ending 46 — what is it?",
    "verifyUrl": "https://your-backend.example/animam/verify",
    "id": "chg-7781"
  }
}
```

The agent relays your `prompt`, then POSTs the visitor's answer to `verifyUrl` (same HMAC signature, 10 s timeout). Reply `{ "verified": true, "context": "…" }` to unlock more, or `{ "verified": false, "message": "…" }` to refuse. You own the factor and the verdict; Animam only carries the conversation.

## 5. Let the agent act

Reading is half the job. To cancel a subscription, release an invoice or update a preference, declare a [verified action](/guides/verified-actions): Animam POSTs to your endpoint carrying the **server-verified email** — never an address the model produced. A JWT identity qualifies, exactly like a one-time code does.

For a broader surface, plug in [your own MCP server](/guides/tools) and the agent discovers your tools directly.

## What this costs you, in plain terms

|               |                                                                                                                          |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Configuration | **API only** — there is no dashboard screen for visitor auth or the context webhook yet                                  |
| Your side     | Sign a JWT, expose one HTTPS endpoint. Half a day if you already have an API                                             |
| Latency       | The webhook is called **on every message**, with a **5 s timeout** and **no caching** — keep it fast                     |
| Size          | Context is truncated at **5 000 characters**                                                                             |
| Failure       | Silent: on timeout or error the agent answers **without** the record rather than stalling. Log on your side              |
| Rate limits   | 20 messages/min and 200/h **per IP** — worth checking if your users sit behind one corporate egress                      |
| Network       | Your URL must be publicly resolvable: private ranges, loopback and cloud metadata are refused (SSRF guard, DNS included) |

## What Animam stores

Conversations, `VisitorUser` profiles and the facts the agent remembers are stored on Animam's infrastructure. The record you return through the webhook is **not** persisted — it is fetched per conversation and injected into the prompt. Plan for this in your privacy notice; a DPA is available at `contact@animam.ai`.
