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

# Identity and context

> What belongs in identify, what belongs in setContext, and how each is stored.

Peeve stores two different things about a user, in two different ways, and the
split is about how each is protected.

|          | `identify`                              | `setContext`                       |
| -------- | --------------------------------------- | ---------------------------------- |
| For      | Who the person is                       | What your product knows about them |
| Shape    | A fixed set of modelled fields          | Any key/value you like             |
| At rest  | **Encrypted** under a per-workspace key | Plaintext                          |
| Examples | email, name, company, phone             | seats, MRR, role, lifecycle stage  |

<Warning>
  Put personal data in `identify`. Do not put email addresses, phone numbers or
  anything else identifying into `setContext` — it is stored as plaintext,
  because it is designed for non-secret business context.
</Warning>

## `identify` — who they are

```js theme={null}
window.peeve.identify({
  id: "user_8412",
  email: "dana@example.com",
  name: "Dana Whitfield",
  company: "Example Corp",
  plan: "Growth",
  createdAt: "2026-02-14T09:31:00Z",
});
```

`email`, `name`, `company` and `phone` are encrypted at rest. The email is
additionally indexed by a keyed hash, so contact lookup and email search keep
working without decrypting anything.

`plan` is the user's plan in **your** product, as a display name. It is stored
plaintext because it is not personal data — and it has no effect on your Peeve
entitlements, which come from your own subscription.

`createdAt` is worth setting. It is how Peeve distinguishes a genuine new signup
from an existing customer browsing logged out, so a returning user is not
miscounted as a visitor-to-user conversion.

<Note>
  Identity is **never trusted for authentication**. Anything the browser sends
  in `contact` is enrichment only — it cannot resolve a workspace, grant access,
  or read another person's data. The workspace always comes from the validated
  key.
</Note>

## `setContext` — what you know about them

```js theme={null}
window.peeve.setContext({
  seats: 12,
  mrr: 4800,
  role: "admin",
  lifecycle: "trial",
  onboardingComplete: false,
});
```

An open bag, merged across calls — the last value for a key wins. It is
sanitized and capped on write.

It surfaces in two places: a teammate sees it as a details panel when a
conversation is handed off, and the agent gets it as reference context so it can
tailor an answer. The agent treats it as data, never as instructions.

## The server-side equivalent

Both have a server-side counterpart on
[`POST /v1/users`](/api/v1/users/create-or-update-a-user), so you can establish a complete
contact before the person ever opens a browser:

```bash theme={null}
curl https://api.peeve.ai/v1/users \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "external_user_id": "user_8412",
    "email": "dana@example.com",
    "name": "Dana Whitfield",
    "plan": "Growth",
    "attributes": { "seats": 12, "role": "admin" }
  }'
```

The top-level identity fields are `identify`; the `attributes` object is
`setContext`. A later browser `identify` with the same `external_user_id` merges
into the same record.

## Identity is keyed on your id, not on the key

A contact is identified by `(workspace, external_user_id)`. It is deliberately
not keyed on the API key — if it were, every user would be orphaned the day you
rotated a key, and their history would vanish.

So: use a **stable** id. Your internal user id is right. An email address is
usable but changes; a session id is not an identity at all.

## Anonymous visitors

Before someone signs in, Peeve still tracks them, using a persistent browser
visitor id. When you later call `identify`, the pre-login history links to the
now-known user.

This is why calling `identify` at the right moment matters: it is what joins
"someone browsed pricing three times" to "Dana signed up".

## On logout

```js theme={null}
import { Peeve } from "@peeve/sdk";

Peeve.reset();
```

Clears both identity and context. Without it, the next person to use that
browser inherits the previous user's identity.

## Vendor tokens

`POST /v1/users` accepts a `vendor_token` — a token your backend issues for a
user, so a Custom API connector can call your API as them.

<Warning>
  A vendor token never travels through the browser. It is pushed
  server-to-server, stored encrypted, and used server-side. There is no
  browser-side equivalent, by design: anything in the DOM is reachable by any
  script on the page, including a prompt-injection payload.
</Warning>

Rotating a token supersedes the previous one in the token history, which stores
a fingerprint only — never the recoverable secret. A freshly pushed token is
marked valid but unverified, so it is re-checked against your API on the next
agent request. That is what stops a token you have already revoked staying
valid inside Peeve.

<Note>
  A token authenticates; it does not authorize. Possession of one is full
  account authority, so the capability allowlist you configure — not the token —
  is what stands between a leaked token and real damage.
</Note>
