Skip to content

App Bridge SDK

@onebooks/app-bridge is a small, zero-dependency wrapper around the App Bridge protocol — the postMessage channel between your embedded page and the OneBooks host. It handles origin validation, request/response correlation and timeouts; you call plain async methods on the object createApp() returns.

No bundler? dist/app-bridge.global.js is a self-contained script-tag build — see Script tag usage below.

import { createApp } from '@onebooks/app-bridge';
const app = createApp();
await app.ready(); // required — see below

createApp(options?) reads host (and locale/embedded/target/ resourceType/resourceId on an extension page) from the current URL automatically — see Embedded apps. options.host overrides the detected host origin; options.timeoutMs (default 10000) sets how long to wait for a host response. createApp() throws synchronously — not a rejected promise — if no valid host is available, since nothing can be sent safely without one.

app.params exposes the parsed query params if you need them directly: { host, locale?, embedded, target?, resourceType?, resourceId? }.

await app.ready(); // first
const ctx = await app.context();
console.log(ctx.business.name, ctx.locale, ctx.theme);
await app.toast('Saved', { tone: 'success' });
await app.navigate.host({ resource: 'invoice', id: invoiceId });
const confirmed = await app.confirm({
title: 'Disconnect account?',
message: 'This stops syncing new orders.',
destructive: true,
});
const selection = await app.pick({ resource: 'customer' }); // [] if the merchant cancels
app.autoResize(); // in a block or an action dialog
await app.close(); // in an action dialog, when you're done
MethodNotes
app.ready()Resolves { hostVersion }. Call it first — see above.
app.sessionToken()Resolves a session token (string). The SDK caches it until ~10 s before it expires and fetches a new one transparently; concurrent callers share one in-flight request. Never persist it — no localStorage, cookie or database. You rarely need this directly — see app.fetch().
app.context()Resolves { locale, dir, theme, business: { name, country, currency }, user: { name }, target } — see the reference.
app.toast(message, options?)options.tone is 'info' | 'success' | 'error'. An empty message rejects with INVALID_PAYLOAD; longer ones are cut at 200 characters; more than 5 toasts in 10 seconds rejects with RATE_LIMITED.
app.navigate.host({ resource, id? })Takes the merchant to a OneBooks screen: invoice | quote | customer | supplier | purchase | sales-return | purchase-return (with an id), or item-list | dashboard (no id). From an action dialog this also closes the dialog.
app.navigate.app(path)Moves your app’s home page to another of your paths (/…, same origin, ≤ 2,048 characters, no . or .. segments). Home page only — rejects with NOT_PERMITTED in a dialog or block.
app.close(result?)Closes your action dialog. Action dialogs only — rejects with NOT_PERMITTED elsewhere. OneBooks doesn’t act on result today.
app.resize(height)Sets your frame’s height in pixels, clamped 60–1600. Blocks and action dialogs only — your home page fills the available space, so there it rejects with NOT_PERMITTED.
app.autoResize()Keeps your frame’s height matched to your content: measures your <body> content, watches <body> with a throttled ResizeObserver and calls resize() whenever the height changes — so the frame shrinks as well as grows. Returns a stop function. No-ops if ResizeObserver isn’t available. Same surfaces as resize() — see Sizing a block or dialog.
app.confirm({ title, message, confirmLabel?, destructive? })Host-rendered confirmation, labelled with your app’s name. Resolves true, or false if the merchant cancels or dismisses it. title 1–80, message 1–500, confirmLabel ≤ 30 characters. One confirm or picker at a time — see Errors.
app.pick({ resource, multiple? })Host-rendered search picker for customer | supplier | item | invoice. Resolves the chosen { id, label }[] — an empty array if the merchant cancels. Rejects with NOT_PERMITTED if your installation lacks the resource’s read scope (customers:read, suppliers:read, items:read or invoices:read).
app.loading(loading)true shows a thin progress bar across the top of your frame (home page, dialog or block), announced to screen readers as <your app> is working…; false clears it. Only shown after ready.
app.setTitle(title)Adds a muted secondary heading beside your registered app name in the home-page and dialog chrome (<your app> · <title>), 1–80 characters. It never replaces the name OneBooks attributes your content to, and has no visible effect on a block.
app.on(event, callback)Subscribes to 'theme.changed' | 'locale.changed'. Returns an unsubscribe function.

Call app.autoResize() once, after your content has mounted. It measures the height of your page’s <body> content — the height of an auto-height <body> plus its top and bottom margins — reports it straight away, then again (at most every 150 ms) whenever it changes. Because it measures your content rather than the frame, a block or dialog shrinks when your content does, not only grows.

That only works while <body> keeps its natural height. Don’t give <body> a fixed or 100% height (or a min-height of 100vh): autoResize() would then measure that height instead of your content, and the frame would stop following it.

Its resize requests are fire-and-forget: a host that refuses one — your home page, which OneBooks sizes itself — or doesn’t answer never surfaces as an unhandled rejection. Call app.resize(height) yourself when you want a specific height, and handle its promise.

The most common pattern is calling your own backend, not the OneBooks API directly. app.fetch() wraps fetch() and automatically attaches Authorization: Bearer <session token> — but only when the request target is same-origin with the embedded page. A cross-origin request is passed through untouched, so a session token can never leak toward a third party:

// same-origin — gets the Authorization header attached automatically
const res = await app.fetch('/api/invoices/recent');
const { invoices } = await res.json();
// cross-origin — sent as-is, no token attached
await app.fetch('https://cdn.example.com/logo.png');

Because the SDK reuses one session token for up to ~50 seconds, your backend sees the same token on several requests in a row. Verify it on every request, but exchange it only the first time you see that business and user — a session token can be exchanged once. The Node SDK page shows the pattern, and the starter template implements it.

const off = app.on('theme.changed', ({ theme }) => applyTheme(theme));
// later, if you tear the app down without a full page unload
off();

locale.changed carries { locale, dir }. Both events only start after app.ready(); read the starting values from app.context(). OneBooks never reloads your frame when the merchant switches language or theme — the locale in your URL is only the starting language — so re-render in place when these events arrive.

Every rejected promise carries an AppBridgeError:

interface AppBridgeError extends Error {
code: string;
message: string;
}
CodeMeaning
UNKNOWN_ACTIONSent by the host — your SDK/host versions are out of sync.
INVALID_PAYLOADThe host rejected your request’s payload — a wrong type or a value outside the limits above.
NOT_PERMITTEDThe action isn’t available here: a missing read scope for pick(), or a surface-only action (navigate.app, close, resize) called from the wrong surface.
CANCELLEDThe merchant dismissed the picker. app.pick() handles this for you and resolves []; you only see it if you send picker.open yourself. app.confirm() never rejects with it — dismissing a confirmation resolves false.
RATE_LIMITEDMore than 5 toasts in 10 seconds from your frame — or a confirm()/pick() while another confirmation or picker from your frame is still open (one host dialog at a time).
INTERNALUnexpected host-side error — safe to retry once.
MISSING_HOSTRaised locally by createApp() — no host available.
INVALID_HOSTRaised locally by createApp() — host isn’t https: or http://localhost.
TIMEOUTRaised locally — the host didn’t respond within timeoutMs.
<script src="/static/app-bridge.global.js"></script>
<script>
const app = OneBooksAppBridge.createApp();
app.ready();
</script>

The global build exposes OneBooksAppBridge.createApp and OneBooksAppBridge.AppBridgeError. Serve the file from your own origin — the starter template does exactly this.

  • A response is only accepted if both event.origin === host and event.source === window.parent.
  • app.fetch() only ever attaches the session token to same-origin requests.
  • The session token is short-lived (60 s server-side), refreshed transparently, and never persisted by the SDK — keep it that way in your own code.

Guide: add an action to invoices — a full worked example using this SDK end to end.