laravel-saldeo maintained by beziworld
Laravel Saldeo
SaldeoSMART API-XML client for Laravel, built for accounting offices and ERP integrations:
- request signing (
req_sig, MD5 over sorted, URL-encoded parameters plus the API token) and the gzip + base64commandencoding, verified against both examples of the official specification - companies: list, create (with the welcome e-mail option), link existing companies with
COMPANY_PROGRAM_ID - contractors: list and merge (version 3.3 address structure)
- documents: list by policy, month id lists with
listbyidin chunks of 200, search, upload, OCR recognition and recognition results, back-references from the accounting program (document.sync), update, dimensions, deletion - API Engine: add and recognise a file without a company context
- invoices (all kinds, ids by month, add), bank statements with operations, dictionaries (categories, registers, payment methods, dimensions, articles, descriptions)
- multiple offices: named connections, runtime credentials with
using(), per-company scopes - latest documented operation versions by default, overridable per operation; generic
call()for everything else saldeo:fetch-documentscommand dispatchingDocumentFetchedevents for the scheduler
Why this package
There is no SaldeoSMART package on Packagist. The existing open-source clients are a C# wrapper (2021) and a Python MCP server. This package gives Laravel applications a typed, tested client of the official API-XML: it handles signing, XML and compression details, maps the official XML structures to DTOs and error codes to exceptions, and supports several accounting offices and companies at once.
Installation
composer require beziworld/laravel-saldeo
php artisan vendor:publish --tag=saldeo-config # optional
SALDEO_USERNAME=... # SaldeoSMART login
SALDEO_API_TOKEN=... # SaldeoSMART: account settings, API token
SALDEO_TEST=false # true = https://saldeo-test.brainshare.pl (developer accounts)
SALDEO_COMPANY_PROGRAM_ID= # default company for Saldeo::documents() and similar shortcuts
SALDEO_PRODUCER= # optional METAINF/PRODUCER sent with company.create
API access is available in selected SaldeoSMART plans (all plans for accounting offices). A dedicated integration profile is requested at api@saldeosmart.pl.
Several offices
// config/saldeo.php
'connections' => [
'default' => ['username' => env('SALDEO_USERNAME'), 'api_token' => env('SALDEO_API_TOKEN')],
'krakow' => ['username' => env('SALDEO_KRAKOW_USERNAME'), 'api_token' => env('SALDEO_KRAKOW_TOKEN')],
],
Saldeo::connection('krakow')->companies()->list();
Saldeo::using(['username' => $office->saldeo_login, 'api_token' => $office->saldeo_token])->company('ERP-17')->documents()->list();
Companies and contractors
use Beziworld\Saldeo\Data\{BankAccount, NewCompany, NewContractor};
use Beziworld\Saldeo\Facades\Saldeo;
$companies = Saldeo::companies()->list(); // list<Company>: id, programId, vatNumber, bankAccounts, ...
Saldeo::companies()->create([new NewCompany(
programId: 'ERP-17', username: 'acme-admin', email: 'biuro@acme.pl', shortName: 'ACME',
fullName: 'ACME Sp. z o.o.', vatNumber: '5260001246', city: 'Warszawa', postcode: '00-001', street: 'Prosta 1',
bankAccounts: [new BankAccount('PL61109010140000071219812874')], sendEmail: true,
)]);
Saldeo::companies()->synchronize([5202 => 'ERP-17']); // link an existing company (COMPANY_ID => COMPANY_PROGRAM_ID)
$company = Saldeo::company('ERP-17');
$company->contractors()->list();
$results = $company->contractors()->merge([
new NewContractor('KOWAL', 'Jan Kowal', street: 'Długa', buildingNumber: '5', postcode: '30-001', city: 'Kraków', programId: 'K-1', vatNumber: '6750001923', supplier: true),
]);
$results[0]->status; // CREATED, MERGED, CONFLICT, RECREATED or NOT_VALID; ->errors with paths
Documents and OCR
use Beziworld\Saldeo\Data\{DocumentSync, NewDocument};
use Beziworld\Saldeo\Enums\{DocumentPolicy, SplitMode};
$documents = Saldeo::company('ERP-17')->documents();
$list = $documents->list(); // policy SALDEO: documents marked for sending
foreach ($list->documents as $document) {
$document->number; $document->issueDate; $document->sum; $document->vatRegistries; $document->source;
$list->contractorOf($document)?->vatNumber;
}
foreach ($documents->month(2025, 3) as $page) { /* getidlist + listbyid in chunks of 200 */ }
$documents->search(['NUMBER' => 'FV/101/2025', 'NIP' => '5270201490']);
$added = $documents->add([NewDocument::fromPath(storage_path('fv.pdf'), 2025, 3)]);
$recognition = $documents->recognize([$added[0]->id], SplitMode::NoSplit)[0];
$result = $documents->recognized([$recognition->ocrOriginId]); // asynchronous: poll until isRecognized()
$documents->sync([new DocumentSync(saldeoId: (string) $document->id, accountDocumentNumber: 'FZ/1/2025', numberingType: 'ZAKUP', status: 'BOOKED')]);
$documents->delete([50000]);
// API Engine: recognition without a company context
Saldeo::addAndRecognize('5260001246', file_get_contents('fv.pdf'), SplitMode::NoSplit, 'COST');
Invoices, bank statements, dictionaries
$invoices = Saldeo::company('ERP-17')->invoices()->list(); // regular, corrective, advance invoices
$ids = Saldeo::company('ERP-17')->invoices()->ids(2025, 3);
Saldeo::company('ERP-17')->invoices()->byIds($ids);
foreach (Saldeo::company('ERP-17')->bankStatements()->list() as $statement) {
foreach ($statement->operations as $operation) { $operation->value; $operation->isDebit(); }
}
$dictionaries = Saldeo::company('ERP-17')->dictionaries();
$dictionaries->mergeCategories([['CATEGORY_PROGRAM_ID' => '402', 'NAME' => 'Materiały biurowe']]);
$dictionaries->mergeRegisters([['REGISTER_PROGRAM_ID' => 'VZ', 'NAME' => 'Rejestr zakupów']]);
$dictionaries->mergePaymentMethods([['PAYMENT_METHOD_PROGRAM_ID' => 'P', 'NAME' => 'Przelew']]);
$dictionaries->mergeDimensions([['CODE' => 'MPK', 'NAME' => 'MPK', 'TYPE' => 'ENUM', 'DIMENSION_VALUES' => ['DIMENSION_VALUE' => [['CODE' => '01', 'DESCRIPTION' => 'Biuro']]]]]);
Any other operation (for example document.history, employee.list, financial_balance.merge):
$response = Saldeo::call('document.history', ['company_program_id' => 'ERP-17'], ['DOCUMENT_IDS' => ['DOCUMENT_ID' => [65]]], version: '3.2');
$response->body; // decoded RESPONSE element
Operation versions default to the latest documented ones (for example document.list 3.1, contractor.merge 3.3);
pin others with 'versions' => ['document.list' => '2.18'] in the configuration.
Scheduler
Schedule::command('saldeo:fetch-documents --all-companies')->hourly();
Event::listen(DocumentFetched::class, function (DocumentFetched $event) {
// $event->companyProgramId, $event->document, $event->contractor
});
Errors
| Exception | When |
|---|---|
ApiException |
STATUS=ERROR or HTTP error: errorCode() (for example 6101), operation(), httpStatus(), describe() |
AuthenticationFailed |
4302 user does not exist or is blocked, 4303 API not active |
RateLimitExceeded |
4307 more than 20 requests per minute, 4308 parallel requests, HTTP 429 |
InvalidConfiguration |
missing credentials, unknown connection or no company selected |
Per-item problems of write operations (for example an invalid contractor) are not exceptions: check
MergeResult::failed() and ->errors. Write operations are never retried; reads are retried once on connection
errors and HTTP 5xx with a new req_id. SaldeoSMART allows 20 requests per minute per user and one request at a
time.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need an integration? Get in touch through beziworld.eu.
Other packages by the author
laravel-ksef · laravel-fakturownia · laravel-ifirma · laravel-wfirma · laravel-infakt
License
MIT. See LICENSE.md. This package is not affiliated with BrainShare IT or SaldeoSMART.