Skip to content

App Bridge reference

App Bridge is how your embedded page talks to the OneBooks host it’s rendered inside — a postMessage protocol under the hood, wrapped by the App Bridge SDK so you rarely touch the raw messages directly. This page documents the wire protocol (v1) and, for each action, the SDK method that sends it.

Your page sends requests to window.parent with targetOrigin set to the host URL param (never '*'):

// app → host
{ type: 'onebooks:request', v: 1, id: string, action: string, payload?: object }
// host → app
{ type: 'onebooks:response', v: 1, id, ok: true, result: object }
| { type: 'onebooks:response', v: 1, id, ok: false, error: { code: string, message: string } }
// host → app, unsolicited
{ type: 'onebooks:event', v: 1, name: 'theme.changed' | 'locale.changed', payload: object }

The host validates every inbound message strictly: event.source must be your frame’s own contentWindow, event.origin must equal the origin of the URL OneBooks loaded into your frame (your App URL’s origin), id must be a string of 1–64 characters, the serialized message must be ≤ 64 KB, and an unknown action is rejected with UNKNOWN_ACTION rather than silently ignored. Every reply is posted back to your origin only. The SDK handles request/response correlation (matching ids) and timeouts for you.

Your page runs in one of three surfaces, and a few actions only make sense in one of them:

SurfaceWhat it isSurface-only actions
Home pageYour App URL, opened from your app’s row under Appsnavigate.app
Action dialogAn *_ACTION extension, opened from a record’s Apps menumodal.close; resize
BlockA *_BLOCK extension, a card on a record page or the dashboardresize

Calling a surface-only action anywhere else fails with NOT_PERMITTED.

ActionPayloadResultSDK method
ready{ sdkVersion }{ hostVersion: 1 }app.ready() — call it first; nothing sends it for you
sessionToken.get–{ token, expiresAt }app.sessionToken() (resolves the token string — prefer app.fetch() for calling your own backend)
context.get–{ locale, dir, theme, business: { name, country, currency }, user: { name }, target }app.context()
toast.show{ message, tone?: 'info' | 'success' | 'error' } — message non-empty, cut at 200 characters{}app.toast(message, { tone })
navigate.host{ resource, id? } — id matches ^[A-Za-z0-9_-]{1,64}${}app.navigate.host({ resource, id? })
navigate.app{ path } — starts with a single /, ≤ 2,048 characters, no \ or ://, no . or .. segments (percent-encoded ones included){}app.navigate.app(path) — home page only
modal.close{ result? }{}app.close(result?) — action dialogs only
resize{ height } (a finite number, clamped 60–1600){}app.resize(height), or app.autoResize() — blocks and action dialogs only
confirm.open{ title 1–80, message 1–500, confirmLabel? ≤ 30, destructive? }{ confirmed: boolean }app.confirm({ title, message, ... }) — resolves the boolean directly
picker.open{ resource, multiple? }{ selection: [{ id, label }] }app.pick({ resource, multiple }) — resolves the selection array directly, or [] if the merchant cancels
loading.set{ loading: boolean }{}app.loading(boolean) — see below
title.set{ title 1–80 }{}app.setTitle(title) — see below

Until your page sends ready, OneBooks shows its own loading state over your frame; after 15 seconds without it, the frame is replaced by This app did not load in time and a Retry button. (Blocks load lazily, so a block’s 15 seconds start only once its frame has actually loaded — a block below the fold doesn’t time out before the merchant scrolls to it.) The first ready also opens the event channel: the host sends theme.changed and locale.changed only after it. Send ready as soon as your page can answer messages — before any slow API call.

{
locale: string; // "en" | "ar" | "es" | "fr" | "pt"
dir: 'ltr' | 'rtl';
theme: 'light' | 'dark';
business: { name: string; country: string; currency: string };
user: { name: string };
target: {
type: string; // the extension target, e.g. "INVOICE_ACTION"
resourceType?: string; // e.g. "INVOICE" — absent for DASHBOARD_BLOCK
resourceId?: string; // absent for DASHBOARD_BLOCK
} | null; // null on your app's home page
}

invoice | quote | customer | supplier | purchase | sales-return | purchase-return take an id and open that record; item-list and dashboard take none. Navigating the host from an action dialog closes the dialog.

customer | supplier | item | invoice — each needs your installation to hold the matching read scope (customers:read, suppliers:read, items:read, invoices:read); without it the request fails with NOT_PERMITTED. The merchant searches in a host-rendered dialog; dismissing it answers with the CANCELLED error, which app.pick() turns into [].

The host shows your title and message in its own dialog, with a note naming your app as the one asking (Requested by <your app>). Confirming answers { confirmed: true }; cancelling or dismissing answers { confirmed: false } — never an error.

A frame can have one host dialog open at a time: a confirm.open or picker.open sent while another one from your frame is still on screen is rejected with RATE_LIMITED rather than replacing it. Wait for the first to settle.

  • loading.set shows a thin progress bar across the top of your app’s area — on the home page, in an action dialog and on a block — announced to screen readers as <your app> is working…. It only appears after your page has sent ready. Send { loading: false } to clear it.
  • title.set adds a muted secondary heading next to your app’s name in the chrome of the home page and action dialogs — <your app> · <your title>. It never replaces the name: the provided by … not OneBooks note, a confirmation’s Requested by, and the frame’s accessible name always use the name your app is registered under. It has no visible effect on a block.

Unsolicited, no response expected, and only sent after your page has called ready:

EventPayloadMeaning
theme.changed{ theme: 'light' | 'dark' }Merchant toggled their theme
locale.changed{ locale, dir }Merchant switched language

OneBooks doesn’t reload your frame when the merchant switches language or theme — the locale in your URL is only the language you start in. Listen for these events and re-render in place.

Every failed request resolves with { ok: false, error: { code, message } }:

CodeMeaning
UNKNOWN_ACTIONNot a recognized action name (check for typos or an outdated SDK)
INVALID_PAYLOADPayload failed shape/length validation (see the limits per action above), or the message exceeded 64 KB
NOT_PERMITTEDNot available here — a picker.open for a resource type outside your installation’s scopes, or a surface-only action from the wrong surface
CANCELLEDThe merchant dismissed a picker.open without choosing. (app.pick() resolves [] instead of rejecting.)
RATE_LIMITEDMore than 5 toast.show requests in 10 seconds, or a confirm.open/picker.open while another dialog from your frame is still open
INTERNALUnexpected host-side failure — safe to retry once

App Bridge SDK — install, initialize, and TypeScript types for everything above.