glitchr / omnibank
Omnibank: one contract for bank access - accounts, balances, transactions, transfers, consents and webhooks - over statement files and bank APIs, and its Symfony bundle (Omnitrade's sibling for banking).
Requires
- php: >=8.2
- symfony/http-client-contracts: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.0
- symfony/config: ^6.4|^7.0|^8.0
- symfony/dependency-injection: ^6.4|^7.0|^8.0
- symfony/http-client: ^6.4|^7.0|^8.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
Suggests
- omnibank/bridge: Bridge (bridgeapi.io): bank aggregation through its Connect sessions, webhooks
- omnibank/files: Statement files: CAMT.053, OFX, CSV read; SEPA credit transfers written as pain.001
- omnibank/powens: Powens (ex Budget Insight): bank aggregation through its Webview, webhooks
- omnibank/qonto: Qonto: accounts, balances and transactions with an API key
- symfony/http-client: The HTTP client the providers talk through
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-03 16:11:53 UTC
README
One contract for bank access - accounts, balances, transactions, transfers - over statement files, a bank's own API or an aggregator: the Omnitrade of banking.
$gateway = $registry->get('treasury'); $result = $gateway->connect($connection, 'https://app.example/bank/back'); $connection = $result->connection; // keep it: the provider's user, its tokens if ($result->url) { return redirect($result->url); } // the provider's consent page foreach ($gateway->accounts($connection) as $account) { $gateway->balances($connection, $account); // list<Balance> $gateway->transactions($connection, $account, new \DateTimeImmutable('-30 days')); // list<BankTransaction> } $gateway->transfer($connection, $transfer); // a TransferResult (a pain.001 file, for files) $gateway->notify($request->getContent(), $request->headers->all()); // a webhook, checked and read
This package holds the contract (GatewayInterface, GatewayFactory, Registry), the models,
the requests and the Symfony bundle. Each provider is a package of its own:
| Package | Provider |
|---|---|
omnibank/files |
Statement files: CAMT.053, OFX, CSV read; transfers written as pain.001.001.03. No network |
omnibank/qonto |
Qonto: accounts, balances, transactions with an API key (unverified) |
omnibank/powens |
Powens: bank aggregation through its Webview, CONNECTION_SYNCED webhooks (unverified) |
omnibank/bridge |
Bridge API v3: bank aggregation through Connect sessions, item webhooks (unverified) |
The models
Money- an amount in minor units, signed, and its currency:Money::of(-1250, 'EUR'),isNegative(),decimal(),Money::fromDecimal('1234.56', 'EUR')(read digit by digit).Account-id(the provider's),iban,bic,name,currency,type,raw.Balance-amount,at,type(booked,available,expected, or the provider's word).BankTransaction-id(stable: the bank's reference, or a fingerprint of the line),bookedOn,valueOn,amount(negative when money leaves the account),label,counterpartyName,counterpartyIban,reference,raw.Connection- what the application keeps for one access and hands back on every call: the provider'sstateand theconsent(Consent: aConsentStatus- ACTIVE, NEEDS_RENEWAL, REVOKED, NONE - andexpiresAt).withState(),withConsent(),toArray()/fromArray()for JSON. It holds tokens: store it as you store secrets.ConnectResult-url(the page to send the user to, null when there is nothing to do) and theconnectionto keep.Transfer/TransferResult- a SEPA credit transfer, and what became of it (id,status, and thefile/fileNamewhen it is a file to hand the bank).Notification- a webhook read:type,connectionId,consent,raw.
What every provider answers with is kept whole in $raw: normalization loses nothing.
Errors
RequestNotSupportedException (the provider does not do that: supports() says so beforehand),
InvalidConfigException (an option missing), ProviderException (the provider refused),
InvalidNotificationException (a webhook whose signature does not hold) and
UnavailableException - the provider could not be reached or failed (network, timeout, 5xx, 429).
An UnavailableException is never "no data": nothing is known, try again later.
Symfony
Omnibank\Bridge\Symfony\OmnibankBundle: every omnibank/* provider installed registered,
Omnibank\Registry autowired, and each configured gateway injectable by its name.
omnibank: gateways: statements: { factory: files, options: { debtor_name: 'Nakaya SAS', debtor_bic: AGRIFRPP882 } } treasury: { factory: qonto, options: { login: '%env(QONTO_LOGIN)%', secret_key: '%env(QONTO_SECRET_KEY)%' } } banks: { factory: powens, options: { domain: '%env(POWENS_DOMAIN)%', client_id: '%env(POWENS_CLIENT_ID)%', client_secret: '%env(POWENS_CLIENT_SECRET)%', webhook_secret: '%env(POWENS_WEBHOOK_SECRET)%' } }
public function __construct(GatewayInterface $treasury) {}
An application's own GatewayFactoryInterface is registered too (autoconfigured).
Docker: every provider with your sandbox keys
docker/ runs this package with every omnibank/* provider installed - from GitHub, or from the
checkouts beside this one when OMNIBANK_PLUGINS=../.. is set - and a console that exercises
them with the keys in docker/.env (copy .env.dist). Connections are kept between runs.
cd docker && cp .env.dist .env docker compose run --rm omnibank gateways docker compose run --rm omnibank connect powens # prints the Webview to open docker compose run --rm omnibank accounts powens docker compose run --rm omnibank transactions qonto --since 2026-09-01 docker compose run --rm -v ~/Downloads:/statements omnibank transactions files --path /statements/releve.xml docker compose run --rm omnibank test # every package's tests
License: LGPL-3.0-or-later.