# 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

1. **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.
2. **Find the port and key.** Read
   `%LOCALAPPDATA%\Exsitec\DataLink\endpoint.json` — it holds the loopback `port`
   and a per‑startup `key`.
3. **Say hello.** `GET http://127.0.0.1:{port}/v1/status` with the header
   `X-DataLink-Key: {key}`. You get the version and whether you are signed in.
4. **Pick a company.** `GET /v1/companies` lists the companies you can access, with
   what your Data Link access and permissions let you do in each.
5. **Run GraphQL.** `POST /v1/nxt/graphql` with a standard GraphQL body:

   ```json
   { "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](#response-headers).

### 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
```

```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 is `http://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
  `\u002B` escapes.
- **`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/login` waits for you to finish the browser sign‑in (up to **5
  minutes**).
- `POST /v1/nxt/graphql` takes 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 **`Origin`** header are rejected (`403`) — so a web page's
  `fetch()` cannot reach it. Call it from a local process (WinHTTP, `HttpClient`,
  `curl`, Python, PowerShell, a VBA `WinHttp.WinHttpRequest`), not browser
  JavaScript.
- The **`Host`** header must be `127.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`](#get-v1status) | Health, version, and sign‑in state. |
| `POST` | [`/v1/auth/login`](#post-v1authlogin) | Interactive browser sign‑in. |
| `POST` | [`/v1/auth/logout`](#post-v1authlogout) | End the Business NXT session. |
| `GET` | [`/v1/companies`](#get-v1companies) | Companies the sign‑in can access, with your rights in each. |
| `POST` | [`/v1/nxt/graphql`](#post-v1nxtgraphql-the-gateway) | Any Business NXT GraphQL query or mutation, rights‑checked. |

### `GET /v1/status`

Health and sign‑in state. It works while signed out.

```json
{
  "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.

```json
{ "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](#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.

```json
{
  "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](#data-link-access-and-permissions)).

### `POST /v1/nxt/graphql` — the gateway

Runs **any** Business NXT GraphQL query or mutation. The body is standard
GraphQL‑over‑HTTP:

```json
{
  "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:

1. The Data Link **parses** the document and works out every company it reads,
   writes or deletes in ([section 6](#how-the-rights-check-reads-a-document)).
2. 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.
3. 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 `errors` and `extensions` (for example
   `extensions.vbnxt-trace-id`).

```json
{
  "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:

```graphql
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.
- `no` must resolve to an integer. A missing `no`, a `null`, a string, or an
  undefined variable is refused with `400 invalid_request`.
- **Every operation** in the document is checked, not only the one
  `operationName` selects.
- **Fragments** and inline fragments are expanded; `@skip` and `@include` are
  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_TABLE` on 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 with `finishEditCache(action: SAVE)` (or
  `DISCARD`). 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`:

```json
{
  "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:

```json
{ "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/graphql` you send to `/v1/nxt/graphql`, subject to
  the [Rights Check](#how-the-rights-check-reads-a-document). 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`](/docs/vba/ExsitecDataLink.bas) | The fixed SDK: auto-start, discovery, sign-in, GraphQL, JSON, errors and worksheet output. |
| [`ExsitecDataLinkCustom.bas`](/docs/vba/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`:

```vb
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

```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:

```python
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.*
