Skip to content

Connect your tool

Data Bridge has two doors: a REST API for integrations and scripts, and an MCP server for AI tools. Both go through the same access policy: every call is checked against the caller's grants (data type × level × administration) and against the subscription, and every call is metered.

The base URLs, shown on the dashboard:

Door URL
REST API https://apps.mvdw-software.com/exact/data-bridge/api/v1
MCP server https://apps.mvdw-software.com/exact/data-bridge/api/mcp
OpenAPI document https://apps.mvdw-software.com/exact/data-bridge/api/v1/openapi.json

REST API

Authentication

Every request carries the API key:

Authorization: Bearer exg_live_…

The key's grants decide what the call may do; the key's IP allowlist (when set) decides where it may come from.

Endpoints

Endpoint What it returns
GET /resources the data types the gateway exposes: name, version, description, operations, available filters and sort keys. Call it first
GET /divisions the administrations (division codes) this key may use
GET /divisions/:division/:resource a list of normalized records
GET /divisions/:division/:resource/:id one record, by its Exact id
GET /openapi.json the OpenAPI 3 document

Every answer is wrapped: { "data": … }.

Data types

Resource What it is Operations
accounts customers, suppliers and prospects (relations): name, code, contact details, status list, get
contacts contact persons of relations list, get
items products and services list, get
sales-invoices sales invoice headers: number, customer, dates, status, amounts list, get
gl-accounts general-ledger accounts (chart of accounts) list, get

The normalized fields are stable and readable (accountId, invoiceDate, isSupplier, …), not Exact's OData field names. The GET /resources answer and the OpenAPI document list them all, per data type.

Filters, sorting, limit

List endpoints take a small, strict query syntax; unknown parameters are rejected rather than guessed:

Parameter Meaning
filter[name]=value equality
filter[name][op]=value a comparison: startswith, ge, gt, le, lt (which ops a field has is in GET /resources)
sort=field sort by one of the resource's sort keys
limit=n at most n records, 1–1000
# customers whose name starts with "A", sorted by name, at most 20
curl -H "Authorization: Bearer exg_live_…" \
  "https://apps.mvdw-software.com/exact/data-bridge/api/v1/divisions/321654/accounts?filter[name][startswith]=A&sort=name&limit=20"

# invoices of one customer dated in October 2026
curl -H "Authorization: Bearer exg_live_…" \
  "https://apps.mvdw-software.com/exact/data-bridge/api/v1/divisions/321654/sales-invoices?filter[customerId]=d527…&filter[invoiceDate][ge]=2026-10-01&filter[invoiceDate][lt]=2026-11-01"

The available filters per data type:

Resource Filters
accounts name (eq, startswith), code, email, vatNumber, isSupplier, modifiedAt (ge, gt, le, lt)
contacts accountId, email, lastName (eq, startswith), modifiedAt
items code (eq, startswith), isSalesItem, isStockItem, isTime, modifiedAt
sales-invoices customerId, status (enum), invoiceDate (eq, ge, gt, le, lt), invoiceNumber (eq, ge, le), modifiedAt
gl-accounts code (eq, startswith), modifiedAt

Formats and linked data

Reads answer in the format of the Accept header. Without one you get JSON ({ "data": …, "meta": … }).

Accept Answer
application/json (default) the normalized JSON
application/ld+json JSON-LD with an inline @context
text/turtle Turtle
application/rdf+xml RDF/XML

The linked-data formats describe the same records with standard vocabularies: schema.org (schema:Organization, schema:Person, schema:Product, schema:Invoice, schema:Project, postal addresses and amounts of money), Dublin Core terms (identifiers and dates) and Hydra (a list is a hydra:Collection). Every record has an IRI (…/divisions/<division>/<resource>/<id>) and links to related records (an invoice's customer, a contact's worksFor) by IRI. Terms no standard vocabulary covers, such as isSupplier, use https://mvdw-software.com/exact/data-bridge/vocab#. The JSON-LD context is inline, so nothing has to be fetched to read it. Filters, sorting and grants work the same in every format; a media type the API cannot serve gets 406 resource.not_acceptable.

curl -H "Authorization: Bearer exg_live_…" -H "Accept: text/turtle" \
  "https://apps.mvdw-software.com/exact/data-bridge/api/v1/divisions/321654/accounts?limit=2"

Errors

Errors are { "error": { "code": …, "message": … } }:

Status Code Meaning
400 resource.bad_parameter, resource.bad_limit a query parameter is repeated, unknown or out of range
401 credential.invalid the key is unknown, revoked or the wrong kind
402 billing.inactive the organization's subscription is not active
403 access.denied the key's grants do not cover this data type, level or administration
403 credential.ip_not_allowed the key has an IP allowlist and this address is not on it
404 exact.not_found the record does not exist in Exact
429 exact.rate_limited Exact's own limit; the answer carries Retry-After, wait and retry
429 exact.paused too many recent errors on that Exact endpoint; the call is paused for a while

The API is rate limited per key (60 calls a minute) on top of Exact's own limits.

API keys

Keys live under Access → API keys:

The Access page, API keys tab: the keys table with access chips, the New API key button and the show-revoked toggle

  • Create: name, access preset (or the custom matrix), administrations, optional IP allowlist. The key is shown once.
  • Revoke: a key can be revoked at any time; consumers using it stop working immediately. Revoked keys stay in the list (toggle Show revoked keys) until you know no consumer uses them.
  • Grants are per key: two integrations in one organization can have completely different access: the billing script reads only sales-invoices, the AI assistant reads accounts and contacts.

The custom matrix groups the data types by domain (CRM, Sales, Logistics, Financial) and offers READ / WRITE / DELETE per type. WRITE and DELETE grants are stored already, but the API is read-only for now.

MCP for AI tools

The MCP server speaks the standard Model Context Protocol (Streamable HTTP). Its tools are generated from the data types and filtered by the caller's grants:

  • list_divisions, the administrations the caller may use;
  • list_accounts, get_accounts, list_contacts, …, one list and one get tool per data type the caller may read;
  • every tool takes the same filters as the REST list endpoints (and the division).

An AI client sees exactly what its grants allow, nothing more, and every tool call is checked again at call time, so a revoked key or a demoted administrator stops working immediately.

Authentication

Two ways, choose per tool:

  1. API key: the client sends Authorization: Bearer exg_live_…. Simplest; the key's grants apply.
  2. OAuth 2.1: the client does a standard authorization-code flow; no key in the client at all. This is the recommended way for tools that support it (Claude, ChatGPT, …).

The server advertises itself for OAuth clients:

  • discovery: https://apps.mvdw-software.com/.well-known/oauth-authorization-server/exact/data-bridge/api/oauth
  • protected resource metadata: https://apps.mvdw-software.com/.well-known/oauth-protected-resource/exact/data-bridge/api/mcp

With OAuth, the first connection opens a consent page for an organization owner or administrator (with two-factor authentication):

The MCP consent page: the client's name and redirect host, the signed-in user, the organization and the access presets

  • the page names the client and its redirect host (will receive access), and who is signed in;
  • the administrator chooses the organization (when there is more than one) and the access: Read only (default), Read & write or Custom (the full matrix);
  • Allow connects the tool; Deny refuses it.

Every MCP call re-checks with the portal that the approving administrator is still an owner/administrator with two-factor enabled; if that is no longer the case, the tool stops. The connected app appears under Access → Connected apps, where it can be revoked at any time.

Per-tool setup

The dashboard's MCP tab has the server URL and the two common clients, ready to copy:

Claude Code:

claude mcp add --transport http exact-gateway https://apps.mvdw-software.com/exact/data-bridge/api/mcp

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "exact-gateway": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://apps.mvdw-software.com/exact/data-bridge/api/mcp"]
    }
  }
}

Any other MCP client that speaks Streamable HTTP works the same way: point it at the server URL and either send the API key as a bearer token or complete the OAuth flow.

The cache

Under Cache the organization can opt in to a short-lived cache of list answers:

  • off by default: every call goes to Exact;
  • when on, repeated list calls within the TTL (up to 24 h) are answered from the cache instead of Exact;
  • the cache is encrypted, per organization, and is purged automatically when the Exact connection is disconnected or needs re-authorization, so a stale copy can never cross organizations;
  • Purge now clears it by hand (for example after a big import in Exact).

The dashboard shows whether the cache is on, and the Usage page shows the cache hit rate.