Aller au contenu

omnibank/bridge

Omnibank Bridge: bank aggregation through Bridge API v3 (bridgeapi.io) - its Connect sessions for the consent, accounts, balances, transactions, item webhooks.

Étage
Fournisseur · Omnibank
Version
1.x
Suite de conformité
non branchée
Vérifié en réel
Non vérifié en réel
Licence
MIT
composer require omnibank/bridge:1.x-dev

Bridge (by Perspecteev) for omnisense/omnibank, its API v3: the user connects their banks in a Bridge Connect session; their accounts, balances and transactions are read with the user's access token; item.* webhooks say when the data is fresh or the consent must be renewed. The gateway calls, on https://api.bridgeapi.io:

Operation Path
a new user (first connect()) POST /v3/aggregation/users
the user's token POST /v3/aggregation/authorization/token
the Connect session (each connect()) POST /v3/aggregation/connect-sessions
accounts GET /v3/aggregation/accounts
balances GET /v3/aggregation/accounts/{id}
transactions GET /v3/aggregation/transactions

Every call sends Client-Id, Client-Secret and Bridge-Version (2025-01-15 by default); a user's call Authorization: Bearer <access token> too; 30-second timeout.

Unverified: written from Bridge's published v3 documentation and recorded answers (Tests/Fixtures); not yet run against a real Bridge sandbox - the webhook signature above all.

Installation

composer require omnisense/omnibank omnibank/bridge

Requires PHP 8.2, omnisense/omnibank (1.x) and symfony/http-client (6.4, 7 or 8). In a Symfony application with Omnibank\Bridge\Symfony\OmnibankBundle, the factory is registered on the application's http_client as soon as the package is installed. (Omnibank\Bridge\ is this package's namespace; the core's Symfony bundle lives in Omnibank\Bridge\Symfony\, a different directory of another package.)

Credentials

In Bridge's dashboard (dashboard.bridgeapi.io): a sandbox application's client_id and client_secret, its callback URL registered, and a webhook on the item.* events with its secret.

Options

new BridgeGatewayFactory(?HttpClientInterface $http = null) (none given: HttpClient::create()); options of create():

Option Type Default Required
client_id string - yes
client_secret string - yes
version string 2025-01-15 no the Bridge-Version header
webhook_secret ?string null for notify()
webhook_tolerance int 300 no the seconds a webhook's timestamp may be from now
callback_url ?string null no the return URL when connect() is given an empty one
country_code ?string FR no the banks a new Connect session lists (not sent for a renewal)
host string https://api.bridgeapi.io no

The connection's state (kept by the application): user_uuid, external_user_id, access_token, expires_at (set by connect()); external_user_id (the application's id for the user, given before the first connect()), user_email (sent to the session) and item_id (to renew one bank connection) put there by the application.

Capabilities

Request Supported
Connect yes: a Connect session's page
FetchAccounts yes
FetchBalances yes
FetchTransactions yes
Transfer no, in this version
Notify yes: item.* events

Behaviour

Connect. Without a user_uuid in the state, POST /v3/aggregation/users creates the Bridge user, its external_user_id the state's or one made up (omnibank-<24 hex>); user_uuid and external_user_id go in the state. Each time, a token is asked (POST /v3/aggregation/authorization/token, kept as access_token and expires_at), then POST /v3/aggregation/connect-sessions with callback_url (the return URL, else the option), user_email when the state has one, and country_code - or, when the consent is NEEDS_RENEWAL and the state has an item_id, that item_id instead, for the user to authenticate that bank again. The result's URL is the session's url; a session without one is a ProviderException. The consent is not changed by connect().

The token. A user's call uses the state's access_token while expires_at is more than a minute away; otherwise a new token is asked for the call. The new token is not written back into the connection (the gateway returns none from a read), so after its expiry every call asks one; call connect() again, or keep a token of the application's, to avoid it.

Accounts. Every page (pagination.next_uri, up to 100 pages): id, iban, bic, name, currency_code (EUR by default), type; the account whole in raw.

Balances. GET /v3/aggregation/accounts/{id}: balance as booked, instant_balance as instant, at updated_at.

Transactions. account_id, since / until the days asked, limit=500, every page (pagination.next_uri, up to 1000). A link to another host than Bridge's is refused (ProviderException), so the credentials are never sent elsewhere. Lines of another account, future or deleted ones, or without id, amount or a date are left out; the others kept between the days asked, each once by id, oldest first: bookedOn booking_date (else date, transaction_date), valueOn value_date, amount signed by Bridge in the line's currency_code, label provider_description (else clean_description); no counterparty, no reference; raw the line.

Webhooks. notify(): the signature checked first (BridgeApi-Signature, v1=<hex HMAC-SHA256 of the body>, several comma-separated, the hex in either case), then the event read: type the event's (an event without a type is a ProviderException), connectionId content.item_id. The consent, for item.* events: a content.status 402 (credentials), 429 (an action on the bank's site), 430 (a password to change), 1010 (SCA to renew) or 1100 (a professional account to validate) is NEEDS_RENEWAL; without a numeric status, status_code_info mentioning SCA, credential, password, action, renew, validat(ion) or expired is NEEDS_RENEWAL; an item.*.deleted event (not an account's) is REVOKED; anything else ACTIVE. Other events: NONE.

A webhook whose timestamp (the event's time in Unix seconds, inside the signed body) is more than webhook_tolerance seconds from now, or that has none, is refused (InvalidNotificationException, "too old"): one captured and sent again is not read twice.

Example

On the answers the package's tests keep, each request printed (with its JSON body):

<?php // bridge.php

require __DIR__.'/vendor/autoload.php';

use Omnibank\Bridge\BridgeGatewayFactory;
use Omnibank\Model\Connection;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;

// The answers omnibank/bridge's tests keep, written from Bridge's v3 documentation; each request printed
$fixtures = '/ws/bridge/Tests/Fixtures';   // the package's Tests/Fixtures, here in the family's workspace
$http = new MockHttpClient(function (string $method, string $url, array $options) use ($fixtures) {
    echo '  ', $method, ' ', urldecode($url), isset($options['body']) ? ' '.$options['body'] : '', "\n";
    $path = parse_url($url, PHP_URL_PATH);
    $next = str_contains($url, 'after=');

    return new MockResponse(file_get_contents($fixtures.'/'.match ($path) {
        '/v3/aggregation/users' => 'user.json',
        '/v3/aggregation/authorization/token' => 'token.json',
        '/v3/aggregation/connect-sessions' => 'connect-session.json',
        '/v3/aggregation/accounts' => $next ? 'accounts-page-2.json' : 'accounts-page-1.json',
        '/v3/aggregation/transactions' => $next ? 'transactions-page-2.json' : 'transactions-page-1.json',
    }));
});
$bridge = (new BridgeGatewayFactory($http))->create(['client_id' => 'id', 'client_secret' => 'secret']);

echo "connect:\n";
$result = $bridge->connect(new Connection(['external_user_id' => 'nakaya-user-42']), 'https://app.example/bank/back');
echo '  redirect to ', $result->url, "\n";
$connection = $result->connection;
echo '  keep ', implode(', ', array_keys($connection->state)), "\n";

echo "accounts:\n";
foreach ($bridge->accounts($connection) as $account) {
    echo '  ', $account->id, ' ', $account->name, ' ', $account->type, "\n";
}
echo "transactions:\n";
foreach ($bridge->transactions($connection, $account = $bridge->accounts($connection)[0]) as $t) {
    printf("  %s %9s  %s\n", $t->bookedOn->format('Y-m-d'), $t->amount->decimal(), $t->label);
}
$ php bridge.php
connect:
  POST https://api.bridgeapi.io/v3/aggregation/users {"external_user_id":"nakaya-user-42"}
  POST https://api.bridgeapi.io/v3/aggregation/authorization/token {"user_uuid":"c2a4d0de-9a35-4e4f-8a5a-1f3e2b7c9d10"}
  POST https://api.bridgeapi.io/v3/aggregation/connect-sessions {"callback_url":"https:\/\/app.example\/bank\/back","country_code":"FR"}
  redirect to https://connect.bridgeapi.io/session/f81d4fae-7dec-11d0-a765-00a0c91e6bf6
  keep external_user_id, user_uuid, access_token, expires_at
accounts:
  GET https://api.bridgeapi.io/v3/aggregation/accounts
  GET https://api.bridgeapi.io/v3/aggregation/accounts?after=MzE0MTU5MjY&limit=1
  31415926 Compte Courant Pro checking
  27182818 Livret A savings
transactions:
  GET https://api.bridgeapi.io/v3/aggregation/accounts
  GET https://api.bridgeapi.io/v3/aggregation/accounts?after=MzE0MTU5MjY&limit=1
  GET https://api.bridgeapi.io/v3/aggregation/transactions?account_id=31415926&limit=500
  GET https://api.bridgeapi.io/v3/aggregation/transactions?account_id=31415926&after=OTAwMDAwMDAwMDAwMg&limit=2
  2026-09-02   1250.00  VIR SEPA RECU /DE CAMILLE DURAND /MOTIF FACTURE F-2026-042
  2026-09-05    -45.90  PRLV SEPA EDF CLIENT 12345
  2026-09-08    -23.40  CB CARREFOUR CITY 06/09

(run on 2026-10-26, PHP 8.4.27, no network; the user, the accounts and the amounts are made up)

In Symfony:

omnibank:
    gateways:
        banks:
            factory: bridge
            options:
                client_id: '%env(BRIDGE_CLIENT_ID)%'
                client_secret: '%env(BRIDGE_CLIENT_SECRET)%'
                webhook_secret: '%env(BRIDGE_WEBHOOK_SECRET)%'
                callback_url: 'https://app.example/bank/back'

Errors

Case Exception
a transport failure, HTTP 5xx, 429, a body that is not JSON UnavailableException ([bridge] Bridge answered HTTP 503: try again later.)
another 4xx ProviderException, the message errors[0].message (else the body's message), providerCode its code, else the body's type, else the status
no user_uuid in the state for a user's call ProviderException (The connection has no Bridge user yet: connect() first.)
Bridge gives no user, no token, no session URL ProviderException
a pagination link out of Bridge's host ProviderException
a webhook without a secret configured, unsigned or with a wrong signature InvalidNotificationException
missing client_id or client_secret; no return URL InvalidConfigException
transfer() RequestNotSupportedException

No failure carries the client secret and the user's access token: a transport failure's message has them masked as *** (the key parameters of a URL too), its previous is a copy of the HTTP client's exception (the same class and code), and no trace of the chain keeps its arguments, where the request's options and the token stood (No secret in a failure).

Verified, and not

  • Verified (the package's tests, 10, green on 2026-10-26 in the family's workspace): the first connect creates the user and opens a session; a consent to renew opens a session on its item; accounts every page and balances; an expired token renewed for the call; transactions every page, booked only; not connected says so; an item webhook checked then read, one not signed with the secret refused; an outage never "no data"; no transfers.
  • Not verified: against a real Bridge sandbox; the webhook signature as Bridge really sends it.

Responsibility

This package is an independent, unofficial client, not affiliated with, endorsed by or sponsored by Perspecteev SAS (Bridge). Whoever runs it is responsible for complying with Bridge's terms (Bridge API terms of use) and for what is done with what it returns; see the README's Responsibility section.

Proposer une modification docs/index.md