> Documentation index: [Saleor](/llms.txt) · [This section](/developer/llms.txt)
> Source: https://docs.saleor.io/developer/extending/apps/developing-apps/app-sdk/app-bridge

# AppBridge

AppBridge is an interface that connects an App (running inside Dashboard) with the Dashboard itself.

<a id="setup"></a>

## Setup

Create instance of `AppBridge` by running the following code:

```typescript
import { AppBridge } from "@saleor/app-sdk/app-bridge";

const appBridge = new AppBridge(options);
```

Options object is the following:

```typescript
type AppBridgeOptions = {
  saleorApiUrl?: string;
  initialLocale?: LocaleCode;
  autoNotifyReady?: boolean;
  initialTheme?: "dark" | "light";
  /**
   * Forward keyboard shortcuts owned by the Dashboard (e.g. Cmd+K) out of the app iframe.
   * Defaults to true. See "Keyboard shortcuts" below.
   */
  forwardKeyboardShortcuts?: boolean | { shouldForward(event: KeyboardEvent): boolean };
};
```

<a id="appstate"></a>

## AppState

You can get the current state of the app by calling `appBridge.getState()`:

```js
const { token, saleorApiUrl, ready, id } = appBridge.getState();
```

Available state represents `AppBridgeState`:

```typescript
type AppBridgeState = {
  /**
  * JWT token provided by Dashboard. Represents user's session.
  */
  token?: string;
  /**
  * ID of the app
  */
  id: string;
  /**
  *  Flag if app bridge has properly initialized and authorized
  */
  ready: boolean;
  /**
  * Current path on the frontend
  */
  path: string;
  theme: ThemeType;
  locale: LocaleCode; // See src/locales.ts
  /**
   * Full URL including protocol and path where GraphQL API is available
   **/
  saleorApiUrl: string;
  /**
   * Versions of Saleor that app is being installed. Available from 3.15.
   */
  saleorVersion?: string;
  dashboardVersion?: string;
  user?: {
    /**
     * Original permissions of the user that is using the app.
     * *Not* the same permissions as the app itself.
     *
     * Can be used by app to check if user is authorized to perform
     * domain specific actions
     */
    permissions: Permission[];
    email: string;
  };
  /**
   * Permissions of the app itself
   */
  appPermissions?: AppPermission[];
  /**
   * Arbitrary payload passed by the `openPopup` action when this app was opened
   * as a POPUP from one of its own widgets. See "Opening a popup extension".
   */
  appParams?: unknown;
  /**
   * Internal - used by Dashboard and apps to negotiate inner keyboard shortcuts propagation.
   */
  dashboardShortcuts?: DashboardShortcut[];
};
```

<a id="appbridgeprovider"></a>

## AppBridgeProvider

`AppBridgeProvider` and `useAppBridge` hook are exposed from `@saleor/app-sdk`:

```tsx
// app.tsx
import { AppBridgeProvider } from "@saleor/app-sdk/app-bridge";

<AppBridgeProvider>
  <YourApp />
</AppBridgeProvider>;
```

`AppBridgeProvider` can optionally receive AppBridge instance in props, otherwise it will create one automatically.

<a id="useappbridge-hook"></a>

### useAppBridge hook

In components wrapped with `AppBridgeProvider`, you can use the `useAppBridge` hook:

```tsx
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useEffect } from "react";

const MyComponent = () => {
  const { appBridge, appBridgeState } = useAppBridge();

  useEffect(() => {
    appBridge?.dispatch(/* Something */);
  }, [appBridge]);

  return <div>Current locale is: {appBridgeState?.locale}</div>;
};
```

`appBridgeState?` and `appBridge` can be nullish, because they don't exist in the server side context.

<a id="events"></a>

## Events

Events are messages that originate in Saleor Dashboard. AppBridge can subscribe to events and the App can react to them.

<a id="subscribing-to-events"></a>

### Subscribing to events

`subscribe(eventType, callback)` - can be used to listen to particular [event type](#available-event-types). It returns an `unsubscribe` function, which unregister the callback.

Example:

```typescript
const unsubscribe = appBridge.subscribe("handshake", (payload) => {
  setToken(payload.token); // do something with event payload

  const { token } = appState.getState(); // you can also get app's current state here
});

// unsubscribe when callback is no longer needed
unsubscribe();
```

<a id="unsubscribing-multiple-listeners"></a>

### Unsubscribing multiple listeners

`unsubscribeAll(eventType?)` - unregister all callbacks of provided type. If no type was provided, it will remove all event callbacks.

Example:

```js
// unsubscribe from all handshake events
appBridge.unsubscribeAll("handshake");

// unsubscribe from all events
appBridge.unsubscribeAll();
```

<a id="available-event-types"></a>

### Available event types

| Event type | Description |
| --- | --- |
| `handshake` | Fired when iFrame containing the App is initialized or new token is assigned |
| `response` | Fired when Dashboard responds to an Action |
| `redirect` | Fired when Dashboard changes a subpath within the app path |
| `theme` | Fired when Dashboard changes the theme |
| `localeChanged` | Fired when Dashboard changes locale (and passes locale code in payload) |
| `tokenRefresh` | Fired when Dashboard receives a new auth token and passes it to the app |
| `shortcutsChanged` | Internal. Fired when Dashboard changes the set of keyboard shortcuts it owns. Handled automatically by AppBridge |

See [source code for detailed payload](https://github.com/saleor/saleor-app-sdk/blob/5c56cf566d2cc6e4a075c8c619f174fa43aad6c9/src/app-bridge/events.ts)

<a id="actions"></a>

## Actions

Actions expose a high-level API to communicate with Saleor Dashboard. They're exported under an `actions` namespace.

<a id="available-methods"></a>

### Available methods

**`dispatch(action)`** - dispatches an Action. Returns a promise which resolves when action is successfully completed.

Example:

```js
import { actions } from "@saleor/app-sdk/app-bridge";

const handleRedirect = async () => {
  await appBridge.dispatch(actions.Redirect({ to: "/orders" }));

  console.log("Redirect complete!");
};

handleRedirect();
```

<a id="available-actions"></a>

### Available actions

| Action | Arguments | Description |
| --- | --- | --- |
| `Redirect` | `to` (string) - relative (inside Dashboard) or absolute URL path |  |
|  | `newContext` (boolean) - should open in a new browsing context |  |
| `Notification` | `status` (`info` / `success` / `warning` / `error` / undefined) |  |
|  | `title` (string / undefined) - title of the notification |  |
|  | `text` (string / undefined) - content of the notification |  |
|  | `apiMessage` (string / undefined) - error log from api |  |
| `NotifyReady` |  | Inform Dashboard that AppBridge is ready |
| `UpdateRouting` | `newRoute` - current path of App to be set in URL |  |
| `RequestPermissions` (>=3.15) | `permissions` - array of Permissions you want to add, `redirectPath` - value of query param app will receive in `?redirectUrl=` after user approves/rejects permissions | Ask Dashboard to give more permissions to the app. Dashboard will unmount app. After user approves or denies, Dashboard will redirect to `redirectPath`. If operation fails, `?error=REASON` will be appended |
| `PopupClose` (>=3.22.31) |  | Ask Dashboard to close the popup. If the app is running in a popup, Dashboard will close it. If the app is not in a popup, this action does nothing and responds with `ok: true`. |
| `WidgetResize` | `height` - widget content height in pixels (positive, finite) | Ask Dashboard to resize the widget iframe to match the app's content height. Only affects `*_DETAILS_WIDGETS` extensions. See [Sizing sidebar widgets](#sizing-sidebar-widgets) for the recommended helpers. |
| `RefreshEntity` (>=3.23.9) |  | Ask Dashboard to refresh the entity active in the current context (e.g. the currently open Order or Product), without a full page reload. Requires `@saleor/app-sdk` 1.11 or newer and Saleor Dashboard 3.23.9 or newer. |
| `OpenPopup` (>=3.23.19) | `extensionIdentifier` (string) - app-defined `identifier` of the target `POPUP` extension to open. `params` (optional, JSON-serializable) - arbitrary payload forwarded to the opened popup | Ask Dashboard to open one of the **same app's** `POPUP` extensions in full (modal) mode. Intended to be dispatched from a `WIDGET` extension to open a co-located `POPUP`. Requires `@saleor/app-sdk` 1.12 or newer and Saleor 3.23.19 or newer. See [Opening a popup extension](#opening-a-popup-extension). |
| `RedirectToApp` | `appIdentifier` (string) - manifest `id` of the target app. `path` (optional string) - path inside the target app | Ask Dashboard to resolve the URL of another installed app and redirect there. Requires `@saleor/app-sdk` 1.15 or newer. See [Redirecting to another app](#redirecting-to-another-app). |

<a id="redirecting-to-another-app"></a>

## Redirecting to another app

Apps often work together — a checkout app may want to hand the user off to the app that owns tax configuration. Instead of hardcoding the other app's URL (which depends on where it is installed), dispatch `actions.RedirectToApp()` with the target app's manifest `id`. The Dashboard resolves that app's URL and navigates to it.

```ts
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";

const { appBridge } = useAppBridge();

const openTaxesApp = async () => {
  await appBridge.dispatch(
    actions.RedirectToApp({
      appIdentifier: "saleor.app.avatax",
      // Optional: path inside the target app, appended to the resolved URL
      path: "/configuration",
    }),
  );
};
```

`appIdentifier` is required and must be a non-empty string — the SDK throws synchronously otherwise. It is the `id` field from the target app's [manifest](/developer/extending/apps/architecture/manifest.md), not its Saleor object ID.

info

Requires `@saleor/app-sdk` 1.15 or newer. The Dashboard must handle the `redirectToApp` action — older Dashboard versions ignore it. The action fails (resolves with `ok: false`) when no app with the given identifier is installed.

<a id="refreshing-the-active-entity"></a>

## Refreshing the active entity

When an app mutates data that the Dashboard is currently displaying — for example, an order widget that adds a note or changes fulfillment — the Dashboard's view can become stale until the user manually reloads. Dispatch `actions.RefreshEntity()` to ask the Dashboard to re-fetch the entity active in the current context.

```ts
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";

const { appBridge } = useAppBridge();

const handleSaved = async () => {
  // ...perform the mutation that changes the open Order/Product...

  await appBridge.dispatch(actions.RefreshEntity());
};
```

The action takes no arguments: the Dashboard already knows which entity is open in the current context (the mount where your app is rendered, such as `ORDER_DETAILS_WIDGETS` or `PRODUCT_DETAILS_WIDGETS`).

info

Requires `@saleor/app-sdk` 1.11 or newer. Dashboard support for handling this action was added in [Saleor Dashboard 3.23.9](https://github.com/saleor/saleor-dashboard/releases/tag/3.23.9) — coordinate SDK and Dashboard upgrades.

<a id="opening-a-popup-extension"></a>

## Opening a popup extension

A sidebar [`WIDGET`](/developer/extending/apps/extending-dashboard-with-apps.md#widget-target-from-322) is small by design, so complex flows don't fit well inside it. Instead of cramming everything into the widget, you can dispatch `actions.OpenPopup()` to ask the Dashboard to open one of your app's [`POPUP`](/developer/extending/apps/extending-dashboard-with-apps.md#popup-target) extensions in full (modal) mode — a "compact widget that expands into a full view" pattern.

The Dashboard resolves the target popup by its `identifier` and renders it in a modal dialog as a regular app iframe.

<a id="declaring-the-popup-extension"></a>

### Declaring the popup extension

The target `POPUP` must belong to the **same app** and declare a stable `identifier` in the manifest. The `identifier` is what you pass to the action as `extensionIdentifier`:

```json
{
  "extensions": [
    {
      "label": "Order tools",
      "mount": "ORDER_DETAILS_WIDGETS",
      "target": "WIDGET",
      "permissions": ["MANAGE_ORDERS"],
      "url": "/widgets/order"
    },
    {
      "label": "Order tools (full view)",
      "identifier": "order-tools-popup",
      "mount": "ORDER_DETAILS_MORE_ACTIONS",
      "target": "POPUP",
      "permissions": ["MANAGE_ORDERS"],
      "url": "/popups/order-tools"
    }
  ]
}
```

The `identifier` must be unique within a single app (an app cannot reuse the same `identifier` for two of its extensions), but the same value may be used by different apps. See [extension `identifier`](/developer/extending/apps/extending-dashboard-with-apps.md#extension-identifier).

<a id="dispatching-the-action"></a>

### Dispatching the action

Dispatch `actions.OpenPopup()` from the widget, referencing the popup's `identifier`:

```ts
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";

const { appBridge } = useAppBridge();

const handleExpand = async () => {
  await appBridge.dispatch(
    actions.OpenPopup({
      extensionIdentifier: "order-tools-popup",
      // Optional: forward arbitrary, JSON-serializable data to the popup
      params: { mode: "full", focusLineId: "T3JkZXJMaW5lOjE=" },
    }),
  );
};
```

`extensionIdentifier` is required and must be a non-empty string — the SDK throws synchronously otherwise. `params` is optional.

<a id="passing-data-to-the-popup"></a>

### Passing data to the popup

The optional `params` payload is forwarded to the opened popup. The Dashboard serializes it into the popup iframe URL, and the SDK decodes it on load and exposes it on the App Bridge state as `appParams`. Read it via `useAppBridge()` — no URL parsing required:

```tsx
import { useAppBridge } from "@saleor/app-sdk/app-bridge";

const OrderToolsPopup = () => {
  const { appBridgeState } = useAppBridge();
  const params = appBridgeState?.appParams; // { mode: "full", focusLineId: "..." } | undefined

  // ...render based on params
};
```

`appParams` is `undefined` when the app wasn't opened via `OpenPopup`. Because `params` travels in the URL, it must be JSON-serializable and is size-limited (the serialized JSON is capped at ~2 KB). Use it for small references (IDs, a mode flag), not for bulk data — fetch larger data from your backend using those references.

<a id="constraints"></a>

### Constraints

-   **Widget-only.** The action is only honored when dispatched from a `WIDGET` extension. Requests from other targets are rejected.
-   **Same app.** A widget can only open a `POPUP` that belongs to the same app — you cannot open another app's extension.
-   **Co-located.** The target popup must be one of the app's extensions registered on the current page. If no matching `POPUP` with the given `identifier` is found, the action fails (resolves with `ok: false`).

info

Requires `@saleor/app-sdk` 1.12 or newer and Saleor 3.23.19 or newer (the manifest `identifier` field is available from Saleor 3.23.19). The Dashboard must also handle the `openPopup` action — older Dashboard versions ignore it. Coordinate SDK, Core, and Dashboard upgrades.

<a id="sizing-sidebar-widgets"></a>

## Sizing sidebar widgets

Detail-page widgets (`PRODUCT_DETAILS_WIDGETS`, `ORDER_DETAILS_WIDGETS`, and other `*_DETAILS_WIDGETS` mounts) render in the entity sidebar. By default, each widget iframe keeps a fixed height, which can clip content or leave empty space when your UI is shorter or taller than that box.

When you report your widget's content height, the Dashboard resizes the iframe to match. Staff see the full widget without scrolling inside a tiny frame, and multiple app widgets stack naturally in the sidebar without internal scrollbars.

info

Requires `@saleor/app-sdk` 1.9 or newer. Height is sent as the `widgetResize` App Bridge action (`actions.WidgetResize`). The Dashboard must handle this action in its App Bridge message handler — coordinate SDK and Dashboard upgrades.

Reporting height is opt-in. Apps that do not use the helpers below keep the previous fixed height.

<a id="react-apps"></a>

### React apps

Wrap your widget UI in an element with a `ref`, then call `useWidgetAutoResize` inside `AppBridgeProvider` (see [AppBridgeProvider](#appbridgeprovider)). The hook uses `useAppBridge()` internally, measures the root element, and dispatches `WidgetResize` when the layout changes (after App Bridge becomes ready).

```tsx
import { useRef } from "react";
import { useWidgetAutoResize } from "@saleor/app-sdk/app-bridge";

export default function ProductWidget() {
  const rootRef = useRef<HTMLDivElement>(null);

  useWidgetAutoResize(rootRef);

  return (
    <div ref={rootRef}>
      {/* Your widget content */}
    </div>
  );
}
```

Use this on routes served for `target: "WIDGET"` extensions on `*_DETAILS_WIDGETS` mounts. See [Extending Dashboard with Apps](/developer/extending/apps/extending-dashboard-with-apps.md) for manifest setup.

<a id="without-the-hook"></a>

### Without the hook

If you are not using React, or you need to report height after a one-off layout change (for example, when async data finishes loading), pass an `AppBridge` instance to the lower-level helpers (from `useAppBridge()` or your own `new AppBridge()`):

| Helper | Use when |
| --- | --- |
| `reportWidgetHeightFromElement(appBridge, root)` | You have a root element; measure it and dispatch `WidgetResize` in one call |
| `reportWidgetHeight(appBridge, height)` | You already know the height in pixels |

warning

Pass an element that wraps only your widget UI to `reportWidgetHeightFromElement`. Do not measure `document.body` or `document.documentElement` — they match the iframe size, not your content, so reported heights stay too small and widgets stay clipped. The same applies if you pass a height to `reportWidgetHeight` that you calculated from those nodes.

Helpers no-op during SSR. `reportWidgetHeightFromElement` requires a widget root element.

<a id="protocol"></a>

### Protocol

Height is reported through the standard App Bridge channel — `reportWidgetHeight` and `reportWidgetHeightFromElement` call `appBridge.dispatch(actions.WidgetResize({ height }))`, the same mechanism as redirect, notification, and other actions. If you prefer, you can dispatch it yourself:

```ts
import { actions } from "@saleor/app-sdk/app-bridge";

appBridge.dispatch(actions.WidgetResize({ height: 320 })).catch(() => {
  // Best-effort: sizing helpers swallow rejections (timeout or negative response).
});
```

The helpers are the recommended path: they no-op during SSR, skip invalid heights (non-finite or non-positive), and attach a `.catch` so a missing or slow Dashboard response does not surface as an unhandled rejection.

Until the first report, the iframe uses a 200px default height. Reported heights are capped at 5000px.

<a id="keyboard-shortcuts"></a>

## Keyboard shortcuts

Dashboard-level shortcuts (such as `Cmd+K`) would otherwise stop working while the app iframe has focus. The Dashboard and AppBridge negotiate this internally with the `shortcutsChanged` event and the `triggerShortcut` action — no code is required in your app, and there is no `actions.TriggerShortcut()` helper.

Pass `forwardKeyboardShortcuts: false` to `new AppBridge()` to opt out, or `{ shouldForward(event) }` to claim a chord back for the app.

info

Available from `@saleor/app-sdk` 1.16.

<a id="detaching-appbridge"></a>

## Detaching AppBridge

`appBridge.destroy()` detaches the instance from the window: it stops forwarding keyboard shortcuts, stops receiving Dashboard events, and drops all subscribers. It is safe to call more than once.

You rarely need to call it directly — `AppBridgeProvider` destroys the instance it created when it unmounts. An instance passed via the `appBridgeInstance` prop is left alone, because the caller owns its lifecycle.

```ts
const appBridge = new AppBridge();

// later, e.g. when tearing down a non-React host
appBridge.destroy();
```

info

`destroy()` is available from `@saleor/app-sdk` 1.16.
