uploadcare/uploadcare-php
Uploadcare PHP SDK for uploading, storing, and delivering files and images via the Uploadcare CDN, with secure uploads, on-the-fly image transformations, and a simple API for integrating file handling into Laravel and PHP apps.
Install and wire keys.
composer require uploadcare/uploadcare-php
The SDK is HTTP-client agnostic (PSR-18 / PSR-17). Laravel already ships guzzlehttp/guzzle, which satisfies both, so no extra install is normally needed.
// config/services.php
'uploadcare' => [
'public_key' => env('UPLOADCARE_PUBLIC_KEY'),
'secret_key' => env('UPLOADCARE_SECRET_KEY'),
],
Bind the client once. There is no Laravel service provider shipped, so register a singleton yourself — this keeps the HTTP connection pool and serializer shared.
// App\Providers\AppServiceProvider::register()
$this->app->singleton(\Uploadcare\Api::class, function () {
$config = \Uploadcare\Configuration::create(
config('services.uploadcare.public_key'),
config('services.uploadcare.secret_key'),
);
$config->setHttpClient(new \GuzzleHttp\Client([
'timeout' => 15,
'connect_timeout' => 5,
]));
return new \Uploadcare\Api($config);
});
First use case — accept an upload, store it, return the CDN URL.
public function store(Request $request, \Uploadcare\Api $uploadcare)
{
$request->validate(['photo' => ['required', 'file', 'max:20480']]);
$file = $uploadcare->uploader()->fromPath(
$request->file('photo')->getRealPath()
);
$uploadcare->file()->storeFile($file->getUuid()); // otherwise it expires
return response()->json([
'uuid' => $file->getUuid(),
'url' => (string) $file->getUrl(), // https://ucarecdn.com/<uuid>/
]);
}
Where to look first. Read Uploadcare\Api — it is a thin facade exposing uploader(), file(), group(), webhook(), conversion(), addon(), project(). Everything you will use daily lives under src/Apis/ and src/Uploader/. Ignore the serializer internals until you need custom fields.
Server-side vs. browser-side upload. For large media, don't proxy bytes through PHP at all: put the Uploadcare Widget / JS client on the front end (public key only), let it upload directly, then POST the resulting UUID to Laravel. Your server then only calls storeFile() and records the UUID. Reserve uploader()->fromPath() for admin imports, generated files (PDFs, exports), and fromUrl() for "import from a remote link" — fromUrl() makes Uploadcare fetch the asset, which is ideal for migrating existing media without downloading it first.
$file = $uploadcare->uploader()->fromUrl('https://legacy.example.com/img/1234.jpg');
$file = $uploadcare->uploader()->fromContent($pdfBinary); // generated files
Signed uploads for authenticated users. If only logged-in users may upload, generate the signature/expiry pair server-side and pass it to the widget. The public key alone allows anonymous uploads into your project.
Groups for multi-file submissions. A gallery, a product with several images, or a message with attachments maps cleanly to one group:
$uuids = collect($files)->map(fn ($f) => $uploadcare->uploader()->fromPath($f)->getUuid());
$group = $uploadcare->group()->createGroup($uuids->all());
// CDN root for the whole set: https://ucarecdn.com/{$group->getUuid()}/
Store group_uuid alongside individual UUIDs; the group URL is what you render, the individual UUIDs are what you delete or transform.
Derived images belong in the URL, not in the database. Uploadcare's CDN does resizing, cropping, format negotiation and smart cropping on the fly. Persist the raw UUID/URL and append operations at render time:
$thumb = $file->getUrl() . '-/resize/400x300/-/format/auto/-/quality/smart/';
This keeps your schema stable when design requirements change and lets the edge cache do the work.
Webhooks as the sync mechanism. Uploads are asynchronous from the browser's point of view, and conversions/scans finish later. Register a webhook (file.uploaded, file.stored, file.info.updated, file.deleted) and have the handler verify the signature, then dispatch a queued job. Treat the webhook as a notification to go re-fetch the truth via $uploadcare->file()->getFile($uuid) rather than trusting the payload as a complete record.
Route::post('/webhooks/uploadcare', UploadcareWebhookController::class)
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
Keep a local mirror table. uploads with uuid, group_uuid, original_filename, mime, size, cdn_url, status (pending/stored/failed), and your own user_id/model_id morph. You cannot query Uploadcare by your domain concepts, so all listing, authorization and joins must happen locally. Update the row from the webhook job, not from the upload request — that way a lost HTTP response from the browser doesn't orphan the record.
Async conversions. Document and video conversions return a job identifier; poll $uploadcare->conversion() for status or drive it from a queued job with backoff. Don't block a web request waiting for a 200MB video transcode.
Queued uploads. Uploading in a job gives you retries and keeps request latency flat. Make the job idempotent by storing a deterministic key (e.g. your uploads.id) and checking the row's status before re-uploading on retry.
Wrap it behind your own interface. Uploadcare\Api is concrete and hard to fake. A small MediaStorage interface with an UploadcareMediaStorage implementation (and a fake for tests) will save you pain later — see testing note below.
Files are temporary until stored. This is the number one production bug. Uploaded files are purged (~24h) unless you call storeFile() or enable auto-store on the project. Browser-widget uploads are affected too. If you see URLs that work in staging and 404 the next morning, this is why.
Never ship the secret key. Public key → widget, browser, signed-upload config. Secret key → server only, because it grants full REST access to every file in the project. If it leaks, rotate immediately; there is no per-key scoping.
Configure an HTTP client with timeouts. Left to auto-discovery the SDK may resolve a client with no timeout (Guzzle's default is effectively infinite), and a stalled connection will hang a queue worker until your job timeout kills it. Set timeout and connect_timeout explicitly, and consider Guzzle's RetryMiddleware for 429/5xx — the SDK does not abstract rate limiting for you.
Pagination is not automatic. $uploadcare->file()->listFiles() returns one page. Iterating the whole project requires walking the collection's next-page link; on a large account an "export all files" loop will take a long time and hit rate limits. Filter by stored/removed and date ranges where the API allows, and throttle.
Metadata is replaced, not merged. saveFileInfo() performs a whole-object write of the appdata field. Read the current info, modify, write back — otherwise concurrent writers clobber each other. Keep the payload small; there is a per-file size ceiling.
Webhook signature verification needs the raw body. Laravel's $request->json() re-serializes and will break the HMAC. Compute the signature over $request->getContent() and compare with hash_equals(). Also make handlers idempotent — webhooks are retried, so use event + uuid as a dedupe key.
Group deletion ≠ file deletion. Deleting a group removes the grouping, not the underlying files. Conversely, deleting a file that is still referenced by a group leaves a dangling reference. Decide your ownership model up front (files owned by the group, or vice versa) and enforce it in one place.
CDN operations are opaque strings. A typo in -/resize/400x300/ produces a CDN 404 at render time, not an SDK exception. Centralize operation strings in constants or a small URL builder so they are testable and greppable.
"Secure delivery" changes your URLs. If the project has signed/private delivery enabled, plain https://ucarecdn.com/{uuid}/ URLs stop working — you need to append a signature token generated with the secret key. Decide this per project before you hard-code URL construction.
No Flysystem adapter. Uploadcare is not a general-purpose object store (no arbitrary key paths, no overwrite semantics). Storage::disk('uploadcare') does not exist; write a small custom disk or, more sensibly, model media as domain records with UUIDs rather than filesystem paths.
Large files must not be buffered. Use fromPath() (streamed) rather than file_get_contents() + fromContent() for anything over a few megabytes. For very large assets use the multipart uploader, and check PHP's upload_max_filesize / post_max_size if you are proxying through Laravel at all.
Exceptions. Catch narrowly around uploads vs. REST calls — upload failures (network, rejected content) and REST failures (404 unknown UUID, 401 bad keys, 429 rate limit) warrant different handling. Log the UUID and the exception class; the message often carries the Uploadcare error code. A 401 after a deploy almost always means the secret key env var is empty or has trailing whitespace from a .env copy-paste.
Version drift in tutorials. Older 2.x code uses new Api($public, $secret); 3.x/4.x requires Configuration::create() plus an explicit HTTP client. Copy-pasting old examples is a common source of "class not found" or "no HTTP client" errors. Pin the major version in composer.json and read the changelog before upgrading.
Testing. There is no Laravel fake or HTTP recorder. Either inject a mock PSR-18 client into Configuration and return canned JSON, or — better for day-to-day work — depend on your own MediaStorage interface and bind a fake implementation in the test container. Assert on UUIDs and store/delete calls, not on Uploadcare's response shapes.
How can I help you explore Laravel packages today?