keboola/storage-api-client
Keboola Storage API PHP client for managing buckets, tables, files, and workspaces in the Keboola data platform. Handles authentication, data imports and exports, and file transfers via a simple object-oriented interface.
Install with Composer and confirm you are pulling the real distribution, not just the read-only mirror:
composer require keboola/storage-api-client
This repository is marked [READ-ONLY], has almost no public dependents, and is effectively a mirror of code maintained inside Keboola's monorepo. Check Packagist and the package's changelog before pinning a version, and treat GitHub issues/PRs here as informational rather than the primary support channel.
First steps:
Client with a Storage API token and the correct stack host:use Keboola\StorageApi\Client;
$client = new Client([
'token' => getenv('KBC_STORAGE_TOKEN'),
'url' => 'https://connection.keboola.com', // or the regional/stack host
]);
$tokenInfo = $client->verifyToken();
This gives you owner, project, permissions, and expiry before you do any real work. Fail fast here rather than on a random 401 later.
Look first at these classes:
Keboola\StorageApi\Client — core HTTP, jobs, raw API helpers, verifyToken, waitForJobKeboola\StorageApi\Buckets, Tables, Workspaces, Components, Tokens, MetadataKeboola\StorageApi\Options\... — typed option objects for many operationsKeboola\StorageApi\TableExporter / TableImporter — bulk data pathsKeboola\Csv\CsvFile — CSV construction for loadsFirst real use case: list buckets/tables, then load a CSV into a table asynchronously and wait for the job:
$tables = new \Keboola\StorageApi\Tables($client);
$tableId = 'in.c-main.customers';
$csv = new \Keboola\Csv\CsvFile('/tmp/customers.csv');
$jobId = $client->writeTableAsync($tableId, $csv, [
'incremental' => false,
]);
$result = $client->waitForJob($jobId);
That async-job handshake is the central pattern of the whole package. If you understand writeTableAsync() + waitForJob(), you understand most of the client.
Wrap the client once and inject it. The Client is cheap but stateful (run ID, backoff settings, token). Build it in your service container, validate the token at boot, and share the same instance. Do not construct a new client per call unless you deliberately want different tokens/hosts.
Treat every data operation as a job. Writes, exports, workspace creation, and many component operations return a job ID. The consistent pattern is:
$jobId = $tables->/* or $client-> */someAsyncOperation(...);
$result = $client->waitForJob($jobId);
Inspect job results for warnings, imported rows, and output file IDs. Do not assume waitForJob() returning means the data is semantically what you expected — check $result['results'] and $result['warnings'].
Choose the right upload path by size.
CsvFile + writeTableAsync().TableImporter, direct-to-storage uploads, or uploadFile() + async table write) so you do not stream gigabytes through PHP memory.getTableData() for anything large; it returns rows in memory. Use TableExporter::exportTable() to stream to a file.Export with intent. TableExporter supports column selection, filters, limits, and changed-since/changed-until style options. Use these server-side rather than exporting everything and filtering in PHP. Export to a temp file or stream, then hand off to your own storage/SFTP/warehouse step.
Make loads idempotent. Prefer deterministic table IDs, explicit primary keys, and incremental only when you truly mean append. For full refreshes, write to a staging table and swap/alias rather than truncating a live table in place. Storage API aliases are useful for blue/green table swaps.
Use workspaces for SQL-heavy transforms. Keboola\StorageApi\Workspaces lets you create a transient database workspace, load selected tables, run SQL, and unload results back. This is usually faster and cleaner than pulling data into PHP, transforming, and re-uploading. Always delete workspaces in a finally block — they cost money and linger.
Use Components/configurations for orchestration metadata. If you are building extractor/writer-style integrations, create component configurations through the client rather than inventing your own config store. This keeps you compatible with Keboola runners and UI.
Escape hatch: raw API calls. When a convenience method is missing or lagging, use apiGet(), apiPost(), apiPut(), apiDelete() on the client. This is the intended extension point and is much safer than hand-rolling Guzzle calls, because you keep auth, retries, and URL construction.
Set runId and userAgent for observability. Client::setRunId() tags API calls/jobs so they can be traced in Keboola. Set a meaningful userAgent in client options so your application is identifiable in API logs.
Configure backoff and logging at construction. Pass backoffMaxTries, a PSR-3 logger, and userAgent in the client options. This is much better than sprinkling retry loops around individual calls.
Testing pattern. Unit-test your wrappers against mocked Client/resource classes. For integration tests, use a dedicated Keboola project and token with minimal permissions, create uniquely named buckets/tables per test run, and clean up in tearDown. Do not point tests at a shared production project.
waitForJob() means your next step races the load. Likewise, waitForJob() itself can time out on very long jobs; use a generous max-tries/timeout and handle a still-running job explicitly.Keboola\StorageApi\ClientException, inspect getStatusCode() and the error string code, and map 404/409 to domain-specific handling.canManageBuckets, etc.). Run verifyToken() and log the permissions when debugging.url (e.g., a different region or connection.keboola.com vs an Azure stack) will fail in non-obvious ways. Make the host configurable per environment.getTableData() and naive CsvFile reads. Use TableExporter and sliced/direct uploads. Also watch CSV encoding, delimiter, enclosure, and header settings — mismatches surface as malformed rows or “no data” loads rather than clean errors.CsvFile is file-backed. It is great for local temp files but awkward for streamed/generated data at scale. For generated data, consider writing to a temp file first or using the direct upload APIs.ListFilesOptions/file APIs to prune old uploads, and always delete workspaces in finally.api* methods, custom PSR-3 logger, userAgent, runId, and backoff settings. If you need more, wrap the client rather than subclassing it.verifyToken() output and request logs as sensitive, and redact tokens in exception reporting.How can I help you explore Laravel packages today?