keboola/api-bundle
Keboola API Bundle is a Symfony bundle for building consistent JSON APIs, providing reusable request handling, validation, standardized error responses, and extension points for authentication, logging, and documentation. It helps teams standardize endpoint behavior and reduce boilerplate across PHP...
keboola/api-bundle is a Symfony bundle that packages the conventions Keboola uses across its internal APIs: token-based authentication against the Keboola Storage API (with token exchange via the Manage API), a uniform JSON error envelope, request-id propagation, and pagination helpers. Treat it as internal infrastructure made public — the value is consistency with the Keboola platform, not general-purpose API tooling.
Minimal setup
Require it and check what it drags in:
composer require keboola/api-bundle
composer show keboola/api-bundle # inspect requires + suggest
Verify the Symfony version constraint matches your app before installing; a bundle pinned to ^6.4 will fight a Symfony 7 skeleton. Pin an exact version from the start — 6.x is actively released (6.2.0 → 6.3.0 within recent memory), so "latest" moves.
Register the bundle in config/bundles.php:
Keboola\ApiBundle\KeboolaApiBundle::class => ['all' => true],
Read the bundle's own config schema first — this is the highest-value ten minutes you'll spend:
cat vendor/keboola/api-bundle/src/DependencyInjection/Configuration.php
That file is the authoritative list of options, and it is almost always more accurate than the README. The root key is keboola_api. Dump the resolved config with:
bin/console config:dump-reference keboola_api
Add a minimal config/packages/keboola_api.yaml. Note there are two separate client-options bags, and you will need to know which is which:
keboola_api:
auth:
client_options:
backoff_max_tries: 3 # 6.3.0+, auth path only: Storage token verify + Manage token exchange
# ...storage_client_options: # controller-facing clients, NOT affected by auth backoff settings
Leave the rest at defaults initially.
Where to look first
README.md for the intended integration story.src/Resources/config/*.yaml and src/DependencyInjection/KeboolaApiExtension.php — the extension holds the public service-id constants (e.g. KeboolaApiExtension::AUTH_MANAGE_CLIENT_FACTORY_ID), which are the stable handles you should wire against instead of class names.tests/ — the fixture app is the fastest working example of a controller + auth + error response, and AuthenticatorTestTrait is the supported way to stub auth.CHANGELOG.md — with a low-traffic package, breaking changes are documented here rather than in semver discipline.First use case
Expose an authenticated GET /health or GET /items endpoint and let the bundle handle token verification and error shaping, so you write only the controller body:
#[Route('/health', methods: ['GET'])]
public function health(): JsonResponse
{
return $this->json(['status' => 'ok']);
}
If the bundle's authenticator is wired correctly, an unauthenticated call returns the bundle's JSON error envelope (not an HTML 401), and a call with a valid X-StorageApi-Token header reaches your controller. That single round-trip proves auth, the firewall, and the exception listener are all live before you build anything real. It also warms you to the auth-path latency — see the retry notes below.
Let the bundle own the request/response envelope. The whole point is that every Keboola service looks the same to clients. Resist the urge to hand-roll { "error": ... } responses — surface failures by throwing domain exceptions and mapping them through the bundle's exception listener. Your controllers should return happy-path payloads only.
Auth stays declarative. Configure the firewall/authenticator from the bundle's config rather than writing a custom listener, and inject the resolved token/user context into controllers via the service the bundle exposes. In day-to-day code this means you never touch the Authorization/X-StorageApi-Token header directly.
Know which client you're tuning. Since 6.3.0 the bundle distinguishes three things that are easy to conflate:
| Concern | Config / handle | Affects |
|---|---|---|
| Auth retries | keboola_api.auth.client_options.backoff_max_tries (default 3) |
Storage token verification, Manage token exchange — nothing else |
| Controller-facing HTTP clients | storage_client_options |
Your outbound calls to Storage, not auth |
| Autowired Manage clients | ManageApiClientFactory (autowired) |
Your Manage API calls from app code |
Tuning auth retries to fail fast does not make your own Storage calls fail fast, and vice versa. If a request feels slow, sample the two paths separately before changing a number.
Paginate with the bundle's conventions, not ad hoc page/perPage. Keboola APIs use limit + offset with response metadata; if the bundle ships a pagination helper or a request-value object, use it so clients (and the Keboola UI) can page uniformly across services.
Version your routes from day one. /v1/... or a version header — the bundle's error envelope is version-stable, but your payloads won't be. Retrofitting versioning into a Keboola-style API is painful because downstream jobs hardcode paths.
Testing pattern. Functional tests should hit the kernel with a real (test) token rather than mocking the authenticator — you want to assert the envelope, not the middleware. Use the bundle's AuthenticatorTestTrait for the auth path; if you stub auth by hand, replace the service by its extension constant, not by class name:
// correct on 6.3.0+
$container->set(KeboolaApiExtension::AUTH_MANAGE_CLIENT_FACTORY_ID, $mockFactory);
// silently wrong on 6.3.0+: auth now resolves via the dedicated factory id,
// so this set() no longer overrides anything
$container->set(\Keboola\ManageApi\ClientFactory::class /* or ManageApiClientFactory::class */, $mockFactory);
For the outbound Storage client used by your app code, stub at the boundary (Guzzle handler stack / Symfony HttpClient mock) so tests don't require network access:
$client = new MockClient([...]); // storage token verify response
self::getContainer()->set('keboola.storage.client', $client);
Integration tips
keboola/php-component for config validation, keep that for app config and let api-bundle handle HTTP concerns — don't let the two overlap on error formatting.Adoption risk is the headline gotcha. Zero stars and zero dependents means no community backstop: no Stack Overflow answers, no third-party blog posts, and bug reports you file are the only ones. Pin an exact version in composer.json and read CHANGELOG.md before every bump. Releases are tagged and the compare links work (6.2.0…6.3.0), so you can diff properly — do that rather than skimming the notes. Treat upgrades as a code review, not a routine composer update.
The 6.3.0 test-stubbing change is the classic silent failure. Auth moved onto a dedicated Manage client factory registered under KeboolaApiExtension::AUTH_MANAGE_CLIENT_FACTORY_ID. Any hand-rolled test that replaced ManageApiClientFactory::class still compiles and passes setup — it just stops stubbing auth, so your suite quietly makes real network calls (or passes for the wrong reason). Grep your tests for the old class name on every upgrade; the fix is a one-line swap to the constant. Migrating to AuthenticatorTestTrait removes the exposure entirely.
Auth now retries — budget for it. Default backoff_max_tries: 3 with backoff sleep capped at 7 s means an unauthenticated or Storage-degraded request can spend meaningful time retrying before it ever reaches your controller. Consequences:
backoff_max_tries (or set 1) for latency-sensitive or internet-facing endpoints.storage_client_options.Service ID mismatches after a version bump. 6.3.0 is a live example: a factory's lookup id changed without the class being removed. If you've decorated or overridden a bundle service by class or by a literal string, an upgrade can either fail at container compile time (the good outcome — but only if you run bin/console cache:clear / lint:container in CI) or, worse, keep working while ignoring your override. Reference KeboolaApiExtension::*_ID constants wherever they exist.
Debugging the error envelope. When an error comes back HTML instead of JSON, the cause is almost always listener priority or a firewall catching the request before the bundle's authenticator. Diagnose with:
bin/console debug:event-dispatcher kernel.exception
bin/console debug:config keboola_api
A 401 that renders a Symfony error page rather than the Keboola envelope means auth is running outside the bundle's path — check firewall pattern overlap. If a 401 takes several seconds to appear, that's the new backoff, not a hang.
Extension points are the escape hatch. Rather than forking, use Symfony's decoration to replace behaviour:
services:
App\Api\MyAuthClientFactory:
decorates: KeboolaApiExtension::AUTH_MANAGE_CLIENT_FACTORY_ID
Confirm ids with bin/console debug:container --show-private | grep -i keboola. Decoration survives most minor upgrades; monkey-patching the vendor directory does not.
Documentation is thin — budget for source reading. With a score this low, expect to read the bundle's src/ to answer questions. Keep a short internal notes file (config keys, both client-options bags, the KeboolaApiExtension::*_ID constants you depend on, and the exact version you're on) so the next developer doesn't repeat the archaeology. If the package becomes load-bearing, plan an exit path: the interfaces it standardises are simple enough to reimplement in a few hundred lines if maintenance stops.
How can I help you explore Laravel packages today?