Raw ↓ — Raw Markdown for AI assistants
Exsitec Data Link — Usage
The Exsitec Data Link is a small Windows companion that lets a local program —
an Excel workbook, a script, or your own tool — run Visma Business NXT GraphQL
queries and mutations without handling any Visma sign‑in or token. It signs in to
Business NXT for you and exposes a governed GraphQL gateway on your own machine
(localhost): you send a GraphQL document, the Data Link checks your Data Link
Rights for every company the document touches, and forwards it to Business NXT
under your own Visma sign‑in. Business NXT's answer comes back unchanged.
Reading this with an AI assistant? This page is the complete usage reference. You can paste it into your assistant, or hand it the raw Markdown at
/docs/usage.md(same content, unstyled). Ask it to write the requests in your language of choice — everything it needs (endpoints, headers, the rights rules, request and response shapes, and the error format) is below. For the Business NXT schema itself (tables, fields, filters), point it at Visma's Business NXT GraphQL documentation or ask it to run an introspection query through the gateway.
1. Quick start
Install and sign in. Install the Data Link, then right‑click its tray icon (bottom‑right, near the clock) → Sign in. Your browser opens Visma Connect; sign in there.
Find the port and key. Read
%LOCALAPPDATA%\Exsitec\DataLink\endpoint.json— it holds the loopbackportand a per‑startupkey.Say hello.
GET http://127.0.0.1:{port}/v1/statuswith the headerX-DataLink-Key: {key}. You get the version and whether you are signed in.Pick a company.
GET /v1/companieslists the companies you can access, with what your Data Link access and permissions let you do in each.Run GraphQL.
POST /v1/nxt/graphqlwith a standard GraphQL body:{ "query": "{ useCompany(no: 4926238) { generalLedgerAccount(first: 5) { items { accountNo name } } } }" }The response is Business NXT's own
{ "data": …, "errors": … }.
2. How it runs
- The Data Link installs per user and starts automatically when you sign in to Windows. It runs quietly in the background and shows a small system‑tray icon (bottom‑right of the taskbar, near the clock).
- Right‑click the tray icon to:
- see whether you are Signed in or Signed out,
- Sign in / Sign out (sign‑in opens your browser to Visma Connect),
- Open logs folder, Check for updates…, open this Usage documentation, and Quit.
- You must be signed in before any GraphQL request will succeed. Sign‑in is interactive (a browser window); after that the session is renewed automatically and survives restarts of the Data Link.
- The companion holds no "current company." Every GraphQL document names the
companies it works on (
useCompany(no: …)), so one running companion serves all of them. - Updates: tray → Check for updates… downloads and installs a new version in one step and restarts the Data Link (a balloon confirms the new version). A restart gives it a new port and key — see the restart signal in section 3.
Where things live
| What | Where |
|---|---|
| Discovery file (port + key) | %LOCALAPPDATA%\Exsitec\DataLink\endpoint.json — readable only by you. |
| Logs | %LOCALAPPDATA%\Exsitec\DataLink\logs\ (tray → Open logs folder). Logs never contain GraphQL documents, amounts, keys, or tokens. |
| Sign‑in session | session.dat in the same folder, encrypted for your Windows user. |
| The app itself | %LOCALAPPDATA%\Exsitec.DataLink\ (per‑user install that updates itself). |
3. Talking to the local API
Discovery — find the port and key
The companion picks a free loopback port at startup and writes a small discovery file so trusted local callers can find it:
%LOCALAPPDATA%\Exsitec\DataLink\endpoint.json
{
"port": 51234,
"key": "b3d1…a9f0",
"instanceId": "3f2a…",
"processId": 18244,
"protocol": "v1",
"version": "2.0.0",
"startedUtc": "2026-10-07T07:15:00.0000000+00:00"
}
port— the loopback port. The API base URL ishttp://127.0.0.1:{port}.key— the per‑startup shared secret. Send it on every request (below). It changes each time the companion restarts, so read it fresh — never store it. Parse the file with a JSON parser: characters such as+may be written as\u002Bescapes.instanceId— identifies this run of the companion (the "restart signal"; see below).
Base URL and versioning
http://127.0.0.1:{port}/v1/…
All endpoints live under the /v1 prefix (the protocol field in the discovery
file); the GraphQL gateway is /v1/nxt/graphql. Bind to 127.0.0.1 (loopback) —
the companion listens only there.
Required request headers
| Header | When | Value |
|---|---|---|
X-DataLink-Key |
Every request | The key from endpoint.json. |
Content-Type: application/json |
Requests with a JSON body | Send the body as UTF‑8. |
There is no company header: the company is named inside the GraphQL document.
Response headers
| Header | Meaning |
|---|---|
X-DataLink-Instance |
The companion's instanceId for this run. |
Restart signal. If the X-DataLink-Instance value changes between calls,
the companion restarted. Re‑read endpoint.json — the port and key may have
changed.
Timeouts
POST /v1/auth/loginwaits for you to finish the browser sign‑in (up to 5 minutes).POST /v1/nxt/graphqltakes as long as Business NXT takes; large reads can take many seconds. Give your HTTP client a generous timeout (minutes, not seconds).
Security constraints (why a browser fetch is rejected)
The companion enforces a loopback trust boundary:
- Requests carrying an
Originheader are rejected (403) — so a web page'sfetch()cannot reach it. Call it from a local process (WinHTTP,HttpClient,curl, Python, PowerShell, a VBAWinHttp.WinHttpRequest), not browser JavaScript. - The
Hostheader must be127.0.0.1,localhost, or::1. - A missing or wrong key returns
401.
4. Endpoint reference
Every request needs X-DataLink-Key. Response bodies from the Data Link itself are
JSON with camelCase field names; POST /v1/nxt/graphql returns Business NXT's
body unchanged.
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/status |
Health, version, and sign‑in state. |
POST |
/v1/auth/login |
Interactive browser sign‑in. |
POST |
/v1/auth/logout |
End the Business NXT session. |
GET |
/v1/companies |
Companies the sign‑in can access, with your rights in each. |
POST |
/v1/nxt/graphql |
Any Business NXT GraphQL query or mutation, rights‑checked. |
GET /v1/status
Health and sign‑in state. It works while signed out.
{
"status": "ok",
"instanceId": "3f2a…",
"protocol": "v1",
"version": "2.0.0",
"provider": { "id": "business-nxt", "authenticated": true },
"startedUtc": "2026-10-07T07:15:00+00:00"
}
POST /v1/auth/login
Starts the interactive browser sign‑in and returns when it finishes. No body. It
always opens the browser, even when you are already signed in — check
GET /v1/status first if you only want to sign in when needed.
{ "authenticated": true }
If the browser flow is not completed within 5 minutes the call fails with
503 unavailable (retryable: true); a sign‑in Visma Connect rejects returns
401 unauthorized.
POST /v1/auth/logout
Ends the Business NXT session. Returns { "authenticated": false }.
GET /v1/companies
The companies the current sign‑in can access (Business NXT's availableCompanies),
each with its tenant (the Visma.net customer it belongs to) and the
Data Link access and permissions that apply there — so you can resolve a
company name to its number and see what you may do before you try.
{
"available": [
{ "id": "4926238", "name": "Exsitec BNXT Demo", "tenantId": "4926230", "canRead": true, "canWrite": false, "canDelete": false },
{ "id": "6307615", "name": "Demo - Junge Byg Aps 9999", "tenantId": "2728812", "canRead": true, "canWrite": true, "canDelete": false }
]
}
A company listed with canRead: false is one your Visma sign‑in can see but the
Data Link does not open for you (see section 5).
POST /v1/nxt/graphql — the gateway
Runs any Business NXT GraphQL query or mutation. The body is standard GraphQL‑over‑HTTP:
{
"query": "query Accounts($company: Int!) { useCompany(no: $company) { generalLedgerAccount(first: 5) { items { accountNo name } } } }",
"variables": { "company": 4926238 },
"operationName": "Accounts"
}
| Field | Required | Meaning |
|---|---|---|
query |
yes | The GraphQL document (string). |
variables |
no | A JSON object of variable values. |
operationName |
no | Which operation to run when the document has several. |
extensions |
no | Standard GraphQL extension data, forwarded unchanged. The rights check does not read it. |
What happens:
- The Data Link parses the document and works out every company it reads, writes or deletes in (section 6).
- It checks your Data Link access and permissions for each of them. If anything is not allowed, the whole document is refused and nothing reaches Business NXT.
- Otherwise it forwards your request body unchanged to Business NXT under your
Visma sign‑in and returns Business NXT's HTTP status and body verbatim —
including Business NXT's own
errorsandextensions(for exampleextensions.vbnxt-trace-id).
{
"data": {
"useCompany": {
"generalLedgerAccount": {
"items": [ { "accountNo": 1010, "name": "Varesalg DK" }, { "accountNo": 1020, "name": "Anden omsætning" } ]
}
}
},
"extensions": { "vbnxt-trace-id": "…" }
}
The Data Link renews your Visma token by itself and, when Business NXT answers
429 Too Many Requests, waits and retries — both are safe because the request did
not run. It never retries anything else, so a mutation can never run twice; a
5xx from Business NXT is passed back to you as is.
5. Data Link Access and Permissions
What you may do through the Data Link is administered by Exsitec in NxtPlatform admin. The old Config API grants portal is gone; the docs/download host does not grant access.
| What | Granted | Allows |
|---|---|---|
| Data Link Access | Per tenant (a Visma.net customer) through the Data Link module — for all users or chosen groups, with a per‑user override. | Reading any company in that tenant (canRead). Without it, everything on the tenant's companies is refused, reads included. |
| Write permission | Through NxtPlatform roles. | Mutations that create, update, process or report (*_create, *_update, *_processings, *_reports, edit‑cache control) — on tenants where you have Data Link Access (canWrite). |
| Delete permission | Through NxtPlatform roles. | Mutations that delete (*_delete) — on tenants where you have Data Link Access (canDelete). Deleting needs Delete, not Write. |
The Data Link only ever restricts what your own Visma user may do — it never
grants access Visma would not. A company outside your availableCompanies is
refused even with access to its tenant.
When you have no Write permission anywhere, the Data Link also asks Visma for a read‑only sign‑in the next time you sign in, so Business NXT itself refuses mutations too.
Your access and permissions are fetched from NxtPlatform when you sign in and
refreshed every few minutes, and kept in memory only. If NxtPlatform cannot be
reached, the last answer keeps applying for a short grace period; after that — or
if the Data Link has never reached NxtPlatform since it started — the gateway
refuses with 503 unavailable (retryable: true) rather than run unchecked.
6. How the Rights Check reads a document
The check works on the parsed document, never on its text, so aliases, fragments and variables cannot hide anything. It is deliberately strict:
Naming the company: useCompany(no: {companyNo})
Every company‑scoped read or write sits under a root useCompany(no: …). The no
is the company number from GET /v1/companies — a literal, a variable, or a
variable default:
query Projects($company: Int!) {
useCompany(no: $company) {
orgUnit2(filter: { orgUnit2No: { _eq: 2130 } }) { items { orgUnit2No name } }
}
}
(Business NXT's organisational units are relabelled per company; in Demo - Junge
Byg Aps 9999 projects are orgUnit2.)
- A document may name several companies, normally with aliases
(
demo: useCompany(no: 4926238) { … } junge: useCompany(no: 6307615) { … }). Each is checked; one refusal refuses the whole document. nomust resolve to an integer. A missingno, anull, a string, or an undefined variable is refused with400 invalid_request.- Every operation in the document is checked, not only the one
operationNameselects. - Fragments and inline fragments are expanded;
@skipand@includeare ignored, so a conditional selection still counts.
What each part of a document needs
| In the document | Needs |
|---|---|
query { useCompany(no: N) { … } } |
Read on company N's tenant. |
mutation { useCompany(no: N) { x_create / x_update / x_processings / x_reports / finishEditCache … } } |
Write on N's tenant. |
mutation { useCompany(no: N) { x_delete … } } |
Delete on N's tenant. |
query { useCustomer(no: T) { … } } |
Read on tenant T. |
mutation { useCustomer(…) } |
Always refused — customer‑level changes (users, companies) are not done through the Data Link. |
mutation { asyncQuery(query: "…", args: {…}) } |
Whatever the embedded document needs, checked by the same rules. |
availableCompanies, availableCustomers, useModel, api, asyncRequests, asyncResult, asyncQuery_delete, introspection (__schema, __type) |
Only a sign‑in. |
Any other root field, or a subscription |
Refused (403 forbidden). |
Writing safely
Business NXT has no cross‑table transactions. Two habits keep a write predictable:
- Put
onError: FAIL_TABLEon each table operation, so a bad row rejects that whole table operation instead of writing part of it. - To make several table operations succeed or fail together, use Business NXT's
edit cache: run them with
useCompany(no: N, persistEditCache: true, partialRequestId: "…")and finish withfinishEditCache(action: SAVE)(orDISCARD). Each request is rights‑checked on its own, like any other mutation.
7. Errors
There are two kinds of error, and both use GraphQL's error shape.
Business NXT's own errors — a GraphQL error, a value Business NXT rejects, a
rate limit — come back exactly as Business NXT sent them: its HTTP status and
its { "data": …, "errors": [ … ] } body.
Refusals by the Data Link itself (the request never reached Business NXT) carry
extensions.code:
{
"errors": [
{
"message": "You have no Data Link Write permission for company 'Exsitec BNXT Demo' (4926238).",
"extensions": { "code": "forbidden", "retryable": false, "instanceId": "3f2a…", "company": "4926238" }
}
]
}
code |
HTTP | Meaning / what to do |
|---|---|---|
invalid_request |
400 | Body is not JSON, has no query, has members other than query/variables/operationName/extensions or repeated property names (also differing only in case), variables is not an object, the document does not parse, or a company number cannot be resolved. Fix and resend. |
unauthorized |
401 | Missing/invalid key, not signed in, or Visma rejected the sign‑in. Re‑read endpoint.json; sign in. |
forbidden |
403 | Your Data Link access or permissions do not allow this on a company (extensions.company names it), the company is not available to you, or the document uses a refused root (useCustomer mutation, unknown root, subscription). Also cross‑origin/host blocks. |
unavailable |
503 | Your rights could not be fetched (retryable: true), or the Data Link is busy. Retry shortly. |
internal_error |
500 | Unexpected. Check the logs folder (tray → Open logs folder). |
GET /v1/status, /v1/auth/* and GET /v1/companies report their errors in the
Data Link's plain envelope instead:
{ "code": "unauthorized", "message": "…", "retryable": false, "instanceId": "3f2a…" }
An unknown path gets a bare 404.
8. Relationship to the Business NXT GraphQL API
The Data Link is Business NXT's GraphQL API, reached through your own machine:
- Same schema, same documents. Anything you can send to
https://business.visma.net/api/graphqlyou send to/v1/nxt/graphql, subject to the Rights Check. Use Visma's Business NXT documentation, or introspection through the gateway, for tables, fields and filters. - No tokens to manage. The Data Link holds your Visma Connect sign‑in and adds it to every forwarded request; your program never sees a token.
- Governed. Every document is checked against your Data Link access and permissions before it leaves your machine, so Exsitec can decide per tenant who may read, write and delete through the Data Link.
- Nothing reshaped. The response is Business NXT's, byte for byte.
9. Using it from Excel
Download and import the two VBA modules below (Alt+F11 -> File -> Import File...). They are standard modules and use only late binding, so no Tools -> References changes are needed.
| File | What it is |
|---|---|
ExsitecDataLink.bas |
The fixed SDK: auto-start, discovery, sign-in, GraphQL, JSON, errors and worksheet output. |
ExsitecDataLinkCustom.bas |
Your workbook template: COMPANY_NO and example button macros. Change this file per customer/workbook. |
Public VBA API
| API | Purpose |
|---|---|
NxtQuery(gql, [vars]) As Object |
Sends a GraphQL query or mutation to POST /v1/nxt/graphql and returns the data dictionary. If the document uses $company and vars has no company, the SDK adds COMPANY_NO. |
NxtVars(name, value, ...) As Object |
Builds a variables dictionary. Example: NxtVars("company", 4926238, "first", 20). |
NxtItems(data, path) As Collection |
Walks a dotted path such as useCompany.generalLedgerAccount and returns its items collection. items: null becomes an empty collection. |
WriteRows(items, target, [columns], [includeHeader]) As Range |
Writes rows to Excel in one block. Nested objects expand one level (a.b), ISO dates become Excel dates, null becomes empty, and formula-looking text stays text. |
ShowNxtError() |
Shows Err.Description in a friendly message box. Use it in error handlers. |
NxtUseEndpointFile(path) |
Advanced/test hook for a non-default discovery file. |
NxtUseLanguage(languageCode) |
Advanced/test hook that overrides the SDK message language (en, da, sv, nb). |
NxtUseStartPaths(primaryExe, fallbackExe) |
Advanced/test hook for non-default Data Link executable paths. |
Button macro pattern
Put company-specific settings and button macros in ExsitecDataLinkCustom.bas:
Public Const COMPANY_NO As Long = 4926238
Public Sub ListAccounts()
On Error GoTo Failed
Dim data As Object
Set data = NxtQuery("query Accounts($company: Int!) { useCompany(no: $company) { generalLedgerAccount(first: 20) { items { accountNo name } } } }")
WriteRows NxtItems(data, "useCompany.generalLedgerAccount"), ActiveSheet.Range("A1"), Array("accountNo", "name")
Exit Sub
Failed:
ShowNxtError
End Sub
For mutations, call the same NxtQuery function with a mutation document. The SDK
never retries mutations after a port change or transport failure.
Auto-start and sign-in
The SDK reads %LOCALAPPDATA%\Exsitec\DataLink\endpoint.json. If the file is missing
or /v1/status does not answer, it starts %LOCALAPPDATA%\Exsitec.DataLink\Exsitec Data Link.exe
(or current\DesktopDataLink.Host.exe) and waits up to 30 seconds.
Before every call it checks /v1/status. If you are signed out, Excel tells you that
the browser will open, calls /v1/auth/login, then raises an error telling you to run
the action again once sign-in is complete. The original GraphQL request is not run
automatically after sign-in.
VBA error numbers
| Constant | Meaning |
|---|---|
NXT_ERR_NOT_RUNNING |
The Data Link is not installed, could not be started, or never answered. |
NXT_ERR_NOT_SIGNED_IN |
You are signed out, canceled sign-in, or sign-in needs the action to be run again. |
NXT_ERR_FORBIDDEN |
Data Link Access/Permissions forbid the request or company. |
NXT_ERR_INVALID |
The GraphQL request is malformed or not allowed by the gateway. |
NXT_ERR_NXT |
Business NXT returned GraphQL errors; Err.Description is the first Business NXT message. |
NXT_ERR_UNAVAILABLE |
The Data Link, Business NXT or the rights service is temporarily unavailable. |
NXT_ERR_OTHER |
Any other non-success response. |
Messages use the Data Link tray language setting (ui-settings.json) for English,
Danish, Swedish and Norwegian, defaulting to English.
10. Examples in other languages
PowerShell
# 1. Discover the running companion.
$ep = Get-Content "$env:LOCALAPPDATA\Exsitec\DataLink\endpoint.json" | ConvertFrom-Json
$base = "http://127.0.0.1:$($ep.port)/v1"
$hdr = @{ "X-DataLink-Key" = $ep.key }
# 2. Check status / sign-in.
Invoke-RestMethod "$base/status" -Headers $hdr
# 3. Resolve a company name to its number, and see your rights there.
$companies = (Invoke-RestMethod "$base/companies" -Headers $hdr).available
$company = $companies | Where-Object { $_.name -eq "Exsitec BNXT Demo" }
# 4. Run GraphQL.
$body = @{
query = 'query A($c: Int!) { useCompany(no: $c) { generalLedgerAccount(first: 5) { items { accountNo name } } } }'
variables = @{ c = [int]$company.id }
} | ConvertTo-Json -Depth 5
$result = Invoke-RestMethod "$base/nxt/graphql" -Method Post -Headers $hdr `
-ContentType "application/json" -Body $body
$result.data.useCompany.generalLedgerAccount.items
Python
Standard library only:
import json, os, urllib.request
path = os.path.join(os.environ["LOCALAPPDATA"], "Exsitec", "DataLink", "endpoint.json")
with open(path, encoding="utf-8-sig") as f:
ep = json.load(f)
base = f"http://127.0.0.1:{ep['port']}/v1"
def call(method, path, body=None):
headers = {"X-DataLink-Key": ep["key"]}
data = None
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
request = urllib.request.Request(base + path, data=data, headers=headers, method=method)
with urllib.request.urlopen(request, timeout=300) as response: # HTTPError carries the GraphQL errors
return json.load(response)
def graphql(query, **variables):
return call("POST", "/nxt/graphql", {"query": query, "variables": variables})
companies = call("GET", "/companies")["available"]
junge = next(c for c in companies if c["name"] == "Demo - Junge Byg Aps 9999")
result = graphql(
"query P($c: Int!, $p: Int!) { useCompany(no: $c) { orgUnit2(filter: { orgUnit2No: { _eq: $p } }) { items { orgUnit2No name } } } }",
c=int(junge["id"]), p=2130)
print(result["data"]["useCompany"]["orgUnit2"]["items"])
11. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
No endpoint.json, or VBA NXT_ERR_NOT_RUNNING |
The Data Link is not installed or did not answer. | The VBA SDK starts it automatically; if that fails, install it from the docs/download page. |
| Connection refused after it worked before | The Data Link restarted or quit (e.g. after an update): new port and key. | Re‑read endpoint.json. |
401 "A valid connector key is required." |
Missing or stale key. | Re‑read endpoint.json — the key changes on every start. |
401 "Not signed in to Business NXT…" |
Not signed in, or the session ended. | Tray → Sign in (or POST /v1/auth/login). |
403 "Cross-origin requests are not permitted." |
The call came from browser JavaScript. | Call from a local program or script instead. |
403 "You have no Data Link Access to …" |
You have no Data Link Access on that company's tenant. | Ask your Exsitec contact for Data Link access to that tenant. |
403 "You have no Data Link Write / Delete permission…" |
Your NxtPlatform roles lack that permission (canWrite / canDelete). |
Ask your Exsitec contact; check GET /v1/companies. |
403 "Company … is not available to the signed‑in user." |
Your Visma user cannot see that company, or a typo in the number. | Check GET /v1/companies; sign out and in with the right Visma user. |
400 "…'no' argument must resolve to an integer." |
useCompany without a number, or a variable you did not send. |
Pass the company number as an Int literal or variable. |
503 "Data Link rights could not be fetched…" |
NxtPlatform is unreachable. | Retry shortly. |
503 "Sign-in timed out…" |
The browser sign‑in was not finished within 5 minutes. | Try again and complete it in the browser. |
| Excel shows "Not Responding" during sign-in | /v1/auth/login waits for the browser. |
Finish signing in in the browser; then run the Excel action again. |
Still stuck? Tray → Open logs folder and send the latest log file to your Exsitec contact — it contains no GraphQL documents, amounts, keys, or tokens.
12. Glossary
| Term | Meaning |
|---|---|
| Data Link (companion) | The tray app on your PC that signs in to Business NXT and serves the local gateway. |
| NXT Gateway | POST /v1/nxt/graphql: Business NXT GraphQL, rights‑checked and forwarded under your sign‑in. |
| Rights Check | The gateway's check of a parsed GraphQL document against your Data Link Access and Permissions, all or nothing. |
| Data Link Access | Your right to use the Data Link at all on a tenant's companies (canRead), granted in NxtPlatform admin's Data Link module. |
| Data Link Permission | Write or Delete (canWrite / canDelete), granted through NxtPlatform roles; it applies only on tenants where you have Data Link Access. |
| Tenant | A Visma.net customer; Data Link Access is granted per tenant. |
| Company | A Business NXT company, identified by its number and named in each document with useCompany(no: …). |
| Discovery file | endpoint.json: where the running Data Link publishes its port, key, and instance id. |
| Key | The per‑startup secret every request must carry; it changes on each start. |
| Instance id | Identifies one run of the Data Link; a new value means it restarted. |
This document describes the v1 local API. The companion reports its running
version in GET /v1/status and on the tray menu.