Business NXT Apps
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=<guid>&user=...&locale=nb-NOBusiness NXT does not load your origin directly: the layout element first loads a proxy frame at https://apps.business.visma.net/<app-id> 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:
{
"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"
}{
"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:
{
"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 and the GraphQL 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 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 uses it:
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. 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://<id> (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-statealways carriesfromParent: 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-statecarriesfromParent: truewhen the selection came from the bound table andfromParent: falsewhen it came from any other focused table.selection-state-requestwithtoParent: truereturns 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.