Skip to content

The widget.zip format ​

A release is one ZIP archive. You upload it to the release, Bito validates it before a single byte is inflated, and stores it only if validation passes. The report comes straight back to you — every failure names the rule it broke.

Directory layout ​

text
widget.zip
├── manifest.json        required, at the archive root
├── script.js            required, at the archive root
├── style.css            optional
├── i18n/
│   ├── uz.json          one file per language declared in the manifest
│   ├── ru.json
│   └── en.json
├── templates/           optional, any name
│   └── card.twig
└── images/              optional
    ├── logo.png
    └── menu.png         optional, left-menu icon

manifest.json and script.js must be at the root, not inside a folder. Zipping a directory rather than its contents is the single most common upload failure.

manifest.json ​

json
{
    "widget": { "interface_version": 2 },
    "bito": {
        "schema_version": 1,
        "locations": {
            "surfaces": ["customer_profile", "card_phone", "list:customers"],
            "persistent": false
        },
        "network": {
            "connect_origins": ["https://api.example.com", "wss://events.example.com"]
        },
        "pipeline_endpoint": "https://api.example.com/bito/pipeline",
        "left_menu": {
            "widget_page": {
                "title_key": "app_title",
                "icon": "images/menu.png",
                "submenu": {
                    "deals": { "title_key": "deals" },
                    "reports": { "title_key": "reports" }
                }
            },
            "saleMain": {
                "submenu": { "order_sync": { "title_key": "order_sync" } }
            }
        }
    },
    "languages": ["uz", "ru", "en"],
    "default_language": "uz"
}
FieldRequiredRules
widget.interface_versionYesMust be a number. 2 is the current in-page runtime.
bito.schema_versionYesMust be a number. 1 is current.
languagesYesArray of language codes. Every one needs a matching i18n/<lang>.json in the archive.
default_languageYesString. Used when the viewer's language is not in languages.
bito.locations.surfacesNoArray of mount surfaces. Each entry must be a named surface, list:<page_name>, or global. Duplicates are rejected.
bito.locations.persistentNoBoolean. true opts into the always-mounted persistent slot. Listing global in surfaces opts in too — either form counts.
bito.network.connect_originsNoArray of exact origins. Disclosure only — see below.
bito.pipeline_endpointNoExact https:// URL. Where the sale-order pipeline widget_action posts.
bito.left_menuNoPages in the admin panel's left menu: the widget's own section and/or submenu pages in existing modules. Requires the app's widget code; see left menu.

Anything that does not parse, or does not match this shape, is a validation failure with a message, not a server error.

connect_origins is disclosure, never enforcement ​

DANGER

bito.network.connect_origins is shown to the tenant on the consent screen and never technically enforced. Nothing stops your widget — or a compromised version of it — from talking to a host that is not in this list.

It cannot be enforced. A page-level CSP applies to the whole document and cannot be scoped to one script, and your code runs in the host page. Treat this field as a declaration you are making to the tenant, not as a restriction Bito applies to you. Do not describe it to your users as a sandbox.

The shape is validated at upload, so the consent screen shows a clean list:

  • exact https:// or wss:// origin only;
  • no wildcards anywhere (* in the string is an instant reject);
  • no bare scheme (https:);
  • no path, query or fragment, and no trailing slash — the string must round-trip as its own origin.

pipeline_endpoint ​

If a tenant configures a sale-order pipeline stage action of type widget_action against your app, Bito resolves the endpoint from your consented release's manifest — the tenant never types a URL. It POSTs:

json
{
    "event": "pipeline_action",
    "db_name": "tenant_db",
    "sale_order_id": "6712f0a9c4d1e87b3a0f1122",
    "organization_id": "6712f0a9c4d1e87b3a0f1130",
    "state_id": "6712f0a9c4d1e87b3a0f1140",
    "client_id": "69c50a2d50f7b939923a469b"
}

At upload the only check is the shape: it must be an exact https:// URL. At dispatch the destination is re-checked — non-HTTPS, loopback, private-range and link-local addresses are refused, and the call has a hard timeout.

Left menu ​

bito.left_menu gives the installed widget its own pages in the admin panel's left menu. It is modelled on amoCRM's left_menu and its initMenuPage callback. Bito draws the menu items and the page title; the page content is whatever your widget renders. Every such page mounts on the widget_page surface.

bito.left_menu replaces bito.settings_page and bito.menu. Both are removed: a manifest that still declares either one is rejected with a message pointing at left_menu.

Each key of left_menu is a block — widget_page or a module key:

BlockFieldsRules
widget_pagetitle_key (required), icon, submenuThe widget's own top-level section. Without submenu it is one page; submenu holds at most 10 items.
module keysubmenu (required)1 to 3 pages added to that module's Extensions group. title_key and icon are rejected — the module keeps its own title and icon.

A module key must be one of the following; any other key is rejected. The prefix is the start of the page URL.

Module keyURL prefix
saleMain/sale
customerMain/customer
distributionMain/distribution
logisticsOrder/logistics
supplyMain/supply
warehouseMain/warehouse
financeMain/finance
hrMain/hr
integrationMain/integration
manufacturingMain/manufacturing
referenceMain/reference
toolsMain/tools

submenu is an object: the key is the subitem code, the value is { "title_key": "..." }. A subitem code must match ^[a-z][a-z0-9_]{1,31}$ — a lowercase Latin letter, then 1 to 31 lowercase letters, digits or underscores. Items are shown in the order you declare them.

Labels ​

Every title_key must exist in i18n/<default_language>.json, otherwise the upload is rejected. Labels are read from the archive at upload time, so the menu text cannot drift from the reviewed package. At display time each label is taken from the viewer's language bundle; a key missing there falls back to default_language.

Icon ​

icon is optional and allowed only on widget_page. The upload is rejected unless it is:

  • a path to an archive entry under images/ ending in .png;
  • a PNG file of at most 16 KB (16,384 bytes);
  • square, 48 to 128 px on each side;
  • with an alpha channel (PNG colour type 4, grayscale + alpha, or 6, RGBA).

Bito uses only the icon's shape. The PNG's alpha channel is applied as a mask and filled with the menu's current theme colour, so the icon follows light and dark mode, hover and the active state; your own colours are never shown. Draw a single glyph on a transparent background. Without an icon the item shows Bito's standard extension glyph.

Widget code and URLs ​

{widget_code} in menu URLs is the app's widget code — the system_id you choose and claim in My apps (see the FAQ). An app without one (for example a private widget) uses its client_id instead: /widget/{client_id}/{subitem}.

PageURLlocation
widget_page without submenu/widget/{widget_code}widget_page
widget_page submenu item/widget/{widget_code}/{subitem}widget_page
Module submenu item{prefix}/widget/{widget_code}/{subitem}, e.g. /sale/widget/acme_crm/order_syncthe module key, e.g. saleMain

A URL for a block or subitem the manifest does not declare shows a "not available" message instead of your widget. A module page always needs a subitem code. Your widget learns which page is open from initMenuPage and bito.context.menu.

Visibility and access ​

  • The menu shows the widget_page sections of at most five widgets; further widgets' sections are not shown. Module items are not affected by this cap.
  • Menu items and pages are shown to every user of the tenant; Bito does not filter them by role. Your widget decides what each user may see and do.
  • Bito RBAC still applies to every bito.ajax call, so a page cannot read or change anything beyond the signed-in user's own rights.
  • Items appear once the app is installed. Your code runs on the page only after the tenant accepts consent for the release, as on every surface.

Differences from amoCRM ​

  • The label field is title_key (an i18n key).
  • There is no sort: declaration order is display order.
  • There is no is_hidden.
  • The icon is a PNG used as a shape and painted in Bito's theme colours, not an SVG shown as drawn.
  • Module keys are Bito's own, listed above.

The i18n bundles ​

A flat JSON object of key → string. Bito resolves one bundle server-side at mount: the viewer's language if the manifest declares it, otherwise default_language. Your widget receives only that bundle, through bito.i18n().

json
{
    "title": "Call history",
    "empty": "Nothing yet",
    "greeting": "Hello, {{name}}"
}

A bundle that is not valid JSON is logged and treated as empty — your widget still mounts, with every key falling back to itself.

templates/ ​

Every file under templates/ is inlined into the mount payload and addressed by its path after the templates/ prefix — templates/card.twig is card.twig. Rendering is plain double-brace substitution, nothing more: no loops, no conditionals, no filters. The .twig extension is allowed for familiarity; it is not a Twig engine.

style.css ​

If present, it is injected into the host document as a <link> while your widget is mounted, and removed on teardown. It is not scoped — a bare .card { ... } rule restyles the admin panel. Namespace every selector.

script.js ​

The AMD module. See the CustomWidget lifecycle.

Limits ​

Every one of these is checked against the ZIP's central directory before anything is decompressed, so a zip bomb is rejected without being inflated.

LimitValue
Compressed archive10 MiB (10,485,760 bytes)
Total uncompressed50 MiB (52,428,800 bytes)
Single file, uncompressed5 MiB (5,242,880 bytes)
Entry count1000
Expansion ratio, per entry100:1

What gets an entry rejected ​

RuleDetail
Absolute pathsA leading /, or any absolute path
TraversalAny .. path segment
Backslashes\ anywhere in the entry name
SymlinksRejected unconditionally, whatever host the archive claims to come from
Encrypted entriesPassword-protected entries are refused
Nested archives.zip .rar .7z .tar .gz .tgz .bz2
Executable bitAny of the Unix 0o111 mode bits set
Duplicate pathsThe same entry name twice
Unknown extensionAnything outside the allowlist below
Bad text encodingA text file that is not valid UTF-8
Image that is not onePNG, JPEG and WebP are verified by magic bytes; the extension is not believed

Allowed extensions: .js .json .css .html .twig .txt .png .jpg .jpeg .webp .woff2

Content types are assigned server-side from that extension, never from anything the archive or the upload declares.

The validation report ​

upload answers with the report, and it is stored on the release whether it passed or failed:

json
{
    "ok": false,
    "errors": [
        "Extension not allowed: vendor/lib.map",
        "Missing i18n/ru.json declared in manifest.json"
    ],
    "entries": 14,
    "uncompressed_bytes": 189342
}

When ok is true the report also carries the parsed manifest. A release cannot be submitted for review until its stored report says ok.

Bito 2.0 Integration API