Menu
phplaraveltutorialscreenshots

Website Screenshots in Laravel: PHP HTTP Client vs Screenshot API

SnapSharp Team·April 15, 2026·8 min read

Laravel gives you multiple options for capturing webpage screenshots. This guide covers all three approaches — including when to use each — with working code you can drop into any Laravel 10/11 project.

Option 1: Browsershot (Puppeteer bridge)

Spatie's Browsershot wraps Puppeteer via Node.js. It's the most popular Laravel screenshot package.

Requirements

composer require spatie/browsershot
npm install puppeteer

You also need Node.js ≥ 18 on the server. In practice this means Puppeteer on every deploy — Chromium binaries, shared memory config, and regular crashes in constrained environments.

Basic screenshot

use Spatie\Browsershot\Browsershot;

Browsershot::url('https://example.com')
    ->windowSize(1280, 720)
    ->save('/tmp/screenshot.png');

Full-page screenshot

Browsershot::url('https://docs.example.com')
    ->fullPage()
    ->waitUntilNetworkIdle()
    ->save('/tmp/docs-full.png');

Store in S3

use Illuminate\Support\Facades\Storage;

$tmpPath = tempnam(sys_get_temp_dir(), 'browsershot_') . '.png';

Browsershot::url('https://example.com')
    ->windowSize(1280, 720)
    ->save($tmpPath);

Storage::disk('s3')->put('screenshots/example.png', file_get_contents($tmpPath));
unlink($tmpPath);

Drawbacks: Requires Node.js + Puppeteer on every server. Crashes under load. Cold starts add 2-4 seconds. Chromium needs 200 MB RAM minimum.


Option 2: SnapSharp API (no server dependency)

The SnapSharp API runs Chromium for you — your Laravel app makes an HTTP request and gets back an image. No Node.js, no Chromium, no shared memory config.

Setup

No package required — Laravel's HTTP client handles it. Add your API key to .env:

SNAPSHARP_API_KEY=sk_live_your_key_here

Basic screenshot helper

// app/Services/ScreenshotService.php
namespace App\Services;

use Illuminate\Support\Facades\Http;

class ScreenshotService
{
    private string $apiKey;
    private string $baseUrl = 'https://api.snapsharp.dev/v1';

    public function __construct()
    {
        $this->apiKey = config('services.snapsharp.key');
    }

    public function capture(string $url, array $options = []): string
    {
        $params = array_merge([
            'url'    => $url,
            'width'  => 1280,
            'height' => 720,
            'format' => 'png',
        ], $options);

        $response = Http::withToken($this->apiKey)
            ->timeout(30)
            ->get("{$this->baseUrl}/screenshot", $params);

        $response->throw();

        return $response->body();
    }

    public function captureToS3(string $url, string $path, array $options = []): string
    {
        $params = array_merge($options, [
            'url'          => $url,
            'upload_to_s3' => true,
        ]);

        $response = Http::withToken($this->apiKey)
            ->timeout(30)
            ->get("{$this->baseUrl}/screenshot", $params);

        $response->throw();

        return $response->json('s3_url');
    }
}

Register in config/services.php:

'snapsharp' => [
    'key' => env('SNAPSHARP_API_KEY'),
],

Full-page screenshot in dark mode

$image = app(ScreenshotService::class)->capture(
    'https://yourapp.com/pricing',
    [
        'full_page' => true,
        'dark_mode' => true,
        'format'    => 'webp',
        'quality'   => 85,
    ]
);

file_put_contents(storage_path('app/screenshots/pricing.webp'), $image);

Screenshots behind authentication (with cookies)

$image = app(ScreenshotService::class)->capture(
    'https://yourapp.com/dashboard',
    [
        'cookies' => json_encode([
            [
                'name'   => 'laravel_session',
                'value'  => 'your_session_value',
                'domain' => 'yourapp.com',
            ],
        ]),
        'width'  => 1440,
        'height' => 900,
    ]
);

Artisan command for bulk screenshots

// app/Console/Commands/CaptureScreenshots.php
namespace App\Console\Commands;

use App\Services\ScreenshotService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;

class CaptureScreenshots extends Command
{
    protected $signature   = 'screenshots:capture {--force}';
    protected $description = 'Capture screenshots for all published pages';

    public function handle(ScreenshotService $snapsharp): int
    {
        $pages = \App\Models\Page::where('published', true)->get();
        $this->info("Capturing {$pages->count()} pages...");

        $bar = $this->output->createProgressBar($pages->count());
        $bar->start();

        foreach ($pages as $page) {
            if (!$this->option('force') && $page->screenshot_url) {
                $bar->advance();
                continue;
            }

            try {
                $bytes = $snapsharp->capture($page->url, [
                    'width'   => 1280,
                    'format'  => 'webp',
                    'quality' => 80,
                ]);

                $path = "screenshots/{$page->id}.webp";
                Storage::disk('s3')->put($path, $bytes, 'public');
                $page->update(['screenshot_url' => Storage::disk('s3')->url($path)]);
            } catch (\Exception $e) {
                $this->warn("\n  Failed {$page->url}: {$e->getMessage()}");
            }

            $bar->advance();
        }

        $bar->finish();
        $this->newLine();
        $this->info('Done.');
        return self::SUCCESS;
    }
}
php artisan screenshots:capture
php artisan screenshots:capture --force  # Re-capture all

Queue job for async screenshot capture

For non-blocking screenshot capture (e.g. triggered on model creation):

// app/Jobs/CapturePageScreenshot.php
namespace App\Jobs;

use App\Models\Page;
use App\Services\ScreenshotService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;

class CapturePageScreenshot implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(public readonly Page $page) {}

    public function handle(ScreenshotService $snapsharp): void
    {
        $bytes = $snapsharp->capture($this->page->url, [
            'width'  => 1280,
            'format' => 'webp',
        ]);

        $path = "screenshots/{$this->page->id}.webp";
        Storage::disk('s3')->put($path, $bytes, 'public');

        $this->page->update([
            'screenshot_url' => Storage::disk('s3')->url($path),
            'screenshot_at'  => now(),
        ]);
    }
}

Dispatch after save:

// In Page model observer or controller
CapturePageScreenshot::dispatch($page)->delay(now()->addSeconds(5));

Scheduled screenshots with Laravel Scheduler

Capture marketing pages every night:

// routes/console.php (Laravel 11) or app/Console/Kernel.php (Laravel 10)
Schedule::command('screenshots:capture')->dailyAt('03:00');

Handling rate limits

SnapSharp returns HTTP 429 when your rate limit is exceeded. Wrap retries around it:

use Illuminate\Http\Client\RequestException;

function screenshotWithRetry(string $url, int $maxAttempts = 3): string
{
    $attempt = 0;
    $lastException = null;

    while ($attempt < $maxAttempts) {
        try {
            return app(ScreenshotService::class)->capture($url);
        } catch (RequestException $e) {
            if ($e->response->status() === 429) {
                $retryAfter = (int) ($e->response->header('Retry-After') ?: 60);
                sleep($retryAfter);
            } else {
                throw $e;
            }
        }
        $attempt++;
    }

    throw new \RuntimeException("Screenshot failed after {$maxAttempts} attempts");
}

Option 3: Playwright (headless via proc_open)

You can shell out to Playwright from PHP, but this combines the worst of both worlds — the complexity of a headless browser with the fragility of proc_open. Avoid this in production. Use Browsershot (which wraps Node.js Playwright/Puppeteer properly) or the SnapSharp API instead.


Comparison

BrowsershotSnapSharp API
Setupnpm + Puppeteer installAPI key only
Node.js requiredYesNo
Server memory200 MB+ (Chromium)0 (runs elsewhere)
ReliabilityChromium crashes under loadManaged, auto-scaled
Stealth modeNoYes (Starter+)
Full-pageYesYes (Starter+)
Dark modeManual CSS injectionNative parameter
Custom cookiesManualNative parameter
Rate limitingYour server's limitsPer-plan
CostServer time + memoryAPI pricing

For most Laravel apps — especially if you don't already run Node.js — the SnapSharp API is significantly simpler to operate.


Related: PHP screenshot examples · Screenshot API docs · Automate screenshots in Python

Website Screenshots in Laravel: PHP HTTP Client vs Screenshot API — SnapSharp Blog