Skip to content

Mount surfaces ​

A surface is a place in the admin panel where your widget can appear. You declare the ones you want in bito.locations.surfaces; the surface your code is actually running on is in bito.context.surface.

json
{
    "bito": {
        "locations": {
            "surfaces": ["customer_profile", "card_phone", "list:customers", "global"],
            "persistent": true
        }
    }
}

An entry must be one of the named surfaces below, a list:<page_name> string, or global. Anything else, or a duplicate, fails validation at upload.

The five profile tabs ​

SurfaceScreen
customer_profileCustomer profile
supplier_profileSupplier profile
employee_profileEmployee profile
natural_person_profileNatural-person profile
legal_person_profileLegal-entity profile

These mount directly, as a tab in the profile's own tab strip — there is no menu the user picks you from first. The tab takes its place from the tenant's saved profile-section order, and a tenant can drag it like any other section. bito.context.entity_id is the id of the profile being viewed.

widget_page ​

The surface of left-menu pages. You do not list it in bito.locations.surfaces and do not add it to an action menu yourself: declaring bito.left_menu is what creates the pages. Their URLs are /widget/{widget_code}[/{subitem}] and {module prefix}/widget/{widget_code}/{subitem} — built from your app's widget code, or from client_id when it has none.

Bito mounts the widget with bito.context.surface set to widget_page and the open page in bito.context.menu: { location, item_code, subitem_code }. After bind_actions it calls initMenuPage with the same object, and calls it again, without a remount, when the user switches to another subitem of the same block.

The page opens only for an installed, consented release, and the mount is refused for a release whose manifest declares no left_menu. Menu labels are resolved from the archive's i18n bundles at upload time, so the navigation text cannot drift from the reviewed package.

sale_order_card ​

The sale-order kanban card's tab strip. Same mount contract as a profile tab, with bito.context.entity_id set to the sale order. The card asks which widgets declare this surface before it renders its tabs, so your entry appears alongside the built-in ones.

The tenant can drag these tabs into any order they like, with one exception: the card's first tab is pinned, because it carries the order's own identity fields. The strip refuses to drag it and refuses to drop anything above it — so your tab can land anywhere from second place down.

card_phone ​

Not a tab. A widget declaring card_phone is offered as an entry inside the phone-number click menu on a card, next to Copy and Call. Nothing mounts until the user picks it; then your widget mounts in a modal, with the clicked number in bito.context.phone and the card's record in bito.context.entity_id.

This is the surface for telephony and messaging apps: a click-to-dial integration does not need a tab of its own.

It is not a shortcut past anything. It goes through the same mount endpoint and the same per-release consent gate as every other surface — it just mounts later, when the user asks for it.

The list: surfaces ​

A list: surface puts your widget in the actions menu of a list page. page_name is the admin panel's own name for that table, and must match list: followed by a lowercase letter and then lowercase letters, digits or underscores — list:customers, list:sale_order.

The user selects rows, opens the menu, and picks your entry. You do not get an entity_id; a list action has no single record. You get bito.context.selection instead:

js
var sel = bito.context.selection
// { ids: ["6712...", "6713..."], isAll: false, page_name: "customers" }

isAll means the user selected everything matching the current filter, not just the ids in ids.

global ​

A persistent, always-mounted slot. It lives outside the router, so it survives navigation and mounts once per browser tab, and it is skipped on chrome-less screens — login, print, embed and onboarding pages.

Two things to know:

TIP

Either form opts in. persistent: true and listing global in surfaces are both read as the persistent opt-in, by one shared predicate that both the global list and the cap below use — so there is no way for the two to disagree. Prefer persistent: true: it is the explicit, documented form, and it says what it means.

A global widget runs headless: no card, no spinner, no error alert, no settings icon. Failure containment still applies — a widget that throws twice is still disabled — it just fails silently instead of pushing an error card into every screen. Render your own UI if you want to be seen.

A tenant may have at most five persistent widgets enabled at a time. The cap is enforced server-side at the moment consent is accepted, which is the moment a widget starts running. A sixth is refused with a specific error, not silently dropped.

What has to be true before anything mounts ​

A mount is refused unless all of these hold:

  • your app is installed for that tenant;
  • the release is approved (or you are the owning active developer previewing your own draft);
  • its assets have been extracted;
  • the tenant has accepted consent for that exact release.

Until consent is accepted the mount response carries no script URL at all, so an un-consented widget cannot execute — it can only render the consent prompt.

Previewing before approval ​

An active developer can mount their own draft release in their own tenant, through a separate preview endpoint that takes the release id explicitly. The production mount path cannot take a release id at all, so there is no way to preview through it by accident. In preview there are no installation settings — bito.settings.get() is empty.

Bito 2.0 Integration API