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.
Install
Section titled “Install”No bundler? dist/app-bridge.global.js is a self-contained script-tag build
— see Script tag usage below.
Initialize
Section titled “Initialize”import { createApp } from '@onebooks/app-bridge';
const app = createApp();await app.ready(); // required — see belowcreateApp(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? }.
Using it
Section titled “Using it”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 dialogawait app.close(); // in an action dialog, when you're done| Method | Notes |
|---|---|
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. |
Sizing a block or dialog
Section titled “Sizing a block or dialog”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.
app.fetch() — your own backend
Section titled “app.fetch() — your own backend”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 automaticallyconst res = await app.fetch('/api/invoices/recent');const { invoices } = await res.json();
// cross-origin — sent as-is, no token attachedawait 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.
Reacting to host events
Section titled “Reacting to host events”const off = app.on('theme.changed', ({ theme }) => applyTheme(theme));// later, if you tear the app down without a full page unloadoff();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.
Errors
Section titled “Errors”Every rejected promise carries an AppBridgeError:
interface AppBridgeError extends Error { code: string; message: string;}| Code | Meaning |
|---|---|
UNKNOWN_ACTION | Sent by the host — your SDK/host versions are out of sync. |
INVALID_PAYLOAD | The host rejected your request’s payload — a wrong type or a value outside the limits above. |
NOT_PERMITTED | The 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. |
CANCELLED | The 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_LIMITED | More 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). |
INTERNAL | Unexpected host-side error — safe to retry once. |
MISSING_HOST | Raised locally by createApp() — no host available. |
INVALID_HOST | Raised locally by createApp() — host isn’t https: or http://localhost. |
TIMEOUT | Raised locally — the host didn’t respond within timeoutMs. |
Script tag usage
Section titled “Script tag usage”<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.
Security model
Section titled “Security model”- A response is only accepted if both
event.origin === hostandevent.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.
Where next
Section titled “Where next”Guide: add an action to invoices — a full worked example using this SDK end to end.