The Embedded Finance Index is live: 6,292 companies and 1,383 providers, mapped across the stack. Explore the Index →

Exact Online API Integration Guide: OAuth, Sync, Limits

Exact Online API guide: country hosts, app review, 10-minute OAuth tokens, divisions, Sync and bulk endpoints, webhooks, and the 60/min and 5,000/day limits.

GJGJ

GJ · Co-founder, Apideck

14 min readView as .md
Exact Online API Integration Guide: OAuth, Sync, Limits

An Exact Online access token lives for 600 seconds, and Exact rejects any attempt to refresh it before 570 of them have passed, per its token documentation. That one rule tells you how the Exact Online API wants to be used: one patient client per company that reads sequentially and tracks its tokens to the second. Everything below follows Exact's own documentation as read in October 2026. The January 2026 version of this post said Exact has no $expand and paginated with $skip. Both were wrong, and the corrections are in the section on reading data.

Seven country instances, one app per country

Exact Online runs each country on its own website with its own host, per Exact's developer subscription page:

CountryHost
Netherlandshttps://start.exactonline.nl
Belgiumhttps://start.exactonline.be
Germanyhttps://start.exactonline.de
United Kingdomhttps://start.exactonline.co.uk
United Stateshttps://start.exactonline.com
Spainhttps://start.exactonline.es
Francehttps://start.exactonline.fr

The OAuth endpoints and every REST call sit on that host, and Exact's docs shorten it to .., so ../api/oauth2/auth means https://start.exactonline.nl/api/oauth2/auth for a Dutch customer. App registrations belong to one instance as well. Apideck's Exact Online setup guide puts it plainly: an app registered in one country cannot connect customers in another, so you register one app per country you serve and route each customer to the host where they sign in.

Exact Online's seven country hosts, with the App Store still open in the Netherlands, Belgium and Germany and closed in the UK, US, Spain and France since 2 September 2026

The App Store also differs by country now. "From 2 September 2026, the Exact App Store will no longer be supported in Spain, France, the United Kingdom and the United States," Exact announced on its App Store countries page, adding that existing apps and integrations keep working and that the Netherlands, Belgium and Germany keep the store.

Registering an app and getting it reviewed

Exact supports two kinds of app, per Register your app. An internal app is an API key that a customer registers in their own Exact Online environment, under Import/Export > App registrations, and it only ever reaches that customer's data. An external app is registered through the App Store and can be linked to any Exact Online subscription, and only developers with an Exact Online developer subscription can create one. If you are building a product for many customers, you need the external kind.

In the App Store, go to Manage my apps and choose between "Register for a testing app", which external clients cannot use, and "Register a production app". The client ID and secret sit on the app's Develop your app tab. The redirect URI must be a public HTTPS URL; Exact answers localhost or plain HTTP with a 401. Exact also advises at most four registrations per app, for example one per DTAP stage, in its API limits article.

Until Exact reviews it, a new app can only authorize your own Exact Online account. The review process starts with a Data & Security review. There you describe the app's purpose and declare its scopes per API domain, each as read or read and write, then list any outside platforms that process customer data and answer a security questionnaire. After that you request permission. Every later change to that form needs a new approval, and your users see the consent screen again. The same Apideck setup guide says Exact usually completes a review within five working days, so plan pilot customers around it.

Publishing in the App Store is a separate, optional step, and an approved production app can serve external customers without a listing. Publication brings a monthly app fee "based on the number of companies that connect to your app" and a demo with a partner manager, per Get published. The developer subscription itself costs EUR 15 a month excluding VAT in the Netherlands and includes test administrations and up to 100 linked administrations, per Exact's Dutch developer page.

OAuth 2.0 with 10-minute access tokens

Exact supports the authorization code and implicit grants only. Client credentials and resource owner password grants are not supported, per Exact's OAuth overview, so every connection starts with a person signing in. This is Exact's own authorization request from step 2:

GET https://start.exactonline.nl/api/oauth2/auth?client_id=b81cc4de-d192-400e-bcb4-09254394c52a&redirect_uri=https%3A%2F%2Fwww.mycompany.com%2Fmyapplication&response_type=code&force_login=0

The user signs in (two-step verification included), and the consent screen lets them pick which divisions your app may read. The redirect carries a code that is valid for three minutes and can be used once. Exchange it at the token endpoint with a form-encoded POST. Exact's example puts the client ID and secret in the body, not in a Basic auth header:

curl -X POST "https://start.exactonline.nl/api/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "code=<code from the redirect, URL-decoded>" \
  --data-urlencode "redirect_uri=https://www.mycompany.com/myapplication" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=<client_id>" \
  --data-urlencode "client_secret=<client_secret>"

Exact notes that the code arrives URL-encoded and must be decoded first, so %21 becomes !. The response holds an access_token with expires_in of 600 and a refresh_token. Send the access token as Authorization: Bearer <access_token>.

The refresh rules are where background sync breaks. A refresh before 9 minutes 30 seconds returns 401 "Access Token not expired", and a call with an expired token returns 401 "Authentication Required". Exact advises refreshing in the last 30 seconds of the token's life. Step 4 shows the refresh request with grant_type=refresh_token and states that each refresh returns a new refresh token and invalidates the old one. Refresh tokens also expire after 30 days, and "if the app is not used in 30 days, you must make a new authorization request to the user."

For a background sync, store the latest refresh token atomically and let one process own each connection, because two workers refreshing the same token leave one of them holding a dead token. Refresh dormant connections on a schedule shorter than 30 days, or seasonal users will have to sign in again.

Finding the division

Almost every Exact Online REST URL includes a division, the API's name for an administration (a company's books). The REST reference lists the endpoints for finding one:

/api/v1/current/Me?$select=CurrentDivision
/api/v1/{division}/system/Divisions
/api/v1/{division}/system/AllDivisions

current/Me returns the division the user is working in and needs no division in its URL, which is also why Exact doesn't count it toward the daily limit. system/Divisions lists the divisions the user can reach and granted your app, and for an accountant that includes client administrations. system/AllDivisions covers the whole license.

The consent screen matters here. Exact's implementation overview walks through a user who can open several divisions but scopes the app to only some of them: calls to an unscoped division return "User division is not within division scope", and calls to a division the user can't access return "Wrong division". Store the division per connection, and let users pick from the scoped list instead of assuming the current one.

Reading data with the Exact Online API

Exact's REST API speaks OData. Ask for the fields you need with $select (Exact advises against *), filter with $filter, and pull header and lines in one call with $expand. These examples are from Exact's query string options page:

GET .../api/v1/{division}/purchaseentry/PurchaseEntries?$filter=AmountFC ge -8500&$select=EntryID,Description,AmountFC,DueDate
GET .../api/v1/{division}/purchaseentry/PurchaseEntries?$expand=PurchaseEntryLines&$select=EntryID,Description,AmountFC,DueDate

You can't filter on an expanded collection or on a navigation property, and $orderby "will significantly decrease the performance of a request", so sort on your side.

A regular endpoint returns 60 records per page. Don't page with $skip: Exact dropped it for endpoints released after 1 March 2017 "to prevent parallel requests". Each page instead carries a __next link that repeats your $select and $filter with a $skiptoken, and Exact's OData best practices say to follow it one page at a time, because the API "should be accessed in a synchronous way."

For volume, Exact offers two other endpoint families with 1,000 records per page, per its API types article. Bulk endpoints (/api/v1/{division}/bulk/..., 21 in the reference) return records that match a filter and require $select. Sync endpoints (/api/v1/{division}/sync/..., 61 of them) return only records that changed since a timestamp you pass. The timestamp is a row version and has no relation to a date. Store the highest value you received and send it next time. The sync sales invoice reference says a first sync filters on a timestamp greater than 1, or starts from a date through the SyncTimestamp endpoint, and that the timestamp is the only parameter a sync endpoint accepts.

Sync never returns deletions, so pair it with sync/Deleted. And when Exact moves a division to another database the timestamps reset, so check DivisionMoveDate on system/Divisions and run a full resync when it changes. Where a sync endpoint exists, Exact also makes $filter mandatory on the matching single and bulk endpoints.

Bar chart of the calls needed to read 10,000 sales invoices from Exact Online: 167 calls on a regular endpoint at 60 per page against 10 calls on a bulk or sync endpoint at 1,000 per page, measured against limits of 60 calls a minute and 5,000 a day per company

Reporting data has its own endpoints. financial/ReportingBalance returns summarized balances and period totals per G/L account and reporting period, and Exact advises filtering on the reporting year only, per the ReportingBalance reference. read/financial/AgingReceivablesList returns each customer's outstanding amount by age group, the same data as Exact's A/R ageing report, per its reference page, and AgingPayablesList does the same for suppliers.

Writing a sales invoice

A POST to the SalesInvoices endpoint must name the customer in OrderedBy, a sales Journal, and at least one line, per its reference page. This is the request body from Exact's sales invoice walkthrough, sent to ../api/v1/{division}/SalesInvoice/SalesInvoices:

{
  "Description": "Orders of Bundy Shoes",
  "OrderedBy": "632a807b-2d69-4a80-acb6-d62f4730ec0c",
  "Journal": 70,
  "SalesInvoiceLines": [
    {"Item": "06e78875-0809-42ee-a758-c78e87623a2f", "Quantity": "10"},
    {"Item": "17387283-97eb-4128-9a0e-77ad5e7ff7ee", "Quantity": "12"},
    {"Item": "53c074fe-8999-4b1c-823b-21707fc5c363", "Quantity": "15"}
  ]
}

Exact answers 201 Created and calculates the totals itself. InvoiceTo defaults to OrderedBy, and the walkthrough finds the customer's GUID first with crm/Accounts?$select=ID,Name,Code&$filter=IsSales eq true and trim(Code) eq '26', trimming because Code is a fixed-width field.

The invoice is now open, not booked. A POST to salesinvoice/PrintedSalesInvoices with {"InvoiceID": "<the invoice GUID>"} processes it, which assigns the invoice number (depending on the administration's settings) and creates the financial entries. If you only want to book an invoice your own system already sent, Exact's reference points you to the SalesEntries API instead. A single POST can be up to 10 MB.

Exact's REST API has no endpoint for matching a payment to an invoice. Matching goes through Exact's older XML API (the MatchSets topic) or happens in Exact Online itself, so plan for it before you write payments.

Webhooks

Exact Online has native webhooks. In the REST reference, 39 endpoints are marked as having one, sales invoices and transaction lines among them. Subscribing is a POST per division, from Exact's webhooks tutorial:

POST …/api/v1/{division}/webhooks/WebhookSubscriptions
{
  CallbackURL: 'https://MyApp/WebHooks',
  Topic: 'Accounts'
}

The callback URL must use HTTPS and share the URL and domain of your app's redirect URI. Exact validates it with an empty POST and expects a 200 or 201. A notification names the record and an Action of UPDATE or DELETE, not the changed data, and you fetch the record from the endpoint it gives, using the user's own token. Verify each one: the HashCode is an HMAC SHA256 over the JSON of the Content node, keyed with the webhook secret from your app registration, per the webhooks overview. Failed deliveries are retried 10 times with exponentially growing gaps over 34 hours. Exact Online is also offline daily from 4:00 to 4:30 local time, so a monitor that alarms at 4:15 is checking the wrong thing.

Keep the Sync API running on a schedule next to your webhooks, so it catches anything you missed while your endpoint was down.

Rate limits and error budgets

Exact limits each app per company: 60 calls a minute and 5,000 a day, per the API limits article. All of your app's users on one company share that budget, and running out on one company doesn't affect others. Over the limit you get HTTP 429. Responses to calls with a division in the URL carry the minute's budget in X-RateLimit-Minutely-Limit and X-RateLimit-Minutely-Remaining. The daily budget comes in X-RateLimit-Limit and X-RateLimit-Remaining, and X-RateLimit-Reset gives the end of the daily window in UTC epoch milliseconds, not seconds. The daily window resets at the start of the next day.

Customers on an Exact Online Premium licence get up to 30,000 calls a day in total with no per-app cap, and a fair-use ceiling of 20,000 calls a day applies per Exact Online contract. Exact says 97.5% of its customers never reach the 5,000 limit.

The limit that catches integrations in production is the error budget. Exact allows "no more than 10 errors per API key, per user, per company, per endpoint, and per hour," and client errors such as 400 and 404 count. Past that, Exact blocks the key for an hour and lengthens the block if the errors continue. A sync job that retries a bad request in a loop can lock itself out of a customer for hours. Retry 429s with exponential backoff and a cap on attempts, and log 4xx responses as bugs instead of retrying them.

At 60 calls a minute, a regular endpoint reads 3,600 records a minute. Sync and bulk endpoints raise that to 60,000, which is why Exact recommends sync endpoints wherever they exist.

Testing before you go live

Exact has no separate sandbox host. Its OAuth guide says "Sandbox or test environments are not available"; the developer subscription instead gives you test administrations on the normal Exact Online hosts. They're separate from your customers' books, but they run on the live service, so a test app needs the same care as a production one. Exact's getting started page also suggests a trial customer account for testing. Test on the same country instance as your first customers, since an app registered on the Dutch host can't authorize a Belgian login. Exact's App Store Partners dashboard has a webhook messages log that shows every notification and the HTTP response your endpoint returned, which is the fastest way to debug a subscription.

Exact Online through Apideck

Apideck's Exact Online API integration is live. The main connector lets each connection pick its country from six of the seven instances (France is not offered), and two more connectors are pinned to the Dutch and UK hosts. You still register your own Exact app per country and pass Exact's review. Apideck runs the OAuth flow with redirect URI https://unify.apideck.com/vault/callback and refreshes the 10-minute access tokens itself. It also renews refresh tokens before the 30-day expiry so idle connections stay connected. The connection stores the division the user was in when they authorized, and the x-apideck-company-id header targets another one per request.

According to the connector overview, write-back is limited to transactions such as invoices, bills and journal entries, while master data like customers and ledger accounts stays read-only and is maintained in Exact. When a payment carries an allocation, the connector uses Exact's MatchSets topic to reconcile it with its invoice or bill. Financial statement reads are supported, including the balance sheet and profit and loss. The connector does not surface Exact's webhooks, so you poll. Rate-limit responses carry Exact's per-minute budget in x-downstream-ratelimit-* headers and a Retry-After, so the backoff advice above still applies. An incremental invoice read looks like this:

curl --globoff "https://unify.apideck.com/accounting/invoices?filter[updated_since]=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer <APIDECK_API_KEY>" \
  -H "x-apideck-app-id: <APP_ID>" \
  -H "x-apideck-consumer-id: <CONSUMER_ID>" \
  -H "x-apideck-service-id: exact-online"

For endpoints outside the unified model, such as raw ReportingBalance rows or the aging lists, the Proxy API forwards a raw Exact request through the same connection and token.

If Exact Online is the only ledger you integrate and you need its webhooks or data outside accounting, build directly, with one sequential worker per division that reads through the Sync API. If Exact is one of several accounting systems your customers use, start with Apideck and keep direct Exact calls for what the unified model doesn't cover.

Ready to get started?

Scale your integration strategy and deliver the integrations your customers need in record time.

Ready to get started?
Talk to an expert

Frequently asked questions

Trusted by fast-moving product & engineering teams

Bonsai by Zoom
JobNimbus
Blue Zinc
Exact
Drata