Embed X-com in your SaaS

X-com ships as an embeddable product. Host apps such as 123outsourced and Xatlantica render the inbox, tasks and tunnels surfaces inline, and pull time entries + interaction events back to drive their own billing and time tracking.

1. Register a host app

A platform admin creates a host app at /host-apps, generates an install key (cfik_...) and stores it in your host backend as XCOM_INSTALL_KEY. Add your host's origins toallowed_origins so the iframe can be framed there.

2. Mint short-lived embed tokens (server-side)

For every signed-in end-user, your backend calls POST /api/public/v1/embed/token and returns the 5-minute JWT to the browser. Never ship the install key to the client.

import { Xcom } from "@x-com/sdk";

const cf = new Xcom({
  installKey: process.env.XCOM_INSTALL_KEY!,
  hostAppSlug: "123outsourced",
});

const { token, expires_at, mode } = await cf.mintEmbedToken({
  hostTenantExternalId: workspace.id,
  user: { externalId: user.id, email: user.email, displayName: user.name },
  scope: ["inbox", "tasks"],
});

3. Render the surface

Three flavours, same runtime:

Web component (any framework)

<script src="https://embed.xcom.app/embed-loader.js"></script>
<cf-inbox token="{{token}}" theme="light" style="height:600px"></cf-inbox>

Vanilla SDK

import { mount } from "@x-com/embed";
const handle = mount({ surface: "inbox", target: "#cf", token, onEvent: console.log });
handle.setToken(next); // rotate before expiry

React

import { CFInbox } from "@x-com/react";
<CFInbox token={token} height={640} onEvent={onCFEvent} />

4. Pull usage back into your billing

The same embed JWT authorizes three read-only endpoints so your host app can charge end-clients based on real activity:

  • GET /api/public/v1/events — raw interaction stream, cursor-paginated.
  • GET /api/public/v1/time-entries — derived agent-active windows per client.
  • GET /api/public/v1/usage/summary — rolled-up counters per period.

Prefer push? Register a webhook endpoint on the installation and X-com will POST HMAC-signed batches to your URL as events roll up.

5. Subscription states

X-com is the source of truth for licensing. The embed token response includes a mode field:

  • active — full read/write.
  • grace — subscription past due; the UI drops to read-only.
  • HTTP 402 — subscription cancelled; render a payment-required state.

6. postMessage contract (host-side reference client)

The iframe talks to your page with window.parent.postMessage. Every message from X-com carries source: "xcom" plus the surface (inbox, tasks, tunnels). Messages you send in must carry source: "xcom-host".

  • { source: 'xcom', type: 'ready', surface, ok } — surface mounted; ok:false means auth/subscription failed.
  • { source: 'xcom', type: 'error', surface, reason } — e.g. missing_token, subscription_past_due, no_default_workspace.
  • { source: 'xcom', type: 'pong', surface } — reply to your ping; use it as a liveness check.
  • Host → X-com: { source: 'xcom-host', type: 'ping' }.
// Minimal host-side reference client (no SDK required)
const IFRAME_ORIGIN = "https://layer-link-chat.lovable.app";

function mountXcom(container, { surface = "inbox", token, theme = "light" }) {
  const iframe = document.createElement("iframe");
  iframe.src = `${IFRAME_ORIGIN}/embed/${surface}?token=${encodeURIComponent(token)}&theme=${theme}`;
  iframe.style.cssText = "width:100%;height:640px;border:0";
  iframe.allow = "clipboard-write";
  container.appendChild(iframe);

  function onMessage(e) {
    if (e.origin !== IFRAME_ORIGIN) return;          // always check the origin
    const msg = e.data;
    if (!msg || msg.source !== "xcom") return;
    if (msg.type === "ready" && !msg.ok) console.warn("xcom not ready", msg);
    if (msg.type === "error") handleXcomError(msg.reason);
  }
  window.addEventListener("message", onMessage);

  // Liveness check + token rotation before the 5-minute JWT expires.
  const ping = setInterval(
    () => iframe.contentWindow?.postMessage({ source: "xcom-host", type: "ping" }, IFRAME_ORIGIN),
    30000,
  );
  const rotate = setInterval(async () => {
    const fresh = await fetch("/my-backend/xcom-token").then((r) => r.json());
    iframe.src = `${IFRAME_ORIGIN}/embed/${surface}?token=${encodeURIComponent(fresh.token)}&theme=${theme}`;
  }, 4 * 60 * 1000);

  return () => {
    clearInterval(ping);
    clearInterval(rotate);
    window.removeEventListener("message", onMessage);
    iframe.remove();
  };
}

Framing is enforced server-side: the response sets frame-ancestors from the installation's allowed_origins, so add every host origin before going live.

Packages @x-com/embed, @x-com/sdk and @x-com/react live in this repo under packages/ and will publish to npm at GA.