Reference/example embedded apps for the Xola Seller App's Embedded Apps platform, plus a guide to the @xola/embedded-app-sdk API.
An embedded app is a self-contained web page that the Seller App loads inside an iframe at a declared location (a header button, a sidebar menu item, a hidden background worker, etc.). It talks to the host Seller App over a small, versioned postMessage RPC — wrapped by the SDK — to read page context, make authenticated Xola API calls, show UI, and react to real-time events. It never shares the seller app's codebase or privileged access.
| App | canvasType |
Demonstrates |
|---|---|---|
drawerApp |
drawer |
Slide-out panel. Handshake, fetch, toasts, navigation, event log, refreshing context on canvas.open. |
backgroundMonitorApp |
none |
Hidden background worker. Watches the route via context.change and surfaces itself on demand with ui.canvas.show(). |
fullscreenApp |
fullscreen |
Full-page app with its own internal routing/history; pulls context with xola.context.get(). |
Each is a plain Vite app: index.html + main.js, no framework.
cd drawerApp
npm install
npm run dev # serves on a localhost port, e.g. http://localhost:5173The apps depend on the SDK via a local file link (file:../../x2-seller/packages/embedded-app-sdk), so the x2-seller repo must sit alongside this one and the SDK must be built (cd x2-seller/packages/embedded-app-sdk && npm install && npm run build).
To see an app inside the Seller App, register a plugin whose ui.embeddedUrl points at the local Vite URL (see Registering an app below), install it on your seller, and open the Seller App.
Firefox note: Enhanced Tracking Protection can block the app's cross-site calls to the Xola API from inside the iframe. If
xola.fetchfails with a(null)status, disable ETP for the page.
import { initXola } from "@xola/embedded-app-sdk";
// 1. Initialize with the Xola API version your app is written against.
const xola = initXola({ apiVersion: "2025-07-07" });
// 2. Handshake — resolves once the host has registered this iframe's session.
// The SDK auto-retries internally, so just await it.
const hs = await xola.handshake();
// hs: { appId, sellerId, userId, roles, locale, route, context, apiUrl, appToken, timezoneName, timezoneOffset, apiVersion }
// 3. You're live. Subscribe to events, fetch data, render.After the handshake you can call any SDK method. Calls made before it resolves queue against the pending handshake where it matters (e.g. fetch awaits it internally).
Creates the SDK instance and installs the message listener + a global error reporter (uncaught errors / unhandled rejections are forwarded to the host for telemetry). apiVersion is the Xola API date version your app targets; it's injected as x-api-version on every xola.fetch.
Requests the session payload from the host. Resolves with the seller/user identity, current route, page context, apiUrl, and a scoped appToken. Pass { apiVersion } to override the version for this call. Auto-retries until the host responds (handles the iframe-load/registration race); rejects on a real host error or after a 30s deadline.
Pulls the current route + page context from the host (a live fetch, not a cached snapshot). Use this on canvas.open (or any time) to get fresh context without subscribing to every change. context includes route merged in.
The host pushes real-time events. You must subscribe to the event names you care about, then attach local listeners.
await xola.events.subscribe(["experience.update", "context.change", "canvas.open"]);
const off = xola.events.on("purchase.update", ({ id }) => { /* ... */ });
xola.events.on("context.change", ({ route, context }) => { /* nav happened */ });
xola.events.on("canvas.open", async () => {
const { route, context } = await xola.context.get(); // refresh on open
});
xola.events.on("*", ({ event, data }) => { /* receive everything */ });
off(); // on() returns an unsubscribe fn
xola.events.off("purchase.update", cb); // or remove explicitly
await xola.events.unsubscribe(["experience.update"]);Event names (XolaEvent constants):
| Name | Data | Fires when |
|---|---|---|
context.change |
{ route, context } |
Host route/context changes (navigation). |
canvas.open |
{ route, context } |
This app's drawer/modal is opened. Subscribe to this to refresh on open instead of following every context.change. |
experience.update |
{ id } |
An experience changed. |
purchase.update |
{ id } |
A purchase changed. |
package.update |
{ id } |
A package changed. |
event.update |
{ id } |
A scheduled event/timeslot changed. |
liveChat.update |
{} |
Live-chat availability changed. |
xchatDisabled.update |
{ enabled } |
XChat toggled. |
auth.tokenRefreshed |
{ token } |
SDK refreshed the auth token (rebuild long-lived connections). |
* |
{ event, data } |
Wildcard — every event. |
xola.ui.toast("success", "Saved!"); // type: "success" | "info" | "warning" | "error"
xola.ui.drawer.close(); // dismiss a drawer canvas
xola.ui.modal.close(); // dismiss a modal canvas
// canvasType=none (background) apps only — present/hide self as a drawer:
await xola.ui.canvas.show({ canvasPresentation: "push", canvasLocation: "right", canvasSize: "medium" });
await xola.ui.canvas.hide();const token = await xola.auth.getToken(); // cached; auto-refreshes near expiry
const token2 = await xola.auth.getToken({ force: true }); // bypass cache
// fetch injects x-api-key / x-seller-id / x-user-id / x-app / x-api-version headers
// and the seller query param, retries once on 401, and returns parsed JSON.
const me = await xola.fetch("/api/users/me");
const created = await xola.fetch("/api/something", { method: "POST", body: JSON.stringify({...}) });Prefer xola.fetch over managing tokens yourself. For SSE/WebSocket connections that outlive a token, listen for auth.tokenRefreshed and reconnect.
xola.navigate("/orders");
xola.navigate("/orders", { replace: true });Host-mediated React-router navigation. Note: silently ignored for third-party (non-first-party) apps.
The host keeps your iframe alive across opens — a drawer app is loaded once and reused, not reloaded each time it's opened. Consequences:
- Do the handshake once on startup.
- The context you got at handshake goes stale after the user navigates and reopens you from a different screen. Subscribe to
canvas.openand refresh viaxola.context.get()(or read the event payload) so you always reflect the screen you were opened from. SeedrawerApp/main.js. - Don't assume a fresh page per open; persist your own in-app state as you see fit.
Context is a nested hierarchy of { id, <childrenPlural>: [...] }, children always arrays. Examples by location:
// Roster header
{ events: [{ id, purchases: [{ id, items: [{ id, guests: [{ id }] }] }] }] }
// Purchase detail
{ purchases: [{ id, items: [{ id, guests: [{ id }] }] }] }
// A single-entity detail page
{ customers: [{ id }] } // or { partners: [{ id }] }, { guides: [{ id }] }, ...route is always available alongside the context.
An embedded app is a Plugin whose ui block carries the embedded-app fields (managed in elrond / Store Console):
| Field | Type | Notes |
|---|---|---|
isEmbeddedApp |
boolean | Marks the plugin as an embedded app. |
isFirstParty |
boolean | First-party (Xola-authored) apps get extra privileges (e.g. navigate). |
embeddedUrl |
string | Origin/URL the iframe loads (your hosted app, or local Vite URL in dev). |
canvasType |
none | drawer | modal | fullscreen |
How/where the app surface renders. |
canvasSize |
small | medium | large | xl | 2xl |
Drawer/modal size. |
canvasLocation |
left | right |
Drawer side. |
canvasPresentation |
overlay | push |
Drawer overlays content or pushes it aside. |
triggers |
[{ type, location, label, icon }] |
Entry points. type: button | menuItem | automatic. location: a trigger-location id (below). |
requestedContextFields |
[string] |
Context fields the app needs. |
apiScopes |
[string] |
API scopes the app requires. |
Canvas types: drawer/modal/fullscreen render a visible surface; none is a hidden background worker (must use triggerType: automatic, and can surface itself via ui.canvas.show). fullscreen requires a menuItem trigger.
Example trigger-location ids: dashboard.headerBar, dashboard.roster.headerBar, purchases.detail.headerBar, purchases.detail.buttonBar, customers.contacts.headerBar, distribution.operators.detail.headerBar, resources.equipment.detail.headerBar, customers.leftMenu, settings.leftMenu, … (a button location renders a button; a leftMenu location renders a sidebar item).
- Scaffold a Vite app (copy
drawerAppas a starting point):index.html+main.js, add@xola/embedded-app-sdkas a dependency. - On load:
const xola = initXola({ apiVersion }); const hs = await xola.handshake(); - Render from
hs.context/hs.route; subscribe to the events you need; for drawer/modal apps, refresh oncanvas.open. - Make API calls with
xola.fetch(...). - Host it somewhere the Seller App can reach, register a plugin pointing
ui.embeddedUrlat it with the rightcanvasType/triggers, install it on a seller.
See the three example apps for working patterns.