control-air / laravel-intacct
Laravel integration for the Sage Intacct PHP SDK.
Package info
github.com/control-air-enterprises/laravel-intacct
pkg:composer/control-air/laravel-intacct
Requires
- php: ^8.5
- control-air/intacct-php-sdk: dev-main
- guzzlehttp/guzzle: ^7.10
- illuminate/support: ^13.0
Requires (Dev)
- larastan/larastan: ^3.12
- laravel/pint: ^1.24
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-08 21:04:11 UTC
README
Laravel integration for the Sage Intacct PHP SDK.
This package does not wrap or duplicate the SDK. It builds the SDK objects from Laravel configuration, stores OAuth tokens encrypted in the database, and lets the application inject IntacctClient directly.
Requirements
- PHP 8.5+
- Laravel 13+
Installation
composer require control-air/laravel-intacct
The service provider and the Intacct facade are registered automatically through Laravel package discovery.
Publish and run the token table migration:
php artisan vendor:publish --tag=intacct-migrations php artisan migrate
Configuration
A single Sage Intacct company only needs environment variables:
SAGE_INTACCT_CLIENT_ID= SAGE_INTACCT_CLIENT_SECRET= SAGE_INTACCT_COMPANY_ID= SAGE_INTACCT_USER_ID= SAGE_INTACCT_ENTITY_ID=
| Variable | Required | Description |
|---|---|---|
SAGE_INTACCT_CLIENT_ID |
Yes | OAuth client ID of the Sage application |
SAGE_INTACCT_CLIENT_SECRET |
Yes | OAuth client secret of the Sage application |
SAGE_INTACCT_COMPANY_ID |
Yes | Intacct company ID |
SAGE_INTACCT_USER_ID |
Yes | Intacct Web Services user ID |
SAGE_INTACCT_ENTITY_ID |
No | Entity to work in, for multi-entity companies |
SAGE_INTACCT_TOKEN_KEY |
No | Row key used to store the tokens. Defaults to default |
SAGE_INTACCT_CONNECTION |
No | Name of the default connection. Defaults to default |
SAGE_INTACCT_TOKEN_STORE |
No | Token store driver. Only database is supported |
These variables feed the connection named default. Publishing the configuration file is only needed to add more connections:
php artisan vendor:publish --tag=intacct-config
Usage
Inject IntacctClient. It resolves the default connection:
use ControlAir\Intacct\IntacctClient; use ControlAir\Intacct\Resources\Projects\Project; use ControlAir\Intacct\ValueObjects\ObjectKey; final class ProjectService { public function __construct( private readonly IntacctClient $intacct, ) {} public function find(string $key): Project { return $this->intacct->projects->get(new ObjectKey($key)); } }
The client exposes the SDK API unchanged:
$intacct->projects->get(...); $intacct->projects->query(...); $intacct->construction->projectContracts->query(...); $intacct->accountsPayable->vendors->query(...);
Resources, queries and DTOs are documented in the SDK repository.
Multiple connections
Each connection has its own OAuth application, company, user, entity and stored tokens.
-
Publish the configuration file:
php artisan vendor:publish --tag=intacct-config
-
Add the connection to
config/intacct.php:'connections' => [ 'default' => [ // ... ], 'company-b' => [ 'client_id' => env('SAGE_INTACCT_B_CLIENT_ID'), 'client_secret' => env('SAGE_INTACCT_B_CLIENT_SECRET'), 'company_id' => env('SAGE_INTACCT_B_COMPANY_ID'), 'user_id' => env('SAGE_INTACCT_B_USER_ID'), 'entity_id' => env('SAGE_INTACCT_B_ENTITY_ID'), 'token_key' => 'company-b', ], ],
Every connection needs its own
token_key. Two connections sharing a key overwrite each other's tokens. -
Add the new variables to
.env.
Resolve a named connection through the manager:
use ControlAir\LaravelIntacct\IntacctManager; $intacct = app(IntacctManager::class)->connection('company-b');
Calling connection() without a name returns the default connection. Each connection is built on first use and reused afterwards.
Facade
The Intacct facade proxies IntacctManager. Dependency injection is preferred.
use ControlAir\LaravelIntacct\Facades\Intacct; Intacct::connection()->projects->query(...); Intacct::connection('company-b')->projects->query(...);
Tokens
The package obtains and renews OAuth tokens on its own. Before each request:
- A stored token with more than 60 seconds left is used as is.
- Otherwise the package takes the cache lock
intacct:token-refresh:{token_key}, so only one process renews the token at a time, and then:- uses the token another process stored while this one was waiting, or
- refreshes it with the refresh token when Sage issued one, or
- requests a new token with the client credentials of the connection.
The cache store must support atomic locks (redis, database, file or array). A process that waits more than 30 seconds for the lock fails with Illuminate\Contracts\Cache\LockTimeoutException.
Tokens are stored in the intacct_tokens table, one row per token_key. Access and refresh tokens are encrypted with the application's APP_KEY, so rotating APP_KEY invalidates the stored tokens.
Reference
IntacctManager
Every method takes an optional connection name. Without one, it uses the default connection from intacct.default. Objects are built on first use and cached per connection.
| Method | Returns | Description |
|---|---|---|
connection(?string $name = null) |
IntacctClient |
The SDK client for the connection |
application(?string $name = null) |
OAuthApplication |
The OAuth client ID and secret |
oauthClient(?string $name = null) |
OAuthClient |
The SDK OAuth client, for token, revoke and introspection calls |
tokenManager(?string $name = null) |
TokenManager |
The SDK token manager bound to the database token store. It does not take the token lock |
tokenKey(?string $name = null) |
TokenKey |
The key the connection's tokens are stored under |
getDefaultConnection() |
string |
The name of the default connection |
use ControlAir\LaravelIntacct\IntacctManager; $manager = app(IntacctManager::class); $manager->connection(); // default connection $manager->connection('company-b'); // named connection $manager->tokenManager('company-b'); // TokenManager of company-b $manager->tokenKey('company-b')->value; // 'company-b'
Container bindings
| Abstract | Resolves to |
|---|---|
IntacctManager |
Singleton manager |
IntacctClient |
IntacctManager::connection() |
OAuthApplication |
IntacctManager::application() |
OAuthClient |
IntacctManager::oauthClient() |
TokenManager |
IntacctManager::tokenManager() |
ControlAir\Intacct\Auth\Contracts\TokenStore |
DatabaseTokenStore |
Psr\Http\Client\ClientInterface |
GuzzleHttp\Client |
Psr\Http\Message\RequestFactoryInterface |
GuzzleHttp\Psr7\HttpFactory |
Psr\Http\Message\StreamFactoryInterface |
GuzzleHttp\Psr7\HttpFactory |
Psr\Clock\ClockInterface |
ControlAir\Intacct\Support\SystemClock |
The SDK objects in this table always belong to the default connection. Use IntacctManager for any other connection.
Publishable assets
| Tag | Publishes |
|---|---|
intacct-config |
config/intacct.php |
intacct-migrations |
The intacct_tokens table migration |
Errors
| Exception | When |
|---|---|
ControlAir\Intacct\Exceptions\ConfigurationException |
The connection does not exist, client_id, company_id, user_id or token_key is missing, the token store driver is not database, or the cache store does not support locks |
Illuminate\Contracts\Cache\LockTimeoutException |
Another process held the token lock for more than 30 seconds |
Testing
composer test
composer lint