kraken-io/kraken-php
PHP client for the Kraken.io image optimization API, offering lossy and lossless compression, resizing, cropping, and format conversion for JPEG, PNG, GIF, and WebP images via file upload or URL, with asynchronous callback support.
Install
composer require kraken-io/kraken-php
There is no service provider or facade shipped with the package—it is a thin, framework-agnostic SDK. You wire it into Laravel manually.
Credentials first
Add to .env:
KRAKEN_API_KEY=
KRAKEN_API_SECRET=
Register in config/services.php:
'kraken' => [
'api_key' => env('KRAKEN_API_KEY'),
'api_secret' => env('KRAKEN_API_SECRET'),
],
Bind a singleton in AppServiceProvider::register() (or use a dedicated KrakenService wrapper, see below):
$this->app->singleton(\Kraken\Client::class, fn () => new \Kraken\Client([
'auth' => [
'api_key' => config('services.kraken.api_key'),
'api_secret' => config('services.kraken.api_secret'),
],
]));
First use case: optimize an uploaded image
The most common day-one task is shrinking user uploads before you store them.
use Kraken\Client;
public function store(Request $request, Client $kraken)
{
$path = $request->file('image')->store('tmp', 'local');
$result = $kraken->upload([
'file' => Storage::disk('local')->path($path),
'wait' => true,
'lossy' => true,
]);
// $result['kraked_url'] is the optimized image; download & store it
$bytes = file_get_contents($result['kraked_url']);
Storage::disk('public')->put('images/photo.jpg', $bytes);
return redirect()->back();
}
Where to look next in the source (src/Kraken/): Client.php for the upload() / url() / user() methods, and the request class for the option payload. Everything you can pass is a snake_case key straight from the Kraken.io HTTP API docs—the PHP package does almost no transformation.
Wrap it in a service, don't call Client from controllers
namespace App\Services;
use Kraken\Client;
use Illuminate\Support\Facades\Storage;
class ImageOptimizer
{
public function __construct(private Client $kraken) {}
public function optimize(string $diskPath, string $disk = 'public'): array
{
$result = $this->kraken->upload([
'file' => Storage::disk($disk)->path($diskPath),
'wait' => true,
'lossy' => true,
'quality' => 82,
'auto_orient' => true,
'preserve_meta'=> ['copyright'],
'resize' => ['width' => 2000, 'height' => 2000, 'strategy' => 'auto'],
]);
return $result;
}
}
Always run it on a queue
wait => true blocks for the duration of the optimization. For anything user-facing, dispatch a job:
class OptimizeImage implements ShouldQueue
{
public int $tries = 3;
public int $backoff = 30;
public function __construct(public string $path) {}
public function handle(ImageOptimizer $optimizer)
{
$optimizer->optimize($this->path);
ImageOptimized::dispatch($this->path);
}
}
Async via callback for batch / large jobs
$kraken->upload([
'file' => $absolutePath,
'wait' => false,
'callback_url' => route('webhooks.kraken'), // must be publicly reachable
'lossy' => true,
]);
The response contains an id. Persist it against your model so the webhook handler can correlate:
Route::post('/webhooks/kraken', KrakenWebhookController::class)
->withoutMiddleware(['web']);
Kraken POSTs the full result (including kraked_url) back to you; store the bytes and mark the record as processed.
Source URLs beat temp files when the image is already public
If the asset sits on S3 with a public URL, use url() instead of upload(). It avoids downloading the file locally and avoids needing the file to exist on your web server:
$kraken->url(['url' => $publicUrl, 'wait' => true, 'lossy' => true]);
Push straight to storage (skip the round-trip)
The API supports s3_store, azure_store, gcs_store, rclone_store, and kraken_store options. If you already have S3 credentials configured, let Kraken write directly to your bucket instead of downloading kraked_url back through your app—much cheaper for large batches.
Convert to modern formats in the same call
Use convert (e.g. ['format' => 'webp']) rather than the legacy webp flag; it's the current API surface and composes with resize.
Monitor your quota
$kraken->user()->balance() returns your plan quota. Surface it on an internal admin dashboard or log it nightly—Kraken returns errors once you exceed the plan, and you don't want to discover that in production.
Laravel's Http::fake() will NOT intercept this package. It uses Guzzle directly rather than Laravel's HTTP client, so fakes silently miss and your tests hit the real API. Mock Kraken\Client (or your ImageOptimizer wrapper) in the container instead:
$this->mock(ImageOptimizer::class, fn ($m) => $m->shouldReceive('optimize')
->andReturn(['kraked_url' => 'https://example.test/x.jpg']));
The package does not modify your local file. Optimization happens server-side; you get a kraked_url (or a stored object) back. You must download and persist it yourself unless you use an external store option. The original file on disk is untouched.
upload() needs a local filesystem path, not a Laravel UploadedFile or an S3 key. Use $file->getRealPath() or Storage::path(); for S3-only files, either presign a URL and use url(), or pull to a temp path first.
Callbacks need a public, CSRF-exempt route. Any route in routes/web.php will 419 from Kraken's POST. Register it in routes/api.php or explicitly strip the web middleware group. Make the handler idempotent—Kraken may retry—and key off the returned id.
Synchronous calls can time out for big images. Large PNGs at high quality can take tens of seconds. If you must call synchronously, bump max_execution_time or (better) move it to a job. The default queue worker timeout of 60s is often enough, but set $timeout explicitly on the job.
Options are snake_case and passed verbatim. A typo like callbackUrl or autoOrient is not validated client-side—the API silently ignores unknown keys and you get a confusingly unoptimized result. When debugging, dump the exact array you're sending before the call.
quality only meaningfully applies with lossy => true. The defaults are sane; don't start at quality => 100 thinking you'll get lossless—you'll get slow requests and near-original sizes.
preserve_meta is opt-in per field. If you need EXIF orientation preserved for camera photos, set auto_orient => true. If you need copyright/geotag data, list it in preserve_meta—otherwise all metadata is stripped, which is usually what you want for web images.
Read the error, don't swallow it. Failed calls throw an exception (from Kraken\Exception); catch it at the job level and decide between retry and dead-lettering. Auth failures (401/403) are not retryable—bad keys or exhausted quota. Timeouts and 5xx are retryable.
Config caching will bite you. After adding services.kraken, run php artisan config:clear in local and config:cache in production. env() inside a cached config returns null, so always go through config('services.kraken.*'), never env() directly in code.
No first-party Laravel integration means no events, no rate limiter, no HTTP client instrumentation. If you need retry/backoff on transient failures, implement it on the job ($tries/$backoff) or wrap the client in a decorator—you won't get it from the package.
Version pinning matters. The library changes infrequently (last release Sept 2024), but a composer update can still move API expectations. Pin to a minor range (^1.x) and rely on your wrapper's tests to catch payload changes.
How can I help you explore Laravel packages today?