Business NXT Apps /businessnxtapi/apps section Build apps that embed inside the Business NXT UI and run authenticated GraphQL - without ever handling a bearer token. 2026-05-28T14:06:30+03:00 # Business NXT Apps Build apps that embed inside the Business NXT UI and run authenticated GraphQL - without ever handling a bearer token. ## Introduction A **Business NXT App** is a web app of your own, embedded inside the Business NXT UI - this is how you build apps for Business NXT. It is placed as an element in a layout and loaded inside an iframe. From there your app can react to what the user selects, read and write data over GraphQL, and ask Business NXT to refresh data or open dialogs - all in the context of the signed-in user and the company they are working in. Your app does not deal with authentication. Business NXT loads it, holds the user's access token, attaches it to every GraphQL request, and relays messages to and from your app. Your app never sees the bearer token. ## How it fits together Business NXT loads your app in an iframe and talks to it with `postMessage` in both directions: - Your app posts **up** to `window.parent` (Business NXT). - Business NXT posts **down** to your app. When your app sends a GraphQL request, Business NXT attaches the user's token, runs the query, and posts the result back. Your app never calls the GraphQL API directly and never handles the token. ## Identity in the URL When your app loads, it sees the current context as query parameters on its own URL, for example: ``` https://your-app.example.com/?customerNo=1182082&companyNo=5051201&layoutNo=50466&elementNo=1&sub=&user=...&locale=nb-NO ``` Business NXT does not load your origin directly: the layout element first loads a proxy frame at `https://apps.business.visma.net/` that shows the consent gate, then embeds your origin in a nested iframe and forwards the query string verbatim. From your code's point of view this is transparent - `location.search` carries the parameters below, and `window.parent` is the proxy frame the SDK talks to. | Parameter | Description | | --------- | ----------- | | `companyNo` | The Business NXT company the user is working in. Use it as the `useCompany(no: ...)` argument in your queries. | | `customerNo` | The Visma customer (tenant) number. | | `layoutNo` | The layout the app is mounted in. | | `elementNo` | The App element's identifier within that layout. | | `sub` | The signed-in user's Visma Connect subject id. Treat it as a hint only - Business NXT re-derives identity from the verified token. | | `user` | The user's login name, for display. | | `locale` | The active UI locale (e.g. `nb-NO`). Use it to localize your app. | Read them with `new URLSearchParams(location.search)`. ## GraphQL This is the core integration point. Your app posts a `graphql-request` to the parent and receives a `graphql-response`: ```jsonc { title = "Request (app → Business NXT)" } { "messageType": "graphql-request", "id": "0f8c3bba-7e13-4227-84dc-3dc32970c15e", "query": "query GetAssociates($companyNo: Int!) { useCompany(no: $companyNo) { associate(first: 10) { items { associateNo name } } } }", "variables": { "companyNo": 5051201 }, "operationName": "GetAssociates" } ``` ```jsonc { title = "Response (Business NXT → app)" } { "messageType": "graphql-response", "id": "0f8c3bba-7e13-4227-84dc-3dc32970c15e", "data": { "useCompany": { "associate": { "items": [{ "associateNo": 7, "name": "Acme AS" }] } } } } ``` The `id` correlates a response with its request - generate a fresh one (`crypto.randomUUID()`) per call. On failure the response carries an `errors` array instead of `data`: ```jsonc { title = "Error response" } { "messageType": "graphql-response", "id": "0f8c3bba-7e13-4227-84dc-3dc32970c15e", "errors": [{ "message": "GraphQL request failed with status 400" }] } ``` The query shape is identical to the rest of the API - see [Getting started](/businessnxtapi/gettingstarted/) and the [GraphQL Schema](/businessnxtapi/schema/). The only difference is transport: instead of a `POST` with your own token, you post a message and Business NXT adds the token for you. See the [quickstart sample](/businessnxtapi/apps/sample/) for a small Vite app that runs this query end to end. ## Messages to and from Business NXT Beyond GraphQL, your app and Business NXT exchange a small set of typed messages. Your app posts **request** messages up; Business NXT posts **state** messages down. | Message (app → Business NXT) | Purpose | | -------------------- | ------- | | `selection-state-request` | Ask for the current row selection. Set `toParent: true` for the joined parent table; defaults to the focused table. | | `refresh-data-request` | Ask Business NXT to refresh a table (`{ table: "order" }`) after your app changed data. | | `edit-session-request` | Ask whether an edit session is active. | | `dialog-request` | Ask Business NXT to open a modal dialog with your markdown `content` and one or two buttons. | | Message (Business NXT → app) | Purpose | | -------------------- | ------- | | `selection-state` | The user changed the row selection. Carries `table`, `focusedRow`, `selectedRows`, `fromParent`. | | `edit-session` | An edit session started, was saved or discarded. | | `dialog-response` | The user answered a dialog you opened (`result` is `Closed` `0`, `Primary` `1`, or `Secondary` `2`). | | `ok` | Acknowledges a request that has no data reply (e.g. `refresh-data-request`). | | `error` | A request could not be served (`reason: "schema-violation"` or `"overlay-already-open"`). | ## Recommended: the app-messaging SDK You can talk the protocol directly with `postMessage`, but for production apps we recommend the official helper library, which wraps the contract, handles timeouts, and ships TypeScript types - the [quickstart sample](/businessnxtapi/apps/sample/) uses it: ```ts import { ExecuteGraphQL } from "@business-nxt/app-messaging"; const data = await ExecuteGraphQL( /* GraphQL */ `query GetAssociates($companyNo: Int!) { useCompany(no: $companyNo) { associate(first: 10) { items { associateNo name } } } }`, { companyNo: 5051201 }, { operationName: "GetAssociates", timeout: 30000 }, ); ``` `ExecuteGraphQL` resolves with `data` and rejects with `GraphQLRequestError` when the response carries `errors`. There is also a React adapter, `@business-nxt/app-messaging-react`. ## Registering your app Before Business NXT can load your app, register it in the Partner Admin at [`https://admin-partner.business.visma.net/apps`](https://admin-partner.business.visma.net/apps). Registration captures the HTTPS origin Business NXT loads the app from, its name and description, and a support contact point. Each registered app gets an **app id** of the form `app://` (for example `app://qhl7nzwt`). Paste this URI into an **App** element in a Business NXT layout. There are two ways to add that element: - **Independent.** In design mode, add a new element of type **App** to the layout. There is no parent table, so `selection-state` always carries `fromParent: false`. - **Joined to a table.** In design mode, open a table's context menu and choose **Design → Visualise → App**. The app is bound to that parent table; `selection-state` carries `fromParent: true` when the selection came from the bound table and `fromParent: false` when it came from any other focused table. `selection-state-request` with `toParent: true` returns the parent's current selection. ### Approval A registered app is reviewed by Visma before it can be used. The team checks the origin and that the app behaves as described. The app cannot be loaded until the review has passed. ## Consent and activation The first time a user opens your app, Business NXT shows a consent gate describing the app and the permissions it requests. Nothing loads until the user activates it. Activation is recorded per user, customer and company, so the gate only appears once per context. This is handled entirely by Business NXT - your app does not implement any of it and is only loaded after the user has activated it. ## Security notes - **HTTPS required.** Business NXT runs over HTTPS and embeds your app in an iframe, so your origin must be HTTPS too - an HTTP child is blocked as mixed content. This also satisfies the secure-context requirement for `crypto.randomUUID()` (used for message ids). - **The token stays with Business NXT.** Your app cannot read the user's access token, by design. Run all data access through the GraphQL relay. Quickstart: a Vite + SDK app /businessnxtapi/apps/sample page A small Business NXT App scaffolded with Vite and the @business-nxt/app-messaging SDK - it runs an authenticated GraphQL query and reads the user's selection. 2026-05-28T09:34:55+02:00 # Quickstart: a Vite + SDK app A small Business NXT App scaffolded with Vite and the @business-nxt/app-messaging SDK - it runs an authenticated GraphQL query and reads the user's selection. The quickest way to a working Business NXT App: a Vite project plus the [`@business-nxt/app-messaging`](/businessnxtapi/apps/) SDK. The bundler inlines the SDK, so there is no CDN to allow-list in your CSP. The app reads the current company from the URL, runs a GraphQL query, and asks Business NXT for the current selection. It also pulls in [`@vsn-ux/gaia-styles`](https://www.npmjs.com/package/@vsn-ux/gaia-styles), the Visma design system, so the app looks at home inside Business NXT - import the stylesheet once and apply `ga-*` classes to your elements. ## Scaffold ```bash npm create vite@latest my-app -- --template vanilla-ts cd my-app npm install @business-nxt/app-messaging @vsn-ux/gaia-styles npm install -D @vitejs/plugin-basic-ssl ``` ## vite.config.ts Business NXT serves over HTTPS and loads your app in an iframe, so the dev server must also be HTTPS - an HTTP child is blocked as mixed content. ```ts { title = "vite.config.ts" } import { defineConfig } from "vite"; import basicSsl from "@vitejs/plugin-basic-ssl"; export default defineConfig({ plugins: [basicSsl()], }); ``` The plugin attaches its generated certificate to `server.https` for you, so no extra `server` config is needed. The plugin generates a self-signed certificate on first run. Visit `https://localhost:5173/` once in your browser and accept the warning so the iframe can load. ## index.html ```html { title = "index.html" } Business NXT App sample

Associates

``` ## src/main.ts ```ts { title = "src/main.ts" } import "@vsn-ux/gaia-styles/all.css"; import { ExecuteGraphQL, GraphQLRequestError, SendMessage, } from "@business-nxt/app-messaging"; // Business NXT passes the current context in the iframe URL. const companyNo = Number(new URLSearchParams(location.search).get("companyNo")); const status = document.getElementById("status")!; const list = document.getElementById("list")!; const GetAssociates = /* GraphQL */ ` query GetAssociates($companyNo: Int!, $first: Int!) { useCompany(no: $companyNo) { associate(first: $first) { items { associateNo name } } } } `; interface Associates { useCompany: { associate: { items: { associateNo: number; name: string }[] } }; } document.getElementById("load")!.addEventListener("click", async () => { status.textContent = "Loading..."; list.replaceChildren(); try { const data = await ExecuteGraphQL( GetAssociates, { companyNo, first: 10 }, { operationName: "GetAssociates" }, ); const items = data.useCompany.associate.items; status.textContent = `${items.length} associate(s) in company ${companyNo}`; for (const a of items) { const li = document.createElement("li"); li.textContent = `${a.associateNo} - ${a.name}`; list.appendChild(li); } } catch (err) { status.textContent = err instanceof GraphQLRequestError ? err.message : "The associates couldn't be loaded."; } }); // Ask Business NXT for the current row selection on startup. SendMessage({ messageType: "selection-state-request" }) .then((selection) => console.log("Current selection:", selection)) .catch((err) => console.warn("No selection yet:", err)); ``` ## Run it ```bash npm run dev # local dev server npm run build # outputs static files to dist/ ``` Deploy the contents of `dist/` to the HTTPS origin your app is served from. Business NXT loads it inside an iframe. > [!NOTE] > > The app only does something when it runs inside Business NXT - that is the `window.parent` the SDK talks to. Opened standalone, requests stay pending; open it from within Business NXT instead. Building with React? `@business-nxt/app-messaging-react` ships the same API plus hooks like `useSelection` and `useEditStatus`.