UI extensions
An extension registers a path on your App URL’s origin against a
target — one of 15 slots in the OneBooks UI. Manage extensions on the
developer console’s Embedding tab with Add extension (org admins only;
there’s no CLI command for this today). Each is
{ target, label, path, height?, isActive, position }, up to 20 per app.
Your app needs an App URL first.
Actions vs. blocks
Section titled “Actions vs. blocks”| Kind | Renders as | Size |
|---|---|---|
| Action | An entry in the record’s Apps menu; opens your page in a dialog framed by OneBooks’ chrome bar, which the merchant closes with its × button, Escape or a click outside | Starts 480 px tall; your page can change it with app.resize() / app.autoResize() (60–1600 px), but the dialog never grows past the viewport — a taller page scrolls under the fixed chrome |
| Block | An inline card on the record page — or, for DASHBOARD_BLOCK, in a From your apps section on the dashboard | Starts at the height you register — 120–800 px, 240 px if you leave it empty — and your page can change it with app.resize() / app.autoResize() (60–1600 px) |
Blocks load lazily as the merchant scrolls to them, so keep each one cheap to render. Size the initial height to your content’s usual size — a block that starts far too short or tall jumps when it first resizes.
app.autoResize() measures your page’s <body> content, so a block or dialog
shrinks as well as grows with it — as long as <body> keeps its natural
height: don’t give it a fixed or 100% height (see
Sizing a block or dialog).
When an action dialog on a record closes, OneBooks reloads every block and the App data card on that record, so a block always shows what an action just changed.
A block card is headed by your extension’s label, with one light line under it naming who provides it — Provided by <your app> (<your organization>), not OneBooks, using your registered names. Your page can’t change or hide that line.
The 15 targets
Section titled “The 15 targets”| Target | Kind | Renders on | Installation needs |
|---|---|---|---|
DASHBOARD_BLOCK | Block | The merchant’s dashboard | — |
INVOICE_ACTION | Action | Invoice detail — Apps menu | invoices:read |
INVOICE_BLOCK | Block | Invoice detail — inline card | invoices:read |
QUOTE_ACTION | Action | Quote detail — Apps menu | quotes:read |
QUOTE_BLOCK | Block | Quote detail — inline card | quotes:read |
CUSTOMER_ACTION | Action | Customer detail — Apps menu | customers:read |
CUSTOMER_BLOCK | Block | Customer detail — inline card | customers:read |
SUPPLIER_ACTION | Action | Supplier detail — Apps menu | suppliers:read |
SUPPLIER_BLOCK | Block | Supplier detail — inline card | suppliers:read |
PURCHASE_ACTION | Action | Purchase detail — Apps menu | purchases:read |
PURCHASE_BLOCK | Block | Purchase detail — inline card | purchases:read |
SALES_RETURN_ACTION | Action | Sales return (credit note) detail — Apps menu | sales-returns:read |
SALES_RETURN_BLOCK | Block | Sales return detail — inline card | sales-returns:read |
PURCHASE_RETURN_ACTION | Action | Purchase return (debit note) detail — Apps menu | purchase-returns:read |
PURCHASE_RETURN_BLOCK | Block | Purchase return detail — inline card | purchase-returns:read |
Scopes gate where an extension appears. An extension on a record renders only for businesses whose installation of your app holds that record type’s read scope (the last column — the same mapping App data uses); the dashboard block needs none.
The console enforces the other half when you save. Creating an extension on a
record target, moving one to another target, or switching one back on fails
with 400 EXTENSION_SCOPE_REQUIRED unless your app requests that target’s
read scope:
A INVOICE_ACTION extension requires the "invoices:read" scope. Add it to your app's scopes first.Add the scope on the OAuth tab first. An extension whose scope your app has since dropped can still be switched off, relabelled or reordered, and its path or height edited — it simply never renders.
An extension also renders only while your app is active, installed in that business and not suspended, and only while its URL stays on your App URL’s origin (see Paths) — and an Apps menu only appears on a record once some installed app has an action for it.
path is resolved against your App URL’s origin (not appended to the App
URL’s own path) and must stay on it. Saving a path that breaks a rule fails
with a 400:
| Rule | code |
|---|---|
Required, and starts with / | EXTENSION_PATH_INVALID |
| No backslashes, spaces or control characters | EXTENSION_PATH_INVALID (path must not contain backslashes, spaces or control characters.) |
| Resolves to a page on your App URL’s own origin | EXTENSION_PATH_INVALID (path must resolve to a page on the app’s own origin.) |
| At most 500 characters | EXTENSION_PATH_TOO_LONG |
Starts with a single / — never // | EXTENSION_PATH_PROTOCOL_RELATIVE |
No . or .. segments | EXTENSION_PATH_TRAVERSAL |
No scheme (/javascript:…) | EXTENSION_PATH_HAS_SCHEME |
OneBooks checks the origin again every time it renders an extension, and leaves out any whose URL would resolve anywhere else.
Use a query string if one page serves several extensions — the host adds its
own target, resourceType and resourceId parameters too (see
Embedded apps), so you
can also branch on those.
Labels, in five languages
Section titled “Labels, in five languages”label is a { en, ar?, es?, fr?, pt? } object of at most 40 characters per
language — the same localized-text shape used across the platform (listings,
app data field names). English is required; the others fall back to
English automatically when omitted, so you can ship with English only and add
translations later without breaking anything:
{ "target": "INVOICE_ACTION", "label": { "en": "Sync to Acme", "ar": "مزامنة مع Acme", "es": "Sincronizar con Acme", "fr": "Synchroniser avec Acme", "pt": "Sincronizar com a Acme" }, "path": "/extensions/invoice-action"}For an action, the label is the Apps menu entry; for a block, it heads the card.
Order and switching off
Section titled “Order and switching off”position orders your extensions on the same target (lower first).
isActive: false hides an extension everywhere without deleting it — handy
while you fix a broken page.
Where next
Section titled “Where next”Embedded apps for the iframe contract every extension
page follows, and the
end-to-end guide for building an INVOICE_ACTION
from scratch.