# Embed with one line (/en/docs/developers/agent/embed/iframe)



Add one loader script to display a Praxis-hosted iframe chat interface. Save its appearance in the Dashboard while users stay on your page. “One line” describes frontend installation; your backend still issues tokens for signed-in users.

## Prepare an HTTPS host [#prepare]

You need an HTTPS website whose pages and backend you can change, Bun, an API key in the agent's organization, and a service agent with a short number. This example connects to Develop. Your demo HTTPS entry point must forward to `127.0.0.1:4242` and preserve the original `Host` header. Your host supplies its domain and TLS certificate.

In the Develop Dashboard, open **服务智能体 → your agent → 接入与配置 → 配置 → 安全**. Select a runtime environment, add the actual HTTPS origin under **来源白名单**, and click **保存安全设置**. For example, use `https://support.example.com`, without a path. Embed policies can currently only be configured in the Dashboard.

Enabling **允许 localhost** alone does not authorize iframe embedding. This guide requires a saved HTTPS origin. For HTTP-only local testing, start with the [Kit quickstart](/en/docs/developers/agent/embed/quickstart).

Record two identifiers. Token issuance uses the public ID, `agent_…`. The script's `data-agent` uses the short number copied from the Dashboard header: 5–8 letters or digits. Do not put the full ID in the short-number attribute.

## Provide a token endpoint [#issue]

Save this as `server.ts` behind your HTTPS entry point. It uses two password-protected test accounts. For production, replace `authenticate` with your host's session validation and retain CSRF protection. Set `HOST_ORIGIN` to the actual HTTPS origin, without a trailing slash. Configure all environment variables only on the backend.

```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 = process.env.HOST_ORIGIN;
if (!origin || new URL(origin).protocol !== "https:") throw new Error("Set HOST_ORIGIN to your HTTPS origin");
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 (request.headers.get("Host") !== new URL(origin).host) return new Response("Host rejected", { 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);
```

Start the server after saving `index.html` in the next section. `DEMO_PASSWORD` is your chosen test-account password, not an API key. Sign in through the browser as `customer_123` or `customer_456`.

```bash
export HOST_ORIGIN="https://support.example.com"
export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in the server process.
bun server.ts
```

The loader sends a POST request to `data-token-endpoint` with browser credentials. That endpoint must derive the user ID from validated authentication on every issuance. After Praxis returns HTTP 201, the host returns `token` and `expires_at` to the loader. Keep the API key out of HTML and tokens out of page URLs.

## Install the loader [#install]

Save this as `index.html`, replacing `ABC123` with the actual short number. For an existing page, insert only the script tag where you want chat to appear.

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Order assistant</title>
<body>
  <h1>Order support</h1>
  <script src="https://kit-develop.praxisusercontent.com/v1/loader.js" data-agent="ABC123" data-token-endpoint="/api/praxis-token" async></script>
</body>
</html>
```

The loader creates the iframe and transfers its token. Opening or pasting an iframe address alone does not supply the host user's identity.

| Script attribute      | Purpose                                                          |
| --------------------- | ---------------------------------------------------------------- |
| `data-agent`          | Required agent short number                                      |
| `data-token-endpoint` | Required host token endpoint; same-origin in this example        |
| `data-layout`         | Optional: `inline`, `popup`, `sidebar`, or `modal`               |
| `data-launcher`       | Optional: `bubble`, `none`, or an existing button's CSS selector |

When layout and launcher attributes are absent, the loader uses saved Dashboard appearance. Without a saved layout, it uses `popup`. Explicit attributes override the corresponding settings. With `inline`, the script's location determines insertion; reserve enough height in its containing element.

## Save appearance [#appearance]

Open **接入与配置 → 接入方式与外观 → iframe**. Adjust the name, greeting, and layout, then click **保存**. Expect confirmation that changes apply after refreshing the embedded page. Refresh your host page to verify. Persistent appearance can currently only be configured in the Dashboard.

With `data-launcher="none"`, a floating layout has no default opening button. Supply an existing button's CSS selector, or control opening in host code. In this example, the button exists before the loader executes:

```html
<button id="open-support" type="button">Open support</button>
<script src="https://kit-develop.praxisusercontent.com/v1/loader.js" data-agent="ABC123" data-token-endpoint="/api/praxis-token" data-layout="popup" data-launcher="#open-support" async></script>
```

This replaces the previous script. Keep only one loader script for the same agent on a page.

## Verify and troubleshoot [#verify]

Open your HTTPS page and sign in. Confirm that chat or its launcher appears, send a question, and receive a reply. Use a separate browser to sign in as the second test account. Confirm it cannot see the first account's conversations.

In the network panel, check that `loader.js` loads, the iframe comes from `chat-develop.praxisusercontent.com`, and the host token endpoint succeeds. The API key belongs only in server-to-server requests; it must not appear in browser network records.

* Browser blocks the frame: check that the saved origin matches the page. Your host CSP must allow `https://chat-develop.praxisusercontent.com` in `frame-src` and the script CDN in `script-src`. Do not disable the entire CSP to troubleshoot.
* Console reports `Invalid Praxis loader configuration`: check the short number format, script domain, and token endpoint attribute.
* Host endpoint returns HTTP 401 or 403: restore host authentication or correct the same-origin check. Retrying does not sign the user in.

Test origin rejection by loading the script from another HTTPS site outside the allowlist; expect the browser to block embedding. After signing out of your host, request its token endpoint again and expect rejection. A hidden button does not prove authorization.

Reload the host page after sign-out or account changes so the backend issues for the new identity and removes the old interface. Opening or closing the loader only changes visibility; it does not sign the user out of your host.

## Next steps [#next]

* [Customize appearance](/en/docs/developers/agent/embed/appearance)
* [Protect your integration](/en/docs/developers/agent/embed/security)
* [Agent API reference](/en/docs/api/native/agent)
* [Cookbook](/en/docs/cookbook)
