yeganemehr / hesabfa-api
PHP client for the Hesabfa accounting app frontend API (core.hesabfa.com).
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7
Requires (Dev)
- laravel/pint: ^1
- phpstan/phpstan: ^2
- phpunit/phpunit: ^11
- symfony/var-dumper: ^8.1
This package is auto-updated.
Last update: 2026-08-06 18:51:03 UTC
README
A standalone PHP (Guzzle) client for the Hesabfa accounting app's
frontend API (https://core.hesabfa.com/api/). It speaks the same protocol the web app uses:
an API-TOKEN session cookie, an optional X-XSRF-TOKEN, and a HESABFA-BUSINESS-KEY header
that scopes calls to one business + financial year.
Install
composer require yeganemehr/hesabfa-api
Authenticate
Log in to obtain fresh credentials:
use Yeganemehr\Hesabfa\Hesabfa; $hesabfa = Hesabfa::login('you@example.com', 'password'); // persist these to skip re-login next time: $apiToken = $hesabfa->apiToken(); $xsrfToken = $hesabfa->xsrfToken();
Or reuse a token you already have (e.g. copied from the browser's API-TOKEN cookie via
DevTools → Application → Cookies). Session tokens expire, so refresh when calls return 401:
$hesabfa = Hesabfa::withToken($apiToken, $xsrfToken);
Select a business
Business-level calls (contacts, products, documents, …) need a business key. List your businesses first (account-level, no key), then scope the client:
$businesses = $hesabfa->businesses()->list(); // account-level; each has a "key" $years = $hesabfa->businesses()->loadBusinessAndYears($businesses[0]['key']); $scoped = $hesabfa->forBusiness($years['financialYears'][0]['key']); // scope to a financial year
loadBusinessAndYears() needs the business key (from list()); it returns that business plus
its financialYears, each of which has its own key. Use a year key with forBusiness() for
transactional calls. forBusiness() returns a new immutable instance — the original stays
unscoped, so one process can talk to several businesses at once.
Use the resources
$scoped->contacts()->list(['take' => 20, 'skip' => 0]); // DevExtreme loadOptions $scoped->contacts()->get(42); $scoped->contacts()->save(['Name' => 'Acme', 'Code' => 'C-100']); $scoped->contacts()->delete(42); $scoped->products()->activeList(); $scoped->products()->getQuantity(['ids' => [1, 2, 3]]); $scoped->documents()->findByNumber(1001); $scoped->receipts()->itemsList(['take' => 50]); $scoped->banks()->list(); $scoped->accounts()->merge(mainId: 5, accountId: 9); $scoped->settings()->getSettings(['InvoiceSetting']);
Invoices & vouchers
Hesabfa keeps all invoices and money vouchers on two endpoints (receipt/*, payment/*) keyed by
type. Each typed resource below applies the right endpoint + filter for you (the raw list endpoints
reject an unfiltered request):
$scoped->sales()->list(['take' => 20]); // sales invoices $scoped->bills()->list(); // purchase invoices $scoped->salesReturns()->list(); $scoped->purchaseReturns()->list(); $scoped->payments()->list(); // money-pay vouchers $scoped->expenses()->list(); $scoped->receipts()->list(); // money-receive vouchers
A caller filter is combined with the resource's own filter, e.g.
$scoped->sales()->list(['filter' => ['contactId', '=', 42]]). get(), save(), and delete()
target the underlying receipt/* / payment/* document. Note: save() writes to your real
ledger and its payload shape is unverified — set the type fields yourself and test on a draft first.
Typed convenience methods exist for contacts, products, documents, banks, accounts, businesses, settings, and the invoice/voucher resources above. Every other endpoint is reachable by path:
$scoped->get('contact/get/42'); $scoped->post('contact/list', ['take' => 20]); $scoped->delete('product/delete/9'); $scoped->download('report/export/...'); // raw bytes
Responses are returned as decoded arrays (*/list endpoints return ['data' => [...], 'total' => N]).
Errors
Non-2xx responses throw a Yeganemehr\Hesabfa\Exceptions\HesabfaException subclass:
| Exception | When |
|---|---|
AuthenticationException |
401 — token missing/expired |
RateLimitException |
499, or 400 Execution-Limit-Retry-In<sec> (see ->retryAfter) |
ValidationException |
other 400 (raw error code in ->body) |
ApiException |
404 and other 4xx |
ServerException |
5xx |
TransportException |
network/connection failure |
All carry ->status and ->body.
Development
composer check # pint (style) + phpstan + phpunit
Tests use Guzzle's MockHandler — they make no network calls.