---
title: "Development"
description: "Build apps that embed inside the Business NXT UI and run authenticated GraphQL - without ever handling a bearer token."
lang: en
languages:
  en: https://docs.vismasoftware.no/businessnxtapi/apps/development/index.md
lastmod: 2026-07-10
---

> Documentation index: https://docs.vismasoftware.no/businessnxtapi/apps/llms.txt


# Development

Last modified July 10, 2026

> 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, placed as an element in a layout.

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.

| 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](https://docs.vismasoftware.no/businessnxtapi/intro/gettingstarted/index.md) and the [GraphQL Schema](https://docs.vismasoftware.no/businessnxtapi/apireference/schema/index.md). 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](https://docs.vismasoftware.no/businessnxtapi/apps/sample/index.md) 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](https://docs.vismasoftware.no/businessnxtapi/apps/sample/index.md) 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://<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.

## 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).
- **Allow embedding through the whole frame chain.** Your app is loaded in a nested iframe: the Business NXT UI at `https://business.visma.net` embeds the proxy frame at `https://apps.business.visma.net`, which embeds your origin. The `frame-ancestors` directive is checked against every ancestor in that chain, so your Content-Security-Policy must list both origins: `Content-Security-Policy: frame-ancestors https://business.visma.net https://apps.business.visma.net;`. Do not send `X-Frame-Options: DENY` (or `SAMEORIGIN`), which would block the embed regardless of CSP.
- **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.


---

[View this page](https://docs.vismasoftware.no/businessnxtapi/apps/development/)
