Connecting to Exact Online, NetSuite and Business Central
The differences between the APIs and authentication of three ERP systems, for developers who have to build against them.
9 November 2026 · 6 min read
As part of Eagl’s engineering team, I build and maintain integrations with Exact Online, NetSuite and Business Central. This post goes through what the public documentation says and where the three systems differ. Eagl’s code and customers do not come into it. These APIs change, so check the details in the official documentation before you build on them.
Exact Online
The REST API works in OData style, with query parameters such as $select, $filter and $top. Two things decide the address. Exact has its own domain per country, so a Belgian customer is on a different domain from a Dutch one. An administration is called a division and sits in the path. You ask current/Me which division the logged-in user is active on.
Authentication is OAuth 2.0 with the authorization code flow, so a user at the customer has to log in and give consent. The access token is short-lived, around ten minutes at the time of writing. Request a new one only shortly before it expires, because asking too early returns an error. The refresh token rotates: each refresh gives you a new one, and the old one is then invalid.
That last point needs care as soon as you have more than one process. If two workers use the same refresh token at once, one loses, and at worst the customer has to log in again. Serialise the refresh per connection, and store the new tokens before you use them.
// Simplified illustration, not Eagl's code.
async function getAccessToken(connectionId: string) {
return withLock(`token:${connectionId}`, async () => {
const conn = await db.connections.get(connectionId);
if (conn.accessTokenExpiresAt > Date.now() + 30_000) return conn.accessToken;
const fresh = await exchangeRefreshToken(conn.refreshToken);
await db.connections.update(connectionId, fresh);
return fresh.accessToken;
});
}
Limits apply per minute and per day, and the responses tell you through X-RateLimit headers how much you have left. Read those headers and do not hard-code the numbers in your client. For large volumes, some resources have separate bulk and sync endpoints, where sync lets you read on from a timestamp. On some endpoints you have to choose the fields explicitly with $select. Next to the REST API there is also an older XML interface.
NetSuite
NetSuite has several ways in. SuiteTalk REST works with records. SuiteQL is an SQL-like query language you reach through the REST services, and often the handiest way to read large volumes. RESTlets are your own SuiteScript endpoints that you put in the customer’s account, for what the standard API cannot do. That does mean deploying code in that account.
SOAP is being phased out. According to Oracle’s FAQ, new integrations should run on REST and OAuth 2.0 from release 2026.1. From 2027.1 you can no longer build new integrations with token-based authentication, which SOAP requires. With 2028.2 all SOAP endpoints are disabled. TBA is the older form in which you sign every request in OAuth 1.0 style. Existing REST integrations on TBA do not have to move yet, so you will still come across it.
The address depends on the account: each account has its own domain under suitetalk.api.netsuite.com, and a sandbox is a separate account with its own id.
What sets NetSuite apart most is how far accounts can differ from each other. Custom fields, custom records and custom segments mean two customers can store the same concept differently. The role of your integration decides what you see, so the same query can work at one customer and give an error or fewer rows at another. Test with the role the customer gives you, because an administrator sees more.
The limit is concurrency at account level, which you share with the customer’s other integrations. Over the limit you get HTTP 429, and you try again later. The governanceLimits operation lets you look up the account’s limit.
Business Central
This is about the online version. The on-premises version can differ on a number of points.
The standard API v2.0 is REST based on OData v4. The address contains the tenant, the environment and the company:
https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/v2.0/companies({id})/generalLedgerEntries
A tenant can have several environments, such as production and sandboxes, and each environment several companies. Store all those levels per connection.
Authentication runs through Microsoft Entra ID. For an integration with no user, you use service-to-service authentication with the client credentials flow. That takes two steps: an app registration in Entra with the right API permissions, and then adding the client ID on the Microsoft Entra applications page in Business Central, with its own permission sets. You cannot assign the SUPER permission set there. Expect the customer to have to do this in their own tenant.
What the standard API does not offer, you can add with custom API pages in an AL extension, which you then have to install at the customer. With a PATCH you send the ETag in If-Match.
Business Central also has webhooks. A subscription expires after three days unless you renew it, and both creating and renewing require a handshake with a validationToken. Notifications arrive with some delay and grouped, and with many changes you get one notification of type collection and have to fetch the changes yourself. Watch the error codes from your receiver above all: only 408, 429 and 5xx lead to retries. Any other code deletes the subscription.
One concept, three names
The separation between companies is called division in Exact Online, subsidiary in NetSuite OneWorld and company in Business Central. They do not sit in the same place either: with Exact and Business Central they are in the address, with NetSuite they are data within one account.
The same goes for analytical breakdowns. Exact Online has cost centres and cost units, NetSuite has departments, classes, locations and custom segments, and Business Central has dimensions. An internal model that flattens this throws away information an accountant will need later. Keep the source ID and preferably the raw data next to your own shape.
What comes back in all three
For incremental reads, keep a cursor per connection and per resource. Add an overlap in time and make your writes idempotent on the source ID, so you can safely process the same row twice. A filter on a modified date does not show deleted records, so those need a separate approach.
Store tokens encrypted and per customer. Also show the status of the connection somewhere in your product, because a connection that needs new consent is something a person at the customer has to fix.
Handle limits per connection with a queue of its own. On a 429 you wait, and you follow Retry-After if it is there.
Start with read access. Writing to a set of accounts takes more care: you cannot simply throw away a duplicate booking as a duplicate row, and depending on the system and the status of a booking you correct it with a reversal. Periods can be closed, so a write can rightly be refused. Test against a test environment: Business Central has sandbox environments, NetSuite has sandbox accounts that not every customer has, and with Exact Online you check which test administration you can get.
Read next
What's your idea?
Tell me what you're trying to build. I try to reply within one working day.