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.
Envelope
Section titled “Envelope”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.
Surfaces
Section titled “Surfaces”Your page runs in one of three surfaces, and a few actions only make sense in one of them:
| Surface | What it is | Surface-only actions |
|---|---|---|
| Home page | Your App URL, opened from your app’s row under Apps | navigate.app |
| Action dialog | An *_ACTION extension, opened from a record’s Apps menu | modal.close; resize |
| Block | A *_BLOCK extension, a card on a record page or the dashboard | resize |
Calling a surface-only action anywhere else fails with NOT_PERMITTED.
Actions
Section titled “Actions”| Action | Payload | Result | SDK 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.
context.get result
Section titled “context.get result”{ 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}navigate.host resources
Section titled “navigate.host resources”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.
picker.open resources
Section titled “picker.open resources”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 [].
confirm.open
Section titled “confirm.open”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 and title.set
Section titled “loading.set and title.set”loading.setshows 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 sentready. Send{ loading: false }to clear it.title.setadds 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.
Host-to-app events
Section titled “Host-to-app events”Unsolicited, no response expected, and only sent after your page has called
ready:
| Event | Payload | Meaning |
|---|---|---|
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.
Errors
Section titled “Errors”Every failed request resolves with { ok: false, error: { code, message } }:
| Code | Meaning |
|---|---|
UNKNOWN_ACTION | Not a recognized action name (check for typos or an outdated SDK) |
INVALID_PAYLOAD | Payload failed shape/length validation (see the limits per action above), or the message exceeded 64 KB |
NOT_PERMITTED | Not available here — a picker.open for a resource type outside your installation’s scopes, or a surface-only action from the wrong surface |
CANCELLED | The merchant dismissed a picker.open without choosing. (app.pick() resolves [] instead of rejecting.) |
RATE_LIMITED | More than 5 toast.show requests in 10 seconds, or a confirm.open/picker.open while another dialog from your frame is still open |
INTERNAL | Unexpected host-side failure — safe to retry once |
Where next
Section titled “Where next”App Bridge SDK — install, initialize, and TypeScript types for everything above.