> ## Documentation Index
> Fetch the complete documentation index at: https://docs.consile.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# API reference: Business Central

> The five read-only tools in the Business Central connector (early access): environment and company discovery, company master data, and an allow-listed OData query over the standard API v2.0 entity sets. Covers the environment and companyId conventions, OData literals, cursor paging, size caps and the error contract.

All Business Central tools are read-only and are called via `tools/call` over
`POST /business-central/mcp`. The connector reads Microsoft Dynamics 365 Business
Central online and is connected through OAuth with a Microsoft work or school
account (see [Forbind Business Central](/connect/forbind-business-central)). You need
an active Business Central subscription, and your user must hold a seat on it
(assigned under **Team** in the portal, see [Team](/portal/team)).

<Info>
  The connector is in early access. It currently serves five tools: three discovery tools
  (`list_environments`, `list_companies`, `get_company_information`) and a generic,
  allow-listed query tool (`query_entity`) with its companion
  (`list_entity_metadata`). More curated read tools are planned. This page documents
  only what is served today.
</Info>

<Info>
  Endpoint: `https://mcp.consile.ai/business-central/mcp`. Business Central is its
  own named connector with only its own tools. Tool names are namespaced
  `business-central__<tool>` (e.g. `business-central__list_companies`); the bare
  names are used below for readability. A bare name in a request returns
  `Unknown tool.`

  The connector is multi-instance: one instance covers one Microsoft Entra
  tenant, including all of its environments and companies. An additional tenant is
  added as a portal by an administrator and served at its own endpoint, e.g.
  `https://mcp.consile.ai/business-central-2/mcp`, with tools named
  `business-central-2__<tool>`.
</Info>

For an account with an active Business Central subscription, `tools/list` returns
the five `business-central__*` tools plus the unprefixed system tool
[`connect_account`](#connect_account). Without a subscription only
`connect_account` is listed, and the `initialize` result includes `instructions`
explaining how to get access. A user without a seat still sees the tools, but every
call is refused. Every tool has a `title` and the annotations `readOnlyHint: true`,
`destructiveHint: false` and `openWorldHint: false`. A successful result contains
the payload twice: as JSON text in `content` and as the same object in
`structuredContent`. The examples below show that payload.

## Authorization and read-only enforcement

The connect flow is a standard authorization-code flow with PKCE against
`https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize`. Because it
uses the `organizations` endpoint, only work or school accounts can connect; a
personal Microsoft account fails at Microsoft. The requested scopes are:

* `https://api.businesscentral.dynamics.com/Financials.ReadWrite.All`
* `offline_access` (for the refresh token)

Microsoft publishes no read-only delegated permission for the Business Central API,
so the grant itself is read/write. Consile enforces read-only on its side:

* The connector has no write tools.
* Consile sends only read requests to the Business Central API. Reads ask for
  Business Central's read-only data access intent (`Data-Access-Intent: ReadOnly`),
  which routes them to a read replica. If an API page answers `400` to that, the
  request is retried once without it.
* The granted scope is checked as a closed set when the account is connected. It
  must contain a Business Central API scope (`Financials.ReadWrite.All` or
  `user_impersonation`, qualified or bare). `openid`, `profile`, `email` and
  `offline_access` are tolerated. Any other scope rejects the connect, and so does a
  token response without a refresh token.

Since the Microsoft grant covers writes, Consile recommends connecting a Business
Central user with a read-only permission set. The user's own Business Central
permissions are the only layer that also limits the token outside Consile.

Access tokens are refreshed automatically. Consile never hands the token to the MCP
client, and the MCP client's own token is never forwarded to Microsoft.

## Shared conventions

### Environments and companies

Business Central addresses data by an environment name (a Production or Sandbox
environment in the tenant) and a company id (a legal entity inside that
environment). Both are per-call arguments. Nothing about the choice is stored on
Consile's side. The intended order is `list_environments` (only when the default is
wrong), then `list_companies`, then the company-scoped tools with the GUID in hand.

<ParamField path="environment" type="string" default="Production">
  Business Central environment name, 1 to 80 characters. Optional on every tool that
  takes it. Call [`list_environments`](#list_environments) if the tenant uses
  another name. An unknown name returns an error that lists the environments that
  exist.
</ParamField>

<ParamField path="companyId" type="string (GUID)" required>
  The company id from [`list_companies`](#list_companies). Required on
  `get_company_information` and `query_entity`. It cannot be derived from a company
  name. A wrong GUID returns a `404` error, not another company's data.
</ParamField>

<ParamField path="search" type="string">
  Optional case-insensitive substring filter, applied by Consile after the read
  (`list_companies` and `list_entity_metadata`). At least one character.
</ParamField>

### Result context

Every result except `list_environments` echoes a `context` object with the
`environment` (and the `companyId`, where one applies) it was read from, so a result
cannot be misattributed to another company. `@odata.*` annotation keys are removed
from every row.

### Typical call order

1. `list_companies` (and `list_environments` only if the environment is not
   `Production`).
2. `get_company_information`, to confirm which books are being read.
3. `list_entity_metadata`, to see which entity sets the environment serves.
4. `query_entity` with `select` and `filter`.

***

## list\_environments

Lists the Business Central environments in the connected Microsoft Entra tenant.
Only Business Central environments are returned. Call it only when the tenant runs
several environments or a tool reported an unknown environment name.

**Arguments:** none.

<ResponseField name="items" type="array">
  Each item has `name`, `type` (`Production` or `Sandbox`), `countryCode` and
  `aadTenantId` (the Entra tenant id). A missing `countryCode` or `aadTenantId`
  is `null`.
</ResponseField>

<CodeGroup>
  ```json Request theme={null}
  { "name": "business-central__list_environments", "arguments": {} }
  ```

  ```json Result (payload) theme={null}
  {
    "count": 2,
    "items": [
      { "name": "Production", "type": "Production", "countryCode": "DK", "aadTenantId": "3f2b6c1e-8a4d-4f0e-9b7a-1c2d3e4f5a6b" },
      { "name": "Sandbox", "type": "Sandbox", "countryCode": "DK", "aadTenantId": "3f2b6c1e-8a4d-4f0e-9b7a-1c2d3e4f5a6b" }
    ]
  }
  ```
</CodeGroup>

***

## list\_companies

Lists the companies (legal entities) in an environment, with the GUID every
company-scoped tool requires.

<ParamField path="environment" type="string" default="Production" />

<ParamField path="search" type="string">
  Filters `name` and `displayName` case-insensitively.
</ParamField>

<ResponseField name="items" type="array">
  Each item has `id` (the `companyId`), `name`, `displayName` and `systemVersion`.
</ResponseField>

<ResponseField name="ambiguous" type="boolean">
  `true` when a `search` matched more than one company. The tool description tells
  the model to ask the user which one is meant instead of guessing.
</ResponseField>

<CodeGroup>
  ```json Request theme={null}
  {
    "name": "business-central__list_companies",
    "arguments": { "search": "contoso" }
  }
  ```

  ```json Result (payload) theme={null}
  {
    "context": { "environment": "Production" },
    "count": 1,
    "items": [
      {
        "id": "9d4e2a10-5b7c-4e3f-8a21-6c0b1d2e3f40",
        "name": "CONTOSO",
        "displayName": "Contoso A/S",
        "systemVersion": "26.4.38215.0"
      }
    ],
    "ambiguous": false
  }
  ```
</CodeGroup>

***

## get\_company\_information

Reads a company's own master data: registered name, address, VAT registration
number (the CVR number in Denmark), currency code and the start of the current
fiscal year. Use it to confirm which books you are reading before reporting numbers.

<ParamField path="environment" type="string" default="Production" />

<ParamField path="companyId" type="string (GUID)" required />

<ResponseField name="companyInformation" type="object | null">
  The company's `companyInformation` record as Business Central returns it (for
  example `displayName`, `addressLine1`, `city`, `postalCode`, `country`,
  `taxRegistrationNumber`, `currencyCode`, `currentFiscalYearStartDate`), or `null`
  if the company has none.
</ResponseField>

<CodeGroup>
  ```json Request theme={null}
  {
    "name": "business-central__get_company_information",
    "arguments": { "companyId": "9d4e2a10-5b7c-4e3f-8a21-6c0b1d2e3f40" }
  }
  ```

  ```json Result (payload, shortened) theme={null}
  {
    "context": { "environment": "Production", "companyId": "9d4e2a10-5b7c-4e3f-8a21-6c0b1d2e3f40" },
    "companyInformation": {
      "displayName": "Contoso A/S",
      "addressLine1": "Havnegade 12",
      "city": "København K",
      "postalCode": "1058",
      "country": "DK",
      "taxRegistrationNumber": "DK12345678",
      "currencyCode": "DKK",
      "currentFiscalYearStartDate": "2026-01-01"
    }
  }
  ```
</CodeGroup>

***

## list\_entity\_metadata

Lists the entity sets an environment exposes, read from its OData service document.
Use the `name` values as the `entity` argument of [`query_entity`](#query_entity).
It is environment-level, so it takes no `companyId`: the same sets are served inside
every company of that environment.

<ParamField path="environment" type="string" default="Production" />

<ParamField path="search" type="string">Filters on `name`, case-insensitively.</ParamField>

<ResponseField name="items" type="array">
  Each item has `name`, `kind`, `url` and `queryable`. `queryable: false` means the
  set exists in the environment but is outside the allow-list `query_entity` accepts
  (for example `companies`, or a custom API page), so it cannot be read here.
</ResponseField>

A result larger than 40,000 JSON characters is refused. Pass `search` to narrow it.

<CodeGroup>
  ```json Request theme={null}
  {
    "name": "business-central__list_entity_metadata",
    "arguments": { "search": "compan" }
  }
  ```

  ```json Result (payload) theme={null}
  {
    "context": { "environment": "Production" },
    "count": 2,
    "items": [
      { "name": "companies", "kind": "EntitySet", "url": "companies", "queryable": false },
      { "name": "companyInformation", "kind": "EntitySet", "url": "companyInformation", "queryable": true }
    ]
  }
  ```
</CodeGroup>

***

## query\_entity

Reads one entity set of the standard Business Central API v2.0 with OData options,
one page at a time, or one record by id. Only the 85 allow-listed entity sets can be
read (see [the list below](#allow-listed-entity-sets)).

<ParamField path="environment" type="string" default="Production" />

<ParamField path="companyId" type="string (GUID)" required />

<ParamField path="entity" type="string" required>
  Entity set name, 1 to 80 characters, letters and digits only (e.g. `customers`,
  `salesInvoices`, `generalLedgerEntries`). Matched case-insensitively against the
  allow-list. A name with any other character, or one outside the allow-list, is
  refused before any request is made; an unknown name is answered with up to five
  closest matches.
</ParamField>

<ParamField path="id" type="string (GUID)">
  Reads one record instead of a page. `filter`, `orderby` and `pageSize` do not
  apply; `select` and `expand` do.
</ParamField>

<ParamField path="filter" type="string">
  Raw OData `$filter` expression, 1 to 1,000 characters. String literals are
  single-quoted, with an embedded single quote doubled
  (`displayName eq 'O''Brien'`). Dates are written unquoted as `YYYY-MM-DD`
  (`postingDate ge 2026-01-01`); a quoted date is rejected by Business Central.
</ParamField>

<ParamField path="select" type="string[]">
  Field names for `$select`. Always pass it: Business Central rows are wide, and a
  page of full rows is likely to exceed the size cap.
</ParamField>

<ParamField path="expand" type="string">
  `$expand` expression, 1 to 500 characters (e.g. `dimensionSetLines`). It
  multiplies the result size, so use it sparingly.
</ParamField>

<ParamField path="orderby" type="string">
  `$orderby` expression, 1 to 200 characters (e.g. `postingDate desc`).
</ParamField>

<ParamField path="pageSize" type="integer" default="50">
  Rows per page, `1` to `500`.
</ParamField>

<ParamField path="cursor" type="string">
  Opaque continuation token from a previous result's `nextCursor`. Omit it for the
  first page and never construct or edit one.
</ParamField>

<ResponseField name="items" type="array">
  The rows of the page, with only the selected fields.
</ResponseField>

<ResponseField name="nextCursor" type="string | null">
  Token for the next page, or `null` when the collection is exhausted. `hasMore` is
  `true` when a next page exists. There is no total count.
</ResponseField>

<ResponseField name="item" type="object">
  Returned instead of `count`/`items`/`nextCursor`/`hasMore` when `id` is given.
</ResponseField>

### Paging

Paging is server-driven. Pass the `nextCursor` back as `cursor` to read the next
page. The cursor carries the original query, so `environment`, `filter`, `select`,
`expand` and `orderby` are ignored on a continuation, and `pageSize` applies to the
next page. `entity` and `companyId` are still required and validated, so pass the
same values again. A cursor is only accepted if it points back to the Business
Central API; anything else returns an `Invalid cursor` error. If you pass both `id`
and `cursor`, the cursor is used.

### Size cap

A result larger than 100,000 JSON characters is refused, and the client sees only
`Tool invocation failed.` Narrow `select`, add a `filter` or lower `pageSize`.

<CodeGroup>
  ```json Request: overdue sales invoices theme={null}
  {
    "name": "business-central__query_entity",
    "arguments": {
      "companyId": "9d4e2a10-5b7c-4e3f-8a21-6c0b1d2e3f40",
      "entity": "salesInvoices",
      "filter": "status eq 'Open' and dueDate lt 2026-10-01",
      "select": ["number", "customerName", "dueDate", "remainingAmount"],
      "orderby": "dueDate asc",
      "pageSize": 100
    }
  }
  ```

  ```json Result (payload) theme={null}
  {
    "context": { "environment": "Production", "companyId": "9d4e2a10-5b7c-4e3f-8a21-6c0b1d2e3f40" },
    "count": 2,
    "items": [
      { "number": "103045", "customerName": "Nordhavn ApS", "dueDate": "2026-09-14", "remainingAmount": 18750 },
      { "number": "103061", "customerName": "Vestjysk Byg A/S", "dueDate": "2026-09-22", "remainingAmount": 4312.5 }
    ],
    "nextCursor": null,
    "hasMore": false
  }
  ```

  ```json Request: G/L entries on one account theme={null}
  {
    "name": "business-central__query_entity",
    "arguments": {
      "companyId": "9d4e2a10-5b7c-4e3f-8a21-6c0b1d2e3f40",
      "entity": "generalLedgerEntries",
      "filter": "accountNumber eq '1010' and postingDate ge 2026-08-01 and postingDate le 2026-08-31",
      "select": ["entryNumber", "postingDate", "documentNumber", "description", "debitAmount", "creditAmount"],
      "orderby": "postingDate asc"
    }
  }
  ```

  ```json Request: next page theme={null}
  {
    "name": "business-central__query_entity",
    "arguments": {
      "companyId": "9d4e2a10-5b7c-4e3f-8a21-6c0b1d2e3f40",
      "entity": "generalLedgerEntries",
      "cursor": "<nextCursor from the previous call>"
    }
  }
  ```
</CodeGroup>

### Allow-listed entity sets

<Accordion title="All 85 entity sets query_entity accepts">
  `accounts`, `accountingPeriods`, `agedAccountsPayable`, `agedAccountsReceivable`,
  `applyVendorEntries`, `approvalEntries`, `approvalUserSetups`, `attachments`,
  `balanceSheets`, `bankAccounts`, `cashFlowStatements`, `companyInformation`,
  `contacts`, `contactsInformation`, `countriesRegions`, `currencies`,
  `currencyExchangeRates`, `customers`, `customerContacts`,
  `customerFinancialDetails`, `customerPaymentJournals`, `customerPayments`,
  `customerReturnReasons`, `customerSales`, `defaultDimensions`, `dimensions`,
  `dimensionSetLines`, `dimensionValues`, `disputeStatus`, `documentAttachments`,
  `employees`, `fixedAssets`, `fixedAssetLocations`, `generalLedgerEntries`,
  `generalLedgerSetup`, `generalProductPostingGroups`, `incomeStatements`,
  `inventoryPostingGroups`, `items`, `itemCategories`, `itemLedgerEntries`,
  `itemVariants`, `jobQueueEntries`, `jobQueueLogEntries`, `journals`,
  `journalLines`, `locations`, `opportunities`, `paymentMethods`, `paymentTerms`,
  `pictures`, `postedApprovalEntries`, `projects`, `purchaseCreditMemos`,
  `purchaseCreditMemoLines`, `purchaseInvoices`, `purchaseInvoiceLines`,
  `purchaseOrders`, `purchaseOrderLines`, `purchaseReceipts`,
  `purchaseReceiptLines`, `retainedEarningsStatements`, `salesCreditMemos`,
  `salesCreditMemoLines`, `salesInvoices`, `salesInvoiceLines`, `salesOrders`,
  `salesOrderLines`, `salesQuotes`, `salesQuoteLines`, `salesShipments`,
  `salesShipmentLines`, `salespersonsPurchasers`, `shipmentMethods`,
  `subscriptions`, `taxAreas`, `taxGroups`, `timeRegistrationEntries`,
  `trialBalances`, `unitsOfMeasure`, `vendors`, `vendorPayments`,
  `vendorPaymentJournals`, `vendorPurchases`, `workflowApprovers`.
</Accordion>

`companies` is deliberately not on the list; use `list_companies`. A listed set can
still be missing in a given environment, depending on localisation and version.
`list_entity_metadata` shows what the environment actually serves. Custom API pages
and extension APIs cannot be read.

***

## connect\_account

`connect_account` is a system tool that the Business Central endpoint lists next to
the five tools. It takes no arguments and its name has no `business-central__`
prefix. It returns `{ "connect_url": "…", "instructions": "…" }`. The user opens
`connect_url` in a browser and is sent to Microsoft to sign in and approve access.
The link expires after 10 minutes. It only works for accounts that subscribe to
Business Central and for users who have accepted Consile's terms in the portal.
Calling it on an account that is already connected replaces the stored Microsoft
access, which is how you reconnect after the access expired or was removed.

```json Request theme={null}
{ "name": "connect_account", "arguments": {} }
```

***

## Errors

Tool failures come back as a normal `tools/call` result with `isError: true` and one
text block, not as a JSON-RPC error. The checks run in this order: tool name,
subscription, seat, connection, then argument validation and the call to Business
Central. Business Central is one of the connectors that returns explanatory
messages for the common upstream failures:

| Situation | Text in the result |
| - | - |
| Bare tool name (e.g. `list_companies`) or another connector's tool | `Unknown tool.` |
| No Business Central subscription | `You are not subscribed to business-central. Purchase it here first: https://app.consile.ai/connectors/business-central …` |
| Subscription exists, but your user has no seat | `Your company subscribes to business-central through Consile, but your user does not hold a seat on it yet. …` (an administrator assigns seats under **Team**) |
| Microsoft account not connected yet | `business-central is not connected yet. Connect it here: <link>` (link valid 10 minutes; an additional portal names itself, e.g. `business-central-2`) |
| The stored access has expired and could not be refreshed | `The Business Central access token has expired. The connection must be re-established from the Consile portal.` |
| Business Central rejected the token (`401`) | `Business Central rejected the access token (401). The connection must be re-established from the Consile portal.` |
| Business Central answered `403` | `Business Central returned 403. The connected user lacks a Business Central licence, the required permission set, or access to this company or environment. Reconnecting will not fix it; …` |
| Unknown environment name | `Environment '<name>' not found. Available: <names>. Pass one of these as the environment argument (call list_environments to see them).` |
| Wrong `companyId` or resource (`404`) | `Business Central returned 404 in environment '<environment>': the company id or the resource path does not exist there. Get a valid companyId from list_companies.` |
| Business Central rejected the query (`400`, after the retry without the read-only intent), e.g. a malformed `filter` | `Business Central rejected the request (400 <code>): <message>` (at most 500 characters) |
| Still throttled after one retry (`429`) | `Business Central rate limit hit; retried once and still limited. Wait a moment and try again, and ask for fewer rows.` |
| `entity` contains anything but letters and digits | `Invalid entity '<entity>': an entity set name is letters and digits only, …` |
| `entity` is not on the allow-list | `Unknown entity '<entity>': it is not one of the Business Central API v2.0 entity sets this tool can read. Closest matches: … Call list_entity_metadata to see what this environment exposes.` |
| `cursor` is not a valid continuation token | `Invalid cursor: … Start the listing again without a cursor.` |
| Arguments outside the input schema (e.g. a `companyId` that is not a GUID, `pageSize` above `500`, `filter` over 1,000 characters), an oversized result, a timeout, a `5xx` or `408` from Business Central, or a non-JSON response | `Tool invocation failed.` |

## Limits

* Timeout: each request to Business Central times out after 30 seconds.
* Throttling: a `429` from Business Central is retried once, after its
  `Retry-After` (at most 5 seconds, 1 second if none is given).
* Business Central's own budget: Microsoft allows 6,000 requests per 5 minutes
  per Business Central user, shared with every other Business Central client of that
  user. Narrow the `filter` instead of paging through everything.
* Page size: default 50, maximum 500 rows.
* Result size: 100,000 JSON characters for `query_entity`, 40,000 for
  `list_entity_metadata`.

Requests to the endpoint are also rate-limited per account and connector. When the
limit is exceeded you get HTTP `429` (JSON-RPC error `-32029`, `Retry-After: 1`)
instead of a tool result. See [Errors & limits](/developers/errors-and-limits) for
the current limits and the protocol-level errors shared by all connectors.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.