Skip to content

Embedded Host Page ​

An intranet page that embeds the chat in an iframe, signs the person in with a token its own server signs, handles one host tool and listens to events. The chat page itself is EmbedChat; a Cognipeer Console portal serves it for you at /api-embed/portal/{key}.

1. The host server mints a token ​

The token is whatever your portal expects to verify (a signed identity claim for the signed-in person). The host only has to produce it on request and keep it short-lived. A sketch:

ts
// GET /api/cognipeer-token  (your server, behind your own login)
app.get("/api/cognipeer-token", requireLogin, async (req, res) => {
  const token = await signIdentityToken({ sub: req.user.id, email: req.user.email, exp: nowPlusMinutes(5) });
  res.type("text/plain").send(token);
});

How the portal verifies it (key, claims, lifetime) is configured in the portal; see its own embedding guide.

2. The host page ​

html
<iframe
  id="cp"
  title="Support assistant"
  src="https://console.example.com/api-embed/portal/support?tenant=acme&origin=https%3A%2F%2Fapp.example.com"
  allow="clipboard-write"
  style="width: 420px; height: 640px; border: 0"
></iframe>

<script>
  const CONSOLE = "https://console.example.com";
  const frame = document.getElementById("cp");

  async function getToken() {
    const res = await fetch("/api/cognipeer-token", { credentials: "include" });
    return res.text();
  }

  // The tools the portal declared for this page
  const tools = {
    open_ticket: async ({ id }) => {
      location.hash = `#/tickets/${encodeURIComponent(id)}`;
      return { opened: id };
    },
  };

  const send = (message) => frame.contentWindow.postMessage({ v: 1, ...message }, CONSOLE);

  window.addEventListener("message", async (event) => {
    // Only the iframe, only the console's origin
    if (event.origin !== CONSOLE || event.source !== frame.contentWindow) return;
    const m = event.data;
    if (!m || m.v !== 1) return;

    switch (m.type) {
      case "cp:ready":
      case "cp:token_request":                 // reason: expiring | expired | rejected
        send({ type: "cp:auth", token: await getToken() });
        break;

      case "cp:tool_call": {
        const run = tools[m.name];
        if (!run) return send({ type: "cp:tool_result", callId: m.callId, ok: false, error: "Unknown tool" });
        try {
          send({ type: "cp:tool_result", callId: m.callId, ok: true, output: await run(m.args) });
        } catch (e) {
          send({ type: "cp:tool_result", callId: m.callId, ok: false, error: String(e.message || e) });
        }
        break;
      }

      case "cp:event":
        if (m.event === "message:completed") badge.textContent = "1";
        if (m.event === "auth:error") console.warn("chat sign-in problem", m.data.code, m.data.reference);
        if (m.event === "limit:reached") showToast("Usage limit reached");
        break;
    }
  });

  // Page context (unsigned: never use it for authorization) and a prefilled question
  send({ type: "cp:context", context: { page: "tickets", selectedTicket: 1042 } });
  document.getElementById("ask").onclick = () => send({ type: "cp:prefill", text: "Summarise this ticket", send: true });

  // When the person logs out of your app
  document.getElementById("logout").onclick = () => send({ type: "cp:logout" });
</script>

The embedded chat inside a host page, showing an approval card

3. The chat page (if you serve it yourself) ​

tsx
// the page served in the iframe
import { EmbedChat } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";

export default function EmbeddedSupport() {
  return (
    <EmbedChat
      baseUrl="/api/portal/support"
      allowedOrigins={["https://app.example.com"]}
      clientTools={[
        { name: "open_ticket", description: "Open a ticket in the host app", timeoutSeconds: 30,
          inputSchema: { type: "object", required: ["id"], properties: { id: { type: "number" } } } },
      ]}
      eventContent="none"
      showHistory={false}
      theme="light"
    />
  );
}

Checks before going live ​

  • allowedOrigins lists exactly the host origin (scheme and port included); the iframe's origin query parameter says the same.
  • The server sends frame-ancestors for the allowed origins only.
  • The host answers cp:ready and every cp:token_request, and mints tokens that live long enough to be exchanged.
  • Your tool handlers treat args as untrusted input.
  • You listen for auth:error and show the reference to people who need support.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub