vs-point/kb-adaa

A PHP library for communication with the Komerční banka Account Direct Access (ADAA) API v2.

Maintainers

Package info

github.com/vs-point/kb-adaa

pkg:composer/vs-point/kb-adaa

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-05-17 13:58 UTC

This package is auto-updated.

Last update: 2026-05-18 11:42:31 UTC


README

PHP knihovna pro komunikaci s Komerční banka Account Direct Access API v2. Typovaný interface pro účty, zůstatky, transakce, výpisy v PDF a notifikace o pohybech.

Pokrývá všech 5 vrstev integrace:

Vrstva Co dělá Třída
Runtime ADAA čte účty, zůstatky, transakce, výpisy, spravuje notifikační subscriptions KbAdaaClient->accounts, ->balances, ->transactions, ->statements, ->eventSubscriptions
OAuth2 vyměňuje authorization code za tokeny, refreshuje access token, sestavuje login URL KbAdaaClient->oauth2
Client Registration (mTLS) žádá KB o software statement JWT pro tvou aplikaci KbAdaaClient->clientRegistration
Application Registration builduje redirect URL a dešifruje AES-256-GCM odpověď z callbacku KbAdaaClient->applicationRegistration
Event API receiver server-side helpery pro implementaci tvého webhook endpointu KbAdaaClient->eventApi

Instalace

composer require vs-point/kb-adaa

Vyžaduje PHP ≥ 8.2 a rozšíření openssl, json, curl (curl ≥ 7.71 pro inline certifikát).

Architektura konfigurace

ADAA má tři různé apiKey (Client Registration, OAuth2, ADAA) a každý se získává zvlášť v developer portálu. Proto má knihovna tři oddělené config objekty:

use VsPoint\KbAdaa\Config\KbAdaaConfig;            // pro runtime ADAA volání
use VsPoint\KbAdaa\Config\OAuth2Config;            // pro token endpoint
use VsPoint\KbAdaa\Config\ClientRegistrationConfig; // pro software statement (mTLS)

Při inicializaci KbAdaaClient jsou OAuth2Config a ClientRegistrationConfig volitelné — předáš jen ty, které potřebuješ pro aktuální use-case.

Sandbox vs produkce

use VsPoint\KbAdaa\Enum\Environment;

Environment::Sandbox     // https://api-gateway.kb.cz/sandbox/...
Environment::Production  // https://api-gateway.kb.cz/... (Client Registration má vlastní host)

Token storage

Access token žije pouze 3 minuty, refresh token 12 měsíců. Knihovna vyžaduje TokenStorageInterface:

interface TokenStorageInterface {
    public function load(): ?AccessToken;
    public function save(AccessToken $token): void;
    public function clear(): void;
}

Pro lokální vývoj / CLI použij InMemoryTokenStorage. Pro produkci napiš vlastní implementaci nad svou DB / Redisem / secret managerem.

Auto-refresh: Pokud klientovi předáš OAuth2Config, automaticky před každým voláním zkontroluje expiraci tokenu, sám refreshne a uloží nový. Pokud OAuth2Config nepředáš, používá se uložený access token jak je (chování pro „manuální“ režim).

Quick start — runtime ADAA

Předpokládá, že už máš access token + refresh token získaný OAuth2 flow (viz dál).

use VsPoint\KbAdaa\Config\KbAdaaConfig;
use VsPoint\KbAdaa\Config\OAuth2Config;
use VsPoint\KbAdaa\Enum\Environment;
use VsPoint\KbAdaa\KbAdaaClient;
use VsPoint\KbAdaa\Token\AccessToken;
use VsPoint\KbAdaa\Token\InMemoryTokenStorage;

$tokenStorage = new InMemoryTokenStorage(new AccessToken(
    accessToken: 'eyJ...',
    refreshToken: 'eyJ...',
    expiresAt: time() + 180,
    scope: 'adaa',
));

$client = KbAdaaClient::create(
    config: new KbAdaaConfig(
        apiKey: 'adaa-api-key',
        environment: Environment::Production,
    ),
    tokenStorage: $tokenStorage,
    // Volitelné — povoluje auto-refresh tokenu:
    oauth2Config: new OAuth2Config(
        apiKey: 'oauth2-api-key',
        clientId: 'YourApp-1234',
        clientSecret: 'shh',
        redirectUri: 'https://nas-produkt.cz/kb/callback',  // KB ho vyžaduje i u refresh
        environment: Environment::Production,
    ),
);

// Účty
foreach ($client->accounts->list() as $account) {
    echo $account->iban . ' (' . $account->currency?->getCurrencyCode() . ')' . PHP_EOL;
}

Konfigurace mTLS (Client Registration)

Software statement endpoint vyžaduje kvalifikovaný certifikát od I.CA nebo PostSignum (KB ho sama nevydává). Certifikační autority ho typicky dávají jako .p12 (PKCS#12). Knihovna nabízí tři cesty, vyber si podle toho, kde certifikát žije:

Třída Vstup Kdy použít
ClientRegistrationP12Config .p12 soubor nebo bytes + heslo Máš .p12 přímo od I.CA / PostSignum, nechceš konvertovat
ClientRegistrationConfig cesta k .pem souboru Po jednorázové konverzi P12→PEM, certifikát žije na disku
ClientRegistrationInlineConfig PEM string Certifikát ze secret manageru / env proměnné

Z .p12 souboru (přímo od certifikační autority)

Nejjednodušší — knihovna provede openssl_pkcs12_read() interně:

use VsPoint\KbAdaa\Config\ClientRegistrationP12Config;

$config = ClientRegistrationP12Config::fromFile(
    apiKey: 'cr-api-key',
    p12Path: '/secrets/qualified-cert.p12',
    p12Password: 'heslo-k-p12',
    environment: Environment::Production,
);

Pokud máš .p12 už načtený v paměti (typicky ze secret manageru):

$config = new ClientRegistrationP12Config(
    apiKey: 'cr-api-key',
    p12Content: $secretManager->get('kb-qualified-cert.p12'),
    p12Password: $secretManager->get('kb-qualified-cert-password'),
    environment: Environment::Production,
);

OpenSSL 3.x heads-up: openssl_pkcs12_read() selže na .p12 šifrovaných legacy algoritmy (RC2 / 3DES) — časté u starších Windows exportů z I.CA. Pokud konstruktor vyhodí výjimku, proveď jednorázovou konverzi openssl pkcs12 -legacy ... a použij ClientRegistrationConfig nebo ClientRegistrationInlineConfig.

Z PEM souboru na disku

use VsPoint\KbAdaa\Config\ClientRegistrationConfig;

$config = new ClientRegistrationConfig(
    apiKey: 'cr-api-key',
    certPath: '/secrets/qualified-cert.pem',
    certPassword: null,           // pokud je PEM zašifrovaný
    keyPath: null,                // pokud je klíč v jiném souboru
    keyPassword: null,
    environment: Environment::Production,
);

Konverze .p12.pem:

openssl pkcs12 -in qualified-cert.p12 -out qualified-cert.pem -nodes -passin pass:HESLO

Inline (PEM string ze secret manageru)

Žádný dočasný soubor se nezapisuje — používá CURLOPT_SSLCERT_BLOB (curl 7.71+).

use VsPoint\KbAdaa\Config\ClientRegistrationInlineConfig;

$config = new ClientRegistrationInlineConfig(
    apiKey: 'cr-api-key',
    certPem: $_ENV['KB_QUALIFIED_CERT_PEM'],
    keyPem: $_ENV['KB_QUALIFIED_KEY_PEM'],   // pokud klíč není v certPem
    environment: Environment::Production,
);

Kompletní onboarding flow

KB onboarding je jednorázový proces na začátku integrace. Pak už jen průběžně refreshuješ token a voláš ADAA.

1. Software statement (jednou za 12 měsíců na aplikaci)

use VsPoint\KbAdaa\DTO\ClientRegistration\SoftwareStatementPayload;

$statement = $client->clientRegistration->issue(new SoftwareStatementPayload(
    softwareName: 'Náš produkt',
    softwareNameEn: 'Our product',
    softwareId: bin2hex(random_bytes(16)),
    softwareVersion: '1.0',
    softwareUri: 'https://nas-produkt.cz',
    redirectUris: ['https://nas-produkt.cz/kb/callback'],
    registrationBackUri: 'https://nas-produkt.cz/kb/registration-back',
    contacts: ['email: support@nas-produkt.cz'],
));

echo $statement->token;                     // raw JWT
echo $statement->claims->vendorName;        // dekódované claims
echo $statement->claims->exp;               // expirace (12 měsíců)
// → ulož $statement->token do secret manageru, použiješ při registraci aplikace

2. Application registration (jednou za 12 měsíců na klienta — provede uživatel přes browser)

use VsPoint\KbAdaa\DTO\ApplicationRegistration\RegistrationRequest;
use VsPoint\KbAdaa\Enum\ApplicationType;
use VsPoint\KbAdaa\Enum\Scope;

// Vygeneruj a ulož AES-256 klíč — budeš ho potřebovat pro dešifrování callbacku.
$encryptionKey = $client->applicationRegistration->generateEncryptionKey();
// $encryptionKey ulož do session / DB svázaný s registrací

$redirectUrl = $client->applicationRegistration->buildRedirectUrl(
    Environment::Production,
    new RegistrationRequest(
        clientName: 'Náš produkt',
        clientNameEn: 'Our product',
        redirectUris: ['https://nas-produkt.cz/kb/callback'],
        scope: [Scope::Adaa, Scope::CardData],
        encryptionKey: $encryptionKey,
        softwareStatement: $statement->token,
        applicationType: ApplicationType::Web,
    ),
);

// Redirectni uživatele
header('Location: ' . $redirectUrl);

Po dokončení se uživatel vrátí na tvůj registrationBackUri s ?salt=…&encryptedData=…. Dešifruj:

$registration = $client->applicationRegistration->decryptResponse(
    base64Salt: $_GET['salt'],
    base64EncryptedData: $_GET['encryptedData'],
    base64EncryptionKey: $encryptionKey,    // tentýž klíč jako výše
);

echo $registration->clientId;       // ulož do DB
echo $registration->clientSecret;   // ulož do DB

3. OAuth2 — získání access tokenu (uživatel se přihlásí do KB)

$loginUrl = $client->oauth2->buildAuthorizationUrl(
    scopes: [Scope::Adaa],
    state: bin2hex(random_bytes(16)),
);

header('Location: ' . $loginUrl);

redirect_uri se vždy bere z OAuth2Config::$redirectUri — KB ho vyžaduje shodný napříč authorize / token-exchange / refresh.

Callback handler:

$accessToken = $client->oauth2->exchangeAuthorizationCodeForToken(code: $_GET['code']);

$tokenStorage->save($accessToken);

Od této chvíle můžeš volat ADAA endpointy. Pokud máš OAuth2Config připojený na klientovi, refresh probíhá automaticky.

Použití runtime API

Účty

$accounts = $client->accounts->list();

foreach ($accounts as $account) {
    echo $account->accountId . '' . $account->iban . PHP_EOL;
}

// accountId se nemění — cachuj ho, ať nepáliš zbytečně API call

Zůstatky

foreach ($client->balances->list($accountId) as $balance) {
    echo $balance->type?->value . ': ' . $balance->amount?->toMoney() . PHP_EOL;
}
// CLOSING_AVAILABLE: CZK 85,061.41
// PREVIOUSLY_CLOSED_BOOK: CZK 85,061.41

Transakce

use Brick\DateTime\Duration;
use Brick\DateTime\TimeZone;
use Brick\DateTime\ZonedDateTime;
use VsPoint\KbAdaa\DTO\Transaction\TransactionQuery;

$now = ZonedDateTime::now(TimeZone::utc());

$page = $client->transactions->list(
    $accountId,
    new TransactionQuery(
        fromDateTime: $now->minusDuration(Duration::ofDays(30)),
        toDateTime: $now,
        page: 0,
        size: 100,    // max 100 (KB-enforced)
    ),
);

foreach ($page->content as $tx) {
    echo $tx->bookingDate . ' ' . $tx->amount?->toMoney() . ' ' . $tx->status?->value . PHP_EOL;

    // Pro párování PDNG → BOOK použij references.accountServicer
    echo '  pair key: ' . $tx->references?->accountServicer . PHP_EOL;
}

Throughput limit KB: 1 stažení / hodinu. Pro vyšší frekvenci použij Event API subscriptions níže.

Výpisy v PDF

use Brick\DateTime\ZonedDateTime;
use VsPoint\KbAdaa\DTO\Statement\StatementListQuery;

$statements = $client->statements->list(
    $accountId,
    new StatementListQuery(dateFrom: ZonedDateTime::parse('2024-01-01T00:00:00Z')),
);

foreach ($statements as $statement) {
    $pdf = $client->statements->download($accountId, $statement->statementId);
    file_put_contents("statement-{$statement->statementId}.pdf", $pdf);
}

Notifikace (subscription management)

use VsPoint\KbAdaa\DTO\EventSubscription\CreateSubscriptionPayload;

$subscription = $client->eventSubscriptions->create(
    $accountId,
    new CreateSubscriptionPayload(
        eventApiUrl: 'https://nas-produkt.cz/webhook/kb-events',
        eventApiKey: bin2hex(random_bytes(32)),    // sdílené tajemství — KB ho posílá jako x-api-key header
    ),
);

echo $subscription->subscriptionId;   // ulož do DB ke svému účtu

// Později:
$client->eventSubscriptions->delete($accountId, $subscription->subscriptionId);

KB volá tvůj endpoint z IP 194.50.202.179 a 194.50.226.179 — povolit ve firewallu.

Event API receiver (serverová strana webhooku)

KB vyžaduje, aby tvůj endpoint implementoval dvě cesty:

  • POST /subscriptions/{subscriptionId}/events — musí vrátit HTTP 204 do 5 sekund
  • GET /version — musí vrátit {"version": "1.0"}

Knihovna ti dává helper, který je framework-agnostický:

// V tvém controlleru pro POST /subscriptions/{subscriptionId}/events
try {
    $payload = $client->eventApi->handleEvent(
        jsonBody: file_get_contents('php://input'),
        expectedApiKey: $mySubscriptionApiKey,         // ten, co jsi předal při create()
        providedApiKey: $request->headers->get('x-api-key', ''),
    );

    // $payload->eventCount — počet nových událostí
    // Spusť fetch nových transakcí (async, neblokuj odpověď)
    dispatch_to_queue(new FetchKbTransactions($accountId));

    http_response_code(204);
} catch (InvalidEventApiKeyException) {
    http_response_code(401);
    echo $client->eventApi->errorResponseJson('Invalid API key');
}
// V controlleru pro GET /version
header('Content-Type: application/json');
echo $client->eventApi->versionResponseJson();

KB při 401/403/404 odpovědi subscription trvale zastaví (status STOPPED). Při 500 nebo timeout circuit breaker (SUSPENDED), opakování → STOPPED. Nereaguj 5xx kvůli své interní chybě — vrať 204 a process asynchronně.

Symfony DI integrace

Knihovna sama není Symfony bundle (žádný DI extension). Stačí YAML wiring:

# config/services.yaml
services:
    VsPoint\KbAdaa\Config\KbAdaaConfig:
        arguments:
            $apiKey: '%env(KB_ADAA_API_KEY)%'
            $environment: !php/enum VsPoint\KbAdaa\Enum\Environment::Production

    VsPoint\KbAdaa\Config\OAuth2Config:
        arguments:
            $apiKey: '%env(KB_OAUTH2_API_KEY)%'
            $clientId: '%env(KB_OAUTH2_CLIENT_ID)%'
            $clientSecret: '%env(KB_OAUTH2_CLIENT_SECRET)%'
            $redirectUri: '%env(KB_OAUTH2_REDIRECT_URI)%'
            $environment: !php/enum VsPoint\KbAdaa\Enum\Environment::Production

    VsPoint\KbAdaa\Config\ClientRegistrationConfig:
        arguments:
            $apiKey: '%env(KB_CR_API_KEY)%'
            $certPath: '%env(KB_CR_CERT_PATH)%'
            $environment: !php/enum VsPoint\KbAdaa\Enum\Environment::Production

    # Implementuj vlastní TokenStorage nad svou DB / Redisem
    VsPoint\KbAdaa\Token\TokenStorageInterface:
        class: App\Kb\DoctrineTokenStorage

    VsPoint\KbAdaa\KbAdaaClient:
        factory: ['VsPoint\KbAdaa\KbAdaaClient', 'create']
        arguments:
            $config: '@VsPoint\KbAdaa\Config\KbAdaaConfig'
            $tokenStorage: '@VsPoint\KbAdaa\Token\TokenStorageInterface'
            $oauth2Config: '@VsPoint\KbAdaa\Config\OAuth2Config'
            $clientRegistrationConfig: '@VsPoint\KbAdaa\Config\ClientRegistrationConfig'

Výjimky

Všechny API výjimky dědí z KbAdaaApiException. getHttpStatus(), getResponseBody(), getErrorCode() (parsovaný kód z {"errors":[{"code":"…"}]}).

HTTP / důvod Třída Typické error codes
400 InvalidRequestException DATE_IN_FUTURE, INVALID_DATE_INTERVAL, INVALID_ACCOUNT_ID, INVALID_CORRELATION_ID, INVALID_CURRENCY
401 UnauthorisedException INVALID_TOKEN, 900900
403 ForbiddenException ACCOUNT_ACCESS_DENIED, ACCOUNT_NOT_CONFIGURED, 900908
404 NotFoundException ACCOUNT_NOT_FOUND, STATEMENT_NOT_FOUND, SUBSCRIPTION_NOT_FOUND
429 RateLimitException REQUEST_IS_THROTTLED (1 stažení/hod limit), getRetryAfterSeconds()
(refresh) TokenRefreshFailedException Refresh token expiroval → uživatel musí re-authorize
(callback) ApplicationRegistrationDecryptionException Špatný klíč / poškozený ciphertext
(webhook) InvalidEventApiKeyException x-api-key z KB neodpovídá uložené hodnotě
use VsPoint\KbAdaa\Exception\KbAdaaApiException;
use VsPoint\KbAdaa\Exception\NotFoundException;
use VsPoint\KbAdaa\Exception\RateLimitException;

try {
    $balances = $client->balances->list($accountId);
} catch (NotFoundException $e) {
    // accountId neexistuje
} catch (RateLimitException $e) {
    sleep($e->getRetryAfterSeconds() ?? 60);
} catch (KbAdaaApiException $e) {
    error_log($e->getHttpStatus() . ': ' . $e->getErrorCode() . '' . $e->getResponseBody());
}

Přehled endpointů

Service Metoda HTTP Path
accounts list() GET /adaa/v2/accounts
balances list(accountId) GET /adaa/v2/accounts/{accountId}/balances
transactions list(accountId, ?TransactionQuery) GET /adaa/v2/accounts/{accountId}/transactions
statements list(accountId, StatementListQuery) GET /adaa/v2/accounts/{accountId}/statements
statements download(accountId, statementId) GET /adaa/v2/accounts/{accountId}/statements/{statementId}
eventSubscriptions create(accountId, CreateSubscriptionPayload) POST /adaa/v2/accounts/{accountId}/transactions/event-subscriptions
eventSubscriptions get(accountId, subscriptionId) GET /adaa/v2/accounts/{accountId}/transactions/event-subscriptions/{id}
eventSubscriptions delete(accountId, subscriptionId) DELETE /adaa/v2/accounts/{accountId}/transactions/event-subscriptions/{id}
oauth2 buildAuthorizationUrl(scopes, ?state) — (browser redirect) login.kb.cz nebo sandbox UI
oauth2 exchangeAuthorizationCode(code) POST /oauth2/v3/access_token
oauth2 refresh(refreshToken) POST /oauth2/v3/access_token
clientRegistration issue(SoftwareStatementPayload) POST /client-registration/v3/software-statements
applicationRegistration buildRedirectUrl(env, RegistrationRequest) — (browser redirect) client-registration-ui
applicationRegistration decryptResponse(salt, encrypted, key) — (lokální AES-GCM dešifrování)
eventApi handleEvent(body, expected, provided) — (server-side helper)

Headless bootstrap v sandboxu

Sandbox OAuth2 obrazovka autorizační kód generuje čistě v JavaScriptu — žádné přihlašování, žádný backend roundtrip. Algoritmus je base64(JSON({userId, scopes})) bez = paddingu. Knihovna ho reprodukuje v PHP, takže integrační testy nemusí klikat v prohlížeči.

use VsPoint\KbAdaa\Enum\Scope;
use VsPoint\KbAdaa\Sandbox\SandboxAuthorizationCodeFactory;

$code = (new SandboxAuthorizationCodeFactory())->generate(
    userId: 'test',                       // libovolný neprázdný string — sandbox nehlídá
    scopes: [Scope::Adaa, Scope::CardData],
);

$token = $client->oauth2->exchangeAuthorizationCodeForToken(code: $code);

$tokenStorage->save($token);
// Hotovo — můžeš volat $client->accounts->list() atd.

Pro plný integrační test stačí jen dva apiKey z developer portálu (KB_ADAA_API_KEY + KB_ADAA_OAUTH2_API_KEY); client_id a client_secret jsou KB-dokumentované sandbox defaults (ExampleClient-6303 / bUfDQ1fMmfaSlZBZXlxBOQ). Viz tests/Integration/SandboxBootstrapTest.php jako referenční E2E příklad — udělá code → token → accounts->list() proti reálnému KB sandboxu bez jakékoliv ruční interakce.

Pouze sandbox. Produkční token endpoint tento kód odmítne. V produkci musí uživatel projít opravdovým KB loginem na https://login.kb.cz/autfe/ssologin — viz OAuth2Service::buildAuthorizationUrl().

Canary test sandboxového JS

Sandbox algoritmus je závislý na tom, že ho KB nezmění. Unit test testSandboxJavascriptStillImplementsKnownAlgorithm stáhne živý JS a ověří přítomnost klíčových tokenů. Defaultně se skipuje; aktivuj přes:

docker run --rm -v "$(pwd):/app" -w /app -e KB_ADAA_SANDBOX_CANARY=1 vspoint/php:8.5-fpm-alpine sh -c "./vendor/bin/phpunit --testsuite Unit --filter Sandbox"

Pokud canary spadne, znamená to, že KB algoritmus změnili a je třeba aktualizovat SandboxAuthorizationCodeFactory.

Vývoj

composer není na hostu — všechno přes Docker:

# Instalace
docker run --rm -v "$(pwd):/app" -w /app vspoint/php:8.5-fpm-alpine sh -c "composer install --no-interaction --prefer-dist"

# PHPStan + ECS + Unit testy (musí všechno projít před commitem)
docker run --rm -v "$(pwd):/app" -w /app vspoint/php:8.5-fpm-alpine sh -c "./vendor/bin/ecs check --fix && ./vendor/bin/phpstan analyse --memory-limit=512M && ./vendor/bin/phpunit --testsuite Unit --testdox"

# Integrační testy proti reálnému KB sandboxu (vyžadují apiKeys — viz .secrets/README.md)
./bin/test-integration.sh
./bin/test-integration.sh --filter SandboxBootstrap   # filtr argy projdou skrz na phpunit

Integrační testy

Helper bin/test-integration.sh načte sandbox apiKeys z .secrets/, exportuje je jako env proměnné a spustí integration testy uvnitř Dockeru. Stačí mít dva JWT soubory:

.secrets/
├── sandbox-adaa-apikey.jwt       # KB_ADAA_API_KEY
└── sandbox-oauth2-apikey.jwt     # KB_ADAA_OAUTH2_API_KEY

Plný návod (kde získat klíče, jaký mít obsah) je v .secrets/README.md. Pozor — .secrets/ je gitignored, klíče tam neházej do commitů.

V GitLab CI je k dispozici stage integration, který se aktivuje automaticky, pokud jsou v project Settings → CI/CD → Variables nastavené (Masked + Protected) proměnné KB_ADAA_API_KEY a KB_ADAA_OAUTH2_API_KEY. Forky / unprotected branches stage tiše přeskočí.

Pokud chceš testovat s předem získaným access/refresh tokenem (bez sandbox bootstrap), nastav místo KB_ADAA_OAUTH2_API_KEY proměnné KB_ADAA_ACCESS_TOKEN + KB_ADAA_REFRESH_TOKEN (volitelně + KB_ADAA_ACCESS_TOKEN_EXPIRES_AT, KB_ADAA_SCOPE, KB_ADAA_ACCOUNT_ID).

Kompatibilita

  • PHP ≥ 8.2
  • Symfony Serializer / PropertyAccess / PropertyInfo 6.4, 7.x, 8.x
  • Bez závislosti na Symfony FrameworkBundle / DI containeru

Licence

MIT