mhujer/fio-api-php
PHP client for the Fio Bank API to fetch account transactions, download bank statements, and send domestic and foreign payments. It wraps the bank's REST endpoints in simple typed objects, handles authentication tokens, and installs easily via Composer.
Install and authenticate
composer require mhujer/fio-api-php
Get a token from Fio internet banking (Settings → API, i.e. Nastavení → API). Generate a read-only token unless you genuinely need more, and note two things: the token is bound to a single Fio account, and there is no sandbox — every call hits production.
Three-line first call
use Mhujer\FioApi\FioApi;
$fio = new FioApi(config('services.fio.token'));
$list = $fio->getTransactions(
new \DateTime('first day of last month'),
new \DateTime('last day of last month')
);
foreach ($list->getTransactions() as $transaction) {
// $transaction->getColumn1() ... getColumnN()
}
Use \DateTime, not \DateTimeImmutable — the library's type hints were written in the PHP 5/7 era and may reject immutable instances.
First use case: a nightly job that pulls yesterday's movements into a bank_transactions table so finance can reconcile incoming payments against invoices. That is 90% of what this package is ever used for.
Where to look first: the README (short, with a full method list), then FioApi.php (the whole API surface is one class), TransactionList (iterating results), and the Transaction value object (how columns are exposed). Because the API is column-oriented, the Transaction class is where you will spend your time.
Incremental sync with a cursor. Don't re-download everything. Persist the newest transaction date (or the last transaction id from column 22) after each run and query from there with a small overlap window (e.g. start = cursor − 2 days), then dedupe on insert. Overlap protects you against late-settled or back-dated entries.
Idempotent upsert keyed on transaction id. Column 22 (ID pohybu) is the bank's stable identifier. Make it a unique index, prefix it with an account slug if you store multiple accounts in one table, and use upsert() — cron retries and manual re-runs should be free.
Map columns to a domain object once. Fio returns a fixed set of numbered columns whose meaning depends on the account type and currency (a CZK account and a EUR account do not put the same thing in column 5). Read the column headers off the response (getColumnHeaders() / the name attributes on <column_*> elements) and build a named-array mapper (amount, counter_account, variable_symbol, constant_symbol, specific_symbol, message, transaction_id) in one place. Everything downstream should speak your names, not getColumn14().
Wrap the client in a Laravel service. The package has no service provider and no config file, so bind it yourself:
// AppServiceProvider::register()
$this->app->singleton(FioApi::class, fn () => new FioApi(config('services.fio.token')));
Then put your own FioRepository / FioSync class in front of it. This gives you the seam you need for tests (bind a fake), a place to add retry/throttle logic, and freedom to swap in a fork later.
Drive it from a scheduled command, php artisan fio:sync --days=3, wrapped in withoutOverlapping() and queued if the transaction volume is high. Poll the "last transactions" endpoint for near-real-time notification (a payment landed) and the date-range endpoint for the nightly ledger sync.
Reconciliation tip: Fio's variable symbol is the field Czech counterparties use to carry an invoice number. Match variable_symbol against invoices.variable_symbol, fall back to a regex over the message field, and leave the rest in an "unmatched" queue for a human. Never auto-close an invoice purely on amount.
dd($url) leaks full account access. Redact before logging, and never let the client's exceptions reach a public error page.DECIMAL(15,2) or handle with bcmath. Foreign-currency accounts give you both the original amount and the CZK equivalent in different columns — pick the one your accounting rules require and name it explicitly.composer why-not php 8.x early; if constraints block you, either pin the runtime or fork and bump the composer.json plus any deprecated DateTime handling. The code is small enough that a fork is a realistic afternoon, not a rewrite.curl the raw endpoint (/rest/periods/{token}/{from}/{to}/transactions.xml) and eyeball the <column_*> id/name pairs against what your mapper assumes. Nine times out of ten the bug is a column-meaning assumption, not the library.How can I help you explore Laravel packages today?