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 expiryReact
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:falsemeans 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.