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=<guid>&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/<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.

ParameterDescription
companyNoThe Business NXT company the user is working in. Use it as the useCompany(no: ...) argument in your queries.
customerNoThe Visma customer (tenant) number.
layoutNoThe layout the app is mounted in.
elementNoThe App element’s identifier within that layout.
subThe signed-in user’s Visma Connect subject id. Treat it as a hint only - Business NXT re-derives identity from the verified token.
userThe user’s login name, for display.
localeThe 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:

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"
}
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:

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 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-requestAsk for the current row selection. Set toParent: true for the joined parent table; defaults to the focused table.
refresh-data-requestAsk Business NXT to refresh a table ({ table: "order" }) after your app changed data.
edit-session-requestAsk whether an edit session is active.
dialog-requestAsk Business NXT to open a modal dialog with your markdown content and one or two buttons.
Message (Business NXT → app)Purpose
selection-stateThe user changed the row selection. Carries table, focusedRow, selectedRows, fromParent.
edit-sessionAn edit session started, was saved or discarded.
dialog-responseThe user answered a dialog you opened (result is Closed 0, Primary 1, or Secondary 2).
okAcknowledges a request that has no data reply (e.g. refresh-data-request).
errorA request could not be served (reason: "schema-violation" or "overlay-already-open").

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-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.

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.
Last modified May 28, 2026