Remove Image Backgrounds in PHP and Laravel with an API (cURL, Guzzle, Http Client)
PHP does not have a good in-process background-removal option — the Python models do not port, and shelling out to them from a web request is a support ticket waiting to happen. An HTTP call is the sane path. This tutorial shows the bgclear API from plain cURL, from Guzzle, and from Laravel's Http client, then a queued job for catalogues. The request format is remove.bg's, so if you used the popular remove.bg PHP/Laravel package, the migration is a URL and a header.
In this guide
Plain PHP with cURL
CURLFile builds the multipart part; the response body is the PNG itself.
<?php
$key = getenv('BGCLEAR_API_KEY');
$ch = curl_init('https://www.bgclear.ai/api/v1/remove');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_POSTFIELDS => [
'image_file' => new CURLFile('photo.jpg', 'image/jpeg', 'photo.jpg'),
'size' => 'auto', // preview (free, ≤800px) | full (1 credit) | auto
'format' => 'png', // png | webp | jpg
],
CURLOPT_TIMEOUT => 60,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
$err = json_decode($body, true)['error'] ?? ['message' => $body];
throw new RuntimeException("bgclear $status: {$err['message']}");
}
file_put_contents('photo-no-bg.png', $body);Add 'bg_color' => 'ffffff' and 'format' => 'jpg' for a marketplace-ready white background, or 'crop' => 'true' to trim to the subject.
Guzzle: multipart and JSON modes
use GuzzleHttp\Client;
$client = new Client(['base_uri' => 'https://www.bgclear.ai/api/v1/', 'timeout' => 60]);
$auth = ['Authorization' => 'Bearer ' . getenv('BGCLEAR_API_KEY')];
// multipart upload → image bytes back
$res = $client->post('remove', [
'headers' => $auth,
'multipart' => [
['name' => 'image_file', 'contents' => fopen('photo.jpg', 'r'), 'filename' => 'photo.jpg'],
['name' => 'size', 'contents' => 'auto'],
],
]);
file_put_contents('photo-no-bg.png', (string) $res->getBody());
echo 'credits left: ', $res->getHeaderLine('X-Credits-Remaining'), PHP_EOL;
// URL input → JSON with a hosted result URL (valid 24 h, no auth to fetch)
$res = $client->post('remove', [
'headers' => $auth + ['Accept' => 'application/json'],
'json' => ['image_url' => 'https://example.com/shoe.jpg', 'size' => 'auto',
'format' => 'webp', 'bg_color' => 'ffffff'],
]);
$data = json_decode((string) $res->getBody(), true);
// $data['url'], $data['width'], $data['height'], $data['credits_remaining']Guzzle throws ClientException on 4xx; read $e->getResponse()->getBody() for the JSON error with its stable code (insufficient_credits on 402, rate_limited on 429).
Laravel: Http client with attach()
Laravel's client wraps Guzzle and reads naturally. Put the key in config/services.php and .env, never in the controller.
// config/services.php
'bgclear' => ['key' => env('BGCLEAR_API_KEY')],use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
public function cutout(string $path): string
{
$response = Http::withToken(config('services.bgclear.key'))
->timeout(60)
->attach('image_file', Storage::get($path), basename($path))
->post('https://www.bgclear.ai/api/v1/remove', [
'size' => 'auto',
'format' => 'png',
]);
if ($response->failed()) {
$error = $response->json('error.message') ?? $response->body();
throw new \RuntimeException("bgclear {$response->status()}: $error");
}
$out = 'cutouts/' . pathinfo($path, PATHINFO_FILENAME) . '.png';
Storage::put($out, $response->body());
return $out;
}For an uploaded file in a controller, $request->file('image')->get() gives the contents for attach(). The idempotency header stops a double-submitted form from charging twice: ->withHeaders(['Idempotency-Key' => md5_file($tmpPath)]).
Bulk: a queued job per image
Catalogue imports belong in the queue, one job per image, with retries that cannot double-charge and a 429 that backs off instead of failing the batch.
class RemoveBackgroundJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5;
public function backoff(): array { return [10, 30, 60, 120, 300]; }
public function __construct(public int $productImageId) {}
public function handle(): void
{
$image = ProductImage::findOrFail($this->productImageId);
$response = Http::withToken(config('services.bgclear.key'))
->withHeaders(['Idempotency-Key' => 'product-image-' . $image->id])
->timeout(90)
->attach('image_file', Storage::get($image->path), basename($image->path))
->post('https://www.bgclear.ai/api/v1/remove', ['size' => 'full', 'format' => 'webp', 'bg_color' => 'ffffff']);
if ($response->status() === 429) {
$this->release((int) $response->header('X-RateLimit-Reset', 10));
return;
}
if ($response->status() === 402) {
$this->fail(new \RuntimeException('Out of credits — top up at bgclear.ai/api-pricing/'));
return;
}
$response->throw();
Storage::put("catalogue/{$image->id}.webp", $response->body());
$image->update(['cutout_path' => "catalogue/{$image->id}.webp"]);
}
}Dispatch with RemoveBackgroundJob::dispatch($id) from an Artisan command that iterates the catalogue; queue:work --max-jobs keeps memory flat. For images over 4 megapixels, post to /api/v1/jobs instead and let a callback route finish the work — see the bulk guide.
If you used the remove.bg PHP package
The widely used mtownsend/remove-bg package (and the Laravel wrapper) hard-codes api.remove.bg and the X-Api-Key header, and remove.bg's API ends on 1 December 2026. You do not need a replacement package — the Http snippet above is shorter than the fluent call was. Map the parameters one-to-one: ->size('auto') → 'size' => 'auto', ->format('png') → 'format' => 'png', ->bgColor('ffffff') → 'bg_color' => 'ffffff'. Drop ->type(), ->roi(), ->scale() and the zip format; they have no equivalent. The full mapping is on the migration page, pricing on API pricing.
Frequently asked questions
Is there an official PHP or Laravel package?
No, and you do not need one — Laravel's Http client or Guzzle covers the whole API in a few lines, as shown above.
Can I send the image as a URL instead of a file?
Yes — post JSON with image_url (or image_file_b64) and add Accept: application/json to get a hosted result URL back instead of bytes.
What does a full-resolution image cost?
One credit: $9 for 100, $39 for 500, $129 for 2,000. Credits never expire, previews (size=preview, ≤800 px) are free, failed requests are not charged.
How do I keep the key out of the repo?
Store it in .env as BGCLEAR_API_KEY, read it through config('services.bgclear.key'), and rotate it from the API dashboard if it ever leaks.