# Embed tokens and users (/en/docs/developers/agent/embed/tokens)





Embed tokens let end users access a specific agent and their own sessions. For signed-in users, your backend issues the token and the browser consumes it. This guide covers identity, expiration, refresh, and single-token revocation.

You need a service agent in your organization, a saved embed policy, and a backend API key. Save the policy under **接入与配置 → 配置 → 安全** in the Dashboard; it can currently only be configured there. Examples use Develop. See [Embed in a web page](/en/docs/developers/agent/embed/quickstart) for a complete integration or [Hosted](/en/docs/developers/agent/embed/hosted) for single-use redirects.

## Identity and access [#identity]

Your backend authenticates users through your login system, then supplies a stable `external_user_id`. The same organization, agent, and identifier refer to the same end user. This is neither an organization member ID nor a display name. Identifiers contain 1–128 characters: letters, digits, underscores, periods, colons, `@`, and hyphens.

| Credential       | Consumer                   | Purpose                                                         |
| ---------------- | -------------------------- | --------------------------------------------------------------- |
| API key          | Host backend               | Issue, inspect, and revoke tokens; never send it to the browser |
| Embed token      | End-user client            | Access that user's sessions with the specified agent            |
| Publishable key  | Anonymous page             | Obtain anonymous tokens; may appear in page source              |
| `visitor_secret` | The same visitor's browser | Preserve visitor identity; keep it private                      |

Refreshing a token does not change identity. When accounts change, remove the old component and create one for the new identity. UI confirmation buttons do not replace server-side account authorization in your tools.

## Issue a token [#issue]

Production issuance calls `POST /agent/v1/embed_tokens` from your host backend. These standalone server checks use test user `customer_123`. Your actual host must derive the ID from a verified login session, never a browser assertion. The Dashboard's **试用调试 → 模拟 → 重新签发** is for identity debugging only.

<Tabs items="['cURL', 'TypeScript']">
  <Tab value="cURL">
    Set `PRAXIS_API_KEY` on the server and replace the public agent ID. This command needs cURL and jq.

    ```bash
    set -o pipefail
    curl --fail-with-body --silent --show-error \
      -X POST https://api-develop.deepraxis.com/agent/v1/embed_tokens \
      -H "Authorization: Bearer $PRAXIS_API_KEY" \
      -H "Content-Type: application/json" \
      --data '{"agent":"agent_REPLACE_ME","external_user_id":"customer_123"}' \
      | jq '{id, expires_at, has_token: (.token | type == "string")}'
    ```
  </Tab>

  <Tab value="TypeScript">
    Save as `issue-token.ts`. Set `PRAXIS_API_KEY`, the actual `PRAXIS_AGENT_ID`, and `TEST_EXTERNAL_USER_ID=customer_123` on the server. Run `bun issue-token.ts`.

    ```ts
    const apiKey = process.env.PRAXIS_API_KEY;
    const agentId = process.env.PRAXIS_AGENT_ID;
    const userId = process.env.TEST_EXTERNAL_USER_ID;
    if (!apiKey || !agentId || !userId) throw new Error("Set the three server variables");
    const response = await fetch("https://api-develop.deepraxis.com/agent/v1/embed_tokens", {
      method: "POST",
      headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
      body: JSON.stringify({ agent: agentId, external_user_id: userId }),
    });
    if (!response.ok) throw new Error(`Issuance failed: ${response.status}`);
    const data = await response.json();
    if (typeof data.token !== "string" || typeof data.expires_at !== "string") {
      throw new Error("Invalid token response");
    }
    console.log({ id: data.id, expires_at: data.expires_at, has_token: true });
    ```
  </Tab>
</Tabs>

Expected: HTTP 201 with `id`, `token`, and `expires_at`. Use `id` to revoke this issuance, `token` as the browser credential, and `expires_at` as its ISO 8601 UTC expiration. Examples print only redacted check results. Every call creates a new grant; repeated issuance is not idempotent.

Omitted `ttl_seconds` uses the policy default. The range is 60 seconds through the smaller of the policy maximum and 3600 seconds. `budget_cents` cannot exceed the policy session-budget maximum. `allowed_origins` must be a subset of existing policy entries. Version selection uses explicit `version`, then the policy's pinned version, then latest. The selected version is fixed at issuance. See the [API reference](/en/docs/api/native/agent) for all fields.

## Refresh and end access [#refresh]

Kit calls `getToken(current)` with `null` initially and the current token on refresh. Use this callback for `api.getToken` in the quickstart. Your endpoint verifies that the user is still signed in before returning a fresh token.

```js
async function getToken(current) {
  const response = await fetch("/api/praxis-token", {
    method: "POST", credentials: "same-origin", cache: "no-store",
  });
  if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
  const data = await response.json();
  if (typeof data.token !== "string" || !data.token) throw new Error("Missing token");
  return data.token;
}
```

The callback does not need to send the current token to prove host identity. Kit schedules calls and keeps tokens in memory. Do not hard-code a fixed token. Expected: refreshing preserves access to the same user's sessions.

On sign-out, end the host session and remove the old Kit element. Removal closes its connections but does not revoke issued tokens on the server. Revoke separately when the old credential must stop working immediately. Hosted has no equivalent refresh path; users return to the host after expiration.

## Anonymous visitors [#anonymous]

For public questions without login, open **接入与配置 → 密钥 → 可公开 key → 新建可公开 key**. Configure anonymous limits and budget, and copy the complete value shown only once. Manage allowed origins under **配置 → 安全**. These settings can currently only be configured in the Dashboard. A publishable key cannot replace a backend API key.

Save this standalone page as `index.html`. Replace `PRAXIS_PUBLISHABLE_KEY` with your actual publishable key and serve it from an allowed origin. It does not need a signed-in host token endpoint. Do not also configure `getToken`.

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Public documentation assistant</title>
<script type="module" src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js" integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU" crossorigin="anonymous"></script>
<praxis-agent style="display:block;height:600px;width:100%"></praxis-agent>
<script type="module">
  await customElements.whenDefined("praxis-agent");
  document.querySelector("praxis-agent").setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      publishableKey: "PRAXIS_PUBLISHABLE_KEY",
    },
    agent: { name: "Documentation assistant" },
  });
</script>
</html>
```

Kit sends `publishable_key` to `POST /agent/v1/embed_tokens/anonymous`, adding an existing `visitor_secret` on later requests. No API key is required. Success returns HTTP 200 with `token` and `expires_at`; only new visitors also receive `visitor_secret`. Browsers cannot choose `external_user_id`, budget, instructions, or Hosted options.

Kit saves the visitor credential in local storage, falling back to page memory when storage is unavailable. Do not publish or share it. Expected: the same browser storage preserves visitor identity across refreshes; separate browser storage produces separate identities. Anonymous identity does not authorize access to account data such as orders.

## Inspect and revoke [#revoke]

Revoke only the token whose access should end. These paths perform the same task.

<Tabs items="['Dashboard', 'API']">
  <Tab value="Dashboard">
    Open the agent's **运营与管理 → 签发记录**. Enter `customer_123` in **终端用户** and click **筛选**. Click **吊销** on an active record, verify the user, then click **确认吊销**. Expect the record to show revoked status. Failures offer a retry.
  </Tab>

  <Tab value="API">
    Set `PRAXIS_API_KEY` and the actual `PRAXIS_AGENT_ID` on your server, then list the user's records. Response `data` contains records without tokens. For another page, pass `next_page` as `page` with the original filters.

    ```bash
    curl --fail-with-body --silent --show-error --get \
      https://api-develop.deepraxis.com/agent/v1/embed_tokens \
      -H "Authorization: Bearer $PRAXIS_API_KEY" \
      --data-urlencode "agent_id=$PRAXIS_AGENT_ID" \
      --data-urlencode "external_user_id=customer_123"
    ```

    Set server variable `EMBED_TOKEN_ID` to the chosen record's `id`, then call:

    ```bash
    curl --fail-with-body --silent --show-error -X POST \
      "https://api-develop.deepraxis.com/agent/v1/embed_tokens/$EMBED_TOKEN_ID/revoke" \
      -H "Authorization: Bearer $PRAXIS_API_KEY"
    ```
  </Tab>
</Tabs>

Expected: revocation returns a record with `revoked_at`; repeated revocation returns the original record. Streams using that token receive `session.error` with `EMBED_TOKEN_REVOKED`, then close. Other tokens are unaffected. For user-wide revocation, blocking, and unblocking, see [Manage end users](/en/docs/developers/agent/end-users).

## Errors and next steps [#next]

Revalidate host login after expiration. Stop the original access after revocation. Correct configuration for disallowed origins; ask an administrator to handle blocked users. Do not repeatedly issue tokens to bypass denial. See [Embed limits and errors](/en/docs/developers/agent/embed/limits-and-errors) for codes, HTTP statuses, and recovery conditions.

* [Pass context](/en/docs/developers/agent/embed/context)
* [Manage end users](/en/docs/developers/agent/end-users)
* [Open hosted chat](/en/docs/developers/agent/embed/hosted)
* [Agent API reference](/en/docs/api/native/agent)
