Embed with one line
Install a loader script on an HTTPS website to give signed-in users an iframe chat interface.
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
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.
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
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.
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.
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.tsThe 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
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.
<!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
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:
<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
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.cominframe-srcand the script CDN inscript-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.