# Embed in a web page (/en/docs/developers/agent/embed/quickstart)





Add PraxisKit to your page so signed-in users can talk to a service agent. This local order-assistant example covers backend issuance, mounting, and user isolation. For other interfaces, see [Choose an integration](/en/docs/developers/agent/embed/overview).

You need Bun, an available service agent and runtime environment, and an API key in the same organization. The example connects to Develop. Store `PRAXIS_API_KEY` only in the backend environment. The browser authenticates with your host; your host verifies identity, requests a token from Praxis, and passes that token to Kit.

## 1. Prepare your agent [#prepare]

In the Develop Dashboard, open &#x2A;*服务智能体 (Service agents)** and select an existing agent. Save its instructions under **接入与配置 → 配置 → 指令**. Under **配置 → 安全**, select a runtime environment, enable **允许 localhost**, and click **保存安全设置**. The example runs at `http://localhost:4242`. Use a dedicated test agent with an empty origin allowlist and localhost enabled. Local requests are rejected when the token inherits a nonempty HTTPS origin list. Add your website's origin when moving to HTTPS hosting.

Embed policies can currently only be configured in the Dashboard. Copy the public agent ID (`agent_…`). After the saved confirmation appears, continue with actual issuance; saving settings alone does not prove that chat works.

## 2. Create a token endpoint [#issue]

Create a directory and save this as `server.ts`. This loopback-only demo uses HTTP Basic authentication for two test accounts. Set your own test password in `DEMO_PASSWORD`; it is unrelated to your Praxis API key. The server derives the user ID from the authenticated account and ignores any client-supplied user ID.

```ts
import { timingSafeEqual } from "node:crypto";

const apiKey = process.env.PRAXIS_API_KEY;
const agentId = process.env.PRAXIS_AGENT_ID;
const password = process.env.DEMO_PASSWORD;
if (!apiKey || !agentId || !password) throw new Error("Set the three server variables");
const origin = "http://localhost:4242";
const noStore = { "Cache-Control": "no-store" };
function authenticate(request: Request): string | undefined {
  const supplied = Buffer.from(request.headers.get("Authorization") ?? "");
  for (const id of ["customer_123", "customer_456"]) {
    const expected = Buffer.from("Basic " + Buffer.from(`${id}:${password}`).toString("base64"));
    if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) return id;
  }
}
Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  async fetch(request) {
    const url = new URL(request.url);
    if (url.origin !== origin) return new Response("Use localhost:4242", { status: 403 });
    const userId = authenticate(request);
    if (!userId) return new Response("Sign in", {
      status: 401, headers: { ...noStore, "WWW-Authenticate": 'Basic realm="Local demo"' },
    });
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: noStore });
    }
    if (request.method !== "POST" || url.pathname !== "/api/praxis-token") {
      return new Response("Not found", { status: 404 });
    }
    if (request.headers.get("Origin") !== origin) {
      return new Response("Origin rejected", { status: 403 });
    }
    try {
      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 }),
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) return new Response("Token issuance failed", {
        status: response.status, headers: noStore,
      });
      const data = await response.json();
      if (typeof data.token !== "string" || typeof data.expires_at !== "string") {
        throw new Error("Invalid token response");
      }
      return Response.json({ token: data.token, expires_at: data.expires_at }, { headers: noStore });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);
```

Set the three variables in the server process. Replace `PRAXIS_AGENT_ID` with the actual ID. Save `index.html` in the next step before starting.

```bash
export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in this server process, then run:
bun server.ts
```

Expected: the terminal prints the local URL. The host rejects unauthenticated requests. Only authenticated, same-origin requests reach Praxis. Praxis returns HTTP 201 on issuance; the host returns only `token` and `expires_at`, with caching disabled.

For an existing application, replace `authenticate` with its session validation. Mount the route behind login and CSRF protection, and recheck identity on every issuance. Do not use these test accounts as your production login system. See [Embed tokens and users](/en/docs/developers/agent/embed/tokens) for expiration and refresh.

## 3. Mount the component [#mount]

Choose one frontend implementation. The API address, CDN, and agent must use the same environment. These examples pin Kit 0.2.8 on Develop.

<Tabs items="['Web Component', 'React']">
  <Tab value="Web Component">
    Save this as `index.html` in the same directory, then run the startup command above.

    ```html
    <!doctype html>
    <html lang="en">
    <meta charset="utf-8">
    <title>Order 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>
    <p id="status" role="status"></p>
    <script type="module">
      await customElements.whenDefined("praxis-agent");
      const agent = document.querySelector("praxis-agent");
      agent.addEventListener("praxis.error", () => {
        document.querySelector("#status").textContent = "Chat unavailable. Check your sign-in and configuration.";
      });
      agent.setOptions({
        api: {
          url: "https://api-develop.deepraxis.com",
          getToken: async () => {
            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;
          },
        },
        agent: { name: "Order assistant" },
      });
    </script>
    </html>
    ```
  </Tab>

  <Tab value="React">
    Add this module script to the HTML entry point of an existing React application. Mount the token route on your application backend, keeping `/api/praxis-token` same-origin and protected by login. The demo server does not need to serve your React page.

    ```html
    <script type="module" src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js" integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU" crossorigin="anonymous"></script>
    ```

    Save this as `Support.jsx` and render `<Support />` on a signed-in page. It mounts the CDN-registered Web Component and removes it on unmount.

    ```jsx
    import { useEffect, useRef, useState } from "react";

    export default function Support() {
      const container = useRef(null);
      const [error, setError] = useState("");
      useEffect(() => {
        let cancelled = false;
        let agent;
        const onError = () => setError("Chat unavailable. Check your sign-in and configuration.");
        async function mount() {
          await customElements.whenDefined("praxis-agent");
          if (cancelled) return;
          agent = document.createElement("praxis-agent");
          agent.style.cssText = "display:block;height:600px;width:100%";
          agent.addEventListener("praxis.error", onError);
          agent.setOptions({
            api: {
              url: "https://api-develop.deepraxis.com",
              getToken: async () => {
                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;
              },
            },
            agent: { name: "Order assistant" },
          });
          container.current.appendChild(agent);
        }
        mount().catch(onError);
        return () => { cancelled = true; agent?.removeEventListener("praxis.error", onError); agent?.remove(); };
      }, []);
      return <><div ref={container} /><p role="status">{error}</p></>;
    }
    ```
  </Tab>
</Tabs>

Expected: a chat area appears with a height of 600 pixels. `api.url` sets the API address, `getToken` returns a token string, and `agent.name` sets the display name. Wait for registration before calling `setOptions`. The `praxis.ready` event only confirms mounting. A real reply is required to verify the integration.

## 4. Verify the integration [#verify]

1. Open `http://localhost:4242`. Sign in as `customer_123` using your test password. Ask what order questions the assistant can help with and confirm a reply arrives.
2. In the network panel, check that the host token request succeeds without returning the API key. Session requests should use an embed token. Do not copy credentials into logs or screenshots.
3. In a separate browser profile or private window, sign in as `customer_456`. Expect no access to the first account's conversations. Two tabs sharing one login do not test isolation.
4. Request the local token endpoint without credentials. Expect HTTP 401 from the host. In your application, also verify that signing out prevents further issuance.

```bash
curl --silent --output /dev/null --write-out "%{http_code}\n" \
  -X POST http://localhost:4242/api/praxis-token \
  -H "Origin: http://localhost:4242"
```

## Troubleshoot failures [#troubleshoot]

These are defined API failure cases. Check the actual response in your network panel:

* Issuance HTTP 400: confirm that the embed policy is saved and the runtime environment is available.
* Issuance HTTP 404: check the agent ID and confirm the API key and service agent belong to the same organization.
* Chat returns `EMBED_ORIGIN_NOT_ALLOWED` (HTTP 403): check the page origin. Enable localhost for local testing or save the HTTPS origin for your hosted site.

## Next steps [#next]

* [Pass context](/en/docs/developers/agent/embed/context)
* [Customize appearance](/en/docs/developers/agent/embed/appearance)
* [Agent API reference](/en/docs/api/native/agent)
* [SaaS integration example](/en/docs/cookbook/scenarios/saas-embedded-assistant)
