Download the PHP package picqer/exact-php-client without Composer
On this page you can find all versions of the php package picqer/exact-php-client. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download picqer/exact-php-client
More information about picqer/exact-php-client
Files in picqer/exact-php-client
Package exact-php-client
Short Description A PHP Client for the Exact API
License MIT
Homepage http://github.com/picqer/exact-php-client
Informations about the package exact-php-client
Exact Online PHP client
A PHP client for the Exact Online REST API. Use it to create invoices, book journal entries, sync items, stock and relations, and anything else the API offers.
The client follows the naming of the Exact Online API. Every endpoint is a class with the same name, and every field has the same name as in the API. The Exact API reference is therefore also the reference for this library.
Created and maintained by Picqer.
Contents
- Installation
- Quick start
- Authentication
- Divisions (administrations)
- Other countries
- Reading data
- Creating, updating and deleting
- Related data and files
- Examples
- Rate limits
- Error handling
- Webhooks
- Troubleshooting
- Versions and upgrading
- Contributing
Installation
Requires PHP 7.4 or higher and Guzzle 7.15.2 or higher.
Quick start
- Create an app in the Exact Online App Center to get a client ID and client secret, and set the redirect URL to a page of your application.
- Send the user to Exact to authorize your app. Exact redirects back to your redirect URL with an authorization code.
- Exchange the code for tokens, store them, and start calling the API.
Once you have tokens, using the API looks like this:
A complete, runnable version of the authorization flow is in example/example.php.
Authentication
Exact Online uses OAuth 2.0. A user only needs to authorize your app once. After that, the library keeps the connection alive with the refresh token.
1. Redirect the user to Exact
Use $connection->getAuthUrl() instead if you want to do the redirect yourself. Use setForceLogin(true) to always show Exact's login screen.
2. Handle the callback
Exact redirects back to your redirect URL with a code query parameter. The code can only be used once, so exchange it for tokens right away:
3. Connect with stored tokens
For every later request, create the connection from your stored tokens:
The access token is valid for about 10 minutes. The library refreshes it automatically just before it expires, using the refresh token.
[!IMPORTANT] Always set the token update callback. Every refresh gives you a new refresh token and invalidates the old one. If you don't store the new refresh token, your next connection uses an invalid token, and the user has to authorize your app again.
Refreshing tokens from multiple processes
If several processes (queue workers, cron jobs, web requests) share the same tokens, two of them can refresh at the same time. One of them then ends up with an invalidated refresh token. Prevent this with a lock around the refresh:
Divisions (administrations)
Almost every endpoint works within one division (administration). Set it explicitly:
If you don't, the library asks Exact for the user's current division on every new connection. That costs an extra API call, and the result changes when the user switches administrations in Exact Online.
List the divisions the user has access to with the Division entity (system/Divisions), or all divisions of the license with AllDivision (system/AllDivisions).
Other countries
The library connects to the Dutch Exact Online site by default. For other countries, set the base URL before authorizing:
| Country | Base URL |
|---|---|
| Netherlands | https://start.exactonline.nl |
| Belgium | https://start.exactonline.be |
| Germany | https://start.exactonline.de |
| United Kingdom | https://start.exactonline.co.uk |
| United States | https://start.exactonline.com |
| Spain | https://start.exactonline.es |
| France | https://start.exactonline.fr |
Reading data
Every endpoint is a class in Picqer\Financials\Exact. See src/Picqer/Financials/Exact for the full list.
find(), first(), get() and filter() return new objects. They don't fill the object you call them on.
Filter options
Filters use the OData syntax that Exact Online supports. Write GUIDs as guid'…' and strings as '…'. The Filter column in the API reference shows which fields you can filter on.
Large data sets
A regular endpoint returns 60 records per page. get() and filter() fetch all pages, which can take a lot of memory. Use the generator variants to handle records one at a time:
getAsGenerator() works the same way. To control the paging yourself, for example to spread requests over time:
Sync and bulk endpoints
For large data sets, Exact recommends the sync and bulk endpoints. They return 1000 records per page.
Sync endpoints (classes named Sync…) return records that are new or changed since a given Timestamp. Store the highest Timestamp you've seen and continue from there next time. The timestamp is a row version, not a date.
Deleted records are available through SyncDeleted.
Bulk endpoints (classes named Bulk…) require you to select the fields you need:
Creating, updating and deleting
Set fields as properties and call save(). A record without a primary key is created (POST), a record with one is updated (PUT). After saving, the object contains the record as Exact returned it, including the new ID.
Only fields that exist in the API are sent. Unknown properties are ignored.
Records with lines
Some endpoints, like sales invoices and sales orders, need their lines in the same request when you create them:
Lines are only sent when creating a record. To change the lines of an existing record, use the line endpoint, such as SalesInvoiceLine.
Related data and files
Related collections
Related collections such as SalesInvoiceLines are loaded the first time you use them:
That is an extra API call per record. When you need the lines of many records, include them in the same request with $expand:
If loading fails, for example because of a rate limit, an ApiException is thrown.
Downloading files
Items and document attachments can be downloaded:
Examples
Create, print and email a sales invoice
Invoice sales orders (async)
As of October 2026 Exact only supports the async mode of the InvoiceSalesOrders endpoint. Starting the process returns a ProcessID, which you use to fetch the result later.
Rate limits
Exact Online limits the number of API calls per division, per minute and per day. The limits depend on your agreement with Exact. See API limits in Exact's documentation.
After each request, the connection holds the current limits from the response headers:
When you exceed the minutely limit, Exact only sends the minutely headers. The daily values are then 0 until the next minute.
The library can wait automatically when the minutely limit is reached:
The daily limit is not handled automatically. When you reach it, an ApiException with code 429 is thrown. Wait until getDailyLimitReset() before trying again.
Error handling
Every error is thrown as a Picqer\Financials\Exact\ApiException. For HTTP errors, the exception code is the HTTP status code, and the message contains Exact's error message.
If refreshing the tokens fails because the refresh token is no longer valid, you get an ApiException with code 400 and invalid_grant in the message. The user then has to authorize your app again.
Connection problems, like DNS errors and timeouts, are also thrown as ApiException, with code 0. The original exception is available through $e->getPrevious().
To log every request and response, add a Guzzle middleware:
Webhooks
Subscribe to changes with the WebhookSubscription entity:
Check that incoming webhook calls come from Exact with the Authenticatable trait and your webhook secret from the App Center:
Troubleshooting
Error 400: Please add a $select or a $top=1 statement to the query string
Some endpoints require you to limit the fields or the number of records. Select the fields you need, or request one record:
Error 400 when exchanging the authorization code
An authorization code can only be used once. If you exchange the same code again, Exact responds with 400 Bad Request. Exchange the code once, store the tokens, and connect with the stored tokens from then on.
Error 400 with invalid_grant after some time
The refresh token is no longer valid. This usually happens when a new refresh token wasn't stored after a refresh (see Refreshing tokens from multiple processes). The user has to authorize your app again.
A field isn't saved
Check that the field exists in the API reference of the endpoint. The library only sends fields it knows about. If the field is missing from the entity class, please open a pull request.
Server maintenance
Exact Online is unavailable for maintenance every day between 04:00 and 04:30 CET. Avoid scheduling API calls in that window.
Versions and upgrading
| Version | PHP | Guzzle |
|---|---|---|
| 4.8 and higher | 7.4 or higher | 7.15.2 or higher |
| 4.0 – 4.7 | 7.4 or higher | 6 or 7 |
| 3.x | 7.2 or higher | 6 or 7 |
| 1.x | 3 |
Changes that can affect your code are listed under "Behaviour changes" in the release notes. For example:
- 4.7: lazy loading and
download()throw anApiExceptioninstead of failing silently or throwing Guzzle exceptions. - 4.8: Guzzle 7.15.2 or higher is required.
Contributing
Pull requests are welcome, especially for missing endpoints and fields. Please add tests for your changes and make sure these pass:
Entities are generated from the Exact Online API reference, using DannyvdSluijs/PicqerExactPhpClientGenerator.
License
MIT. See LICENSE.