ScreenshotRun's API plugs into Laravel through the built-in Http facade. One GET request captures any URL as a PNG, JPEG, WebP, AVIF, or PDF. No Puppeteer, no Chrome binary on the server, no Spatie Browsershot configuration to maintain.
Laravel developers who need screenshots usually install Spatie Browsershot. It wraps Puppeteer, which wraps Chrome. That chain works on a local Valet setup. Push it to Forge, Vapor, or a shared host, and you discover that Chrome needs specific system packages. Puppeteer needs a matching Node.js version. The whole stack adds 400+ MB to your Docker image. If you only need URL-to-image conversion, the Http facade does the job in four lines.
Your first screenshot from Laravel
Head to the dashboard and create an account. Once you confirm your email, the API key shows up in your project settings. The free plan gives you 200 captures a month with no restrictions on parameters.
Drop this into a route or an Artisan command to test it:
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
$response = Http::withToken(config('services.screenshotrun.key'))
->get('https://api.screenshotrun.com/v1/screenshots/capture', [
'url' => 'https://github.com',
'format' => 'png',
'response_type' => 'image',
]);
if ($response->successful()) {
Storage::put('screenshots/github.png', $response->body());
echo 'Saved: screenshots/github.png (' . round(strlen($response->body()) / 1024) . ' KB)';
}
Store your API key in config/services.php:
'screenshotrun' => [
'key' => env('SCREENSHOTRUN_KEY'),
],
And add SCREENSHOTRUN_KEY=your_key_here to your .env file. Laravel's Http facade uses Guzzle under the hood, so connection pooling and timeouts work automatically.
Dedicated screenshot service class
A service class you can inject anywhere through Laravel's container:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class ScreenshotService
{
public function capture(
string $url,
string $format = 'png',
int $width = 1280,
bool $fullPage = false,
): string {
$response = Http::withToken(config('services.screenshotrun.key'))
->timeout(60)
->get('https://api.screenshotrun.com/v1/screenshots/capture', [
'url' => $url,
'format' => $format,
'width' => $width,
'full_page' => $fullPage ? 'true' : 'false',
'response_type' => 'image',
]);
$response->throw();
return $response->body();
}
}
Register it in a service provider if you want to type-hint it, or just instantiate it directly:
$service = new \App\Services\ScreenshotService();
$imageBytes = $service->capture(
url: 'https://github.com',
format: 'webp',
width: 1920,
fullPage: true,
);
Storage::put('screenshots/github-full.webp', $imageBytes);
The throw() call converts any non-2xx response into a Laravel RequestException. Your controller can catch that or let the global exception handler deal with it.
Queue screenshots with Laravel Jobs
Screenshot captures take 2-4 seconds on average. In a web request, that blocks the response. Push the work onto a queue instead:
<?php
namespace App\Jobs;
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 CaptureScreenshot implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 10;
public function __construct(
public string $url,
public string $storagePath,
public string $format = 'png',
) {}
public function handle(ScreenshotService $service): void
{
$bytes = $service->capture(
url: $this->url,
format: $this->format,
);
Storage::put($this->storagePath, $bytes);
}
}
Dispatch it from a controller or anywhere else:
CaptureScreenshot::dispatch(
url: 'https://example.com',
storagePath: 'screenshots/example.png',
);
// Or process a batch:
$urls = ['https://github.com', 'https://laravel.com', 'https://packagist.org'];
foreach ($urls as $url) {
$slug = Str::slug(parse_url($url, PHP_URL_HOST));
CaptureScreenshot::dispatch($url, "screenshots/{$slug}.png");
}
Laravel's queue worker handles retries through the $tries and $backoff properties. If the API returns a 5xx or times out, the job retries up to three times with a 10-second gap. No custom retry logic needed because the queue system already has it built in.
Scheduled screenshots with Laravel's scheduler
Capture a set of URLs on a regular schedule. Useful for monitoring competitor pages, archiving your own site, or checking that a deploy did not break anything visible:
// In app/Console/Kernel.php or routes/console.php (Laravel 11+):
use App\Jobs\CaptureScreenshot;
Schedule::call(function () {
$urls = [
'https://your-site.com',
'https://competitor.com',
];
foreach ($urls as $url) {
$slug = Str::slug(parse_url($url, PHP_URL_HOST));
$date = now()->format('Y-m-d');
CaptureScreenshot::dispatch($url, "monitoring/{$slug}/{$date}.png");
}
})->daily();
Each day the scheduler dispatches jobs that capture every monitored URL. Screenshots land in date-stamped folders, so you can compare Tuesday's version to Wednesday's at a glance. Pair this with Laravel's notification system to get a Slack alert when a screenshot file size changes dramatically (a sign that the page layout shifted).
Controller endpoint for on-demand screenshots
Expose a screenshot route in your Laravel app. The frontend sends a URL, your backend calls the API with credentials and streams the image back:
use App\Services\ScreenshotService;
use Illuminate\Http\Request;
Route::get('/api/screenshot', function (Request $request, ScreenshotService $service) {
$request->validate([
'url' => 'required|url',
'format' => 'in:png,jpeg,webp',
'width' => 'integer|min:320|max:3840',
]);
$bytes = $service->capture(
url: $request->input('url'),
format: $request->input('format', 'png'),
width: $request->integer('width', 1280),
);
$contentType = match ($request->input('format', 'png')) {
'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
default => 'image/png',
};
return response($bytes, 200)
->header('Content-Type', $contentType)
->header('Cache-Control', 'public, max-age=3600');
})->middleware('auth:sanctum');
The Sanctum middleware keeps the endpoint private. Only authenticated users can request screenshots. The one-hour cache header prevents the browser from hitting your backend for the same URL twice in a row.
API parameters
Pass these as the second argument to Http::get(). Laravel converts the array into query string parameters automatically:
| Parameter | Type | Default | What it controls |
|---|---|---|---|
url | string | required | Which page to screenshot |
format | string | png | File type: png, jpeg, webp, avif, or pdf |
width | int | 1280 | Viewport width the headless browser uses (px) |
height | int | 720 | Viewport height the headless browser uses (px) |
full_page | bool | false | Scroll to the bottom, capture the entire document as one image |
device | string | desktop | Screen preset: desktop, mobile, or tablet |
retina | bool | false | 2x pixel density for sharp images on HiDPI screens |
dark_mode | bool | false | Request the page's dark color scheme |
selector | string | Capture only the element matching this CSS selector | |
delay | int | 0 | Milliseconds to wait after page load, useful for animations |
block_cookies | bool | false | Dismiss GDPR cookie walls before capturing |
block_ads | bool | false | Remove ad scripts and banner placements |
block_chats | bool | false | Strip chat widgets like Drift or Intercom |
css | string | Custom CSS rules applied to the page before Chrome takes the screenshot | |
js | string | JavaScript that executes inside the page after load but before the image is generated |
The full list of parameters, response shapes, and status codes lives in the API reference.
Dark mode, element screenshots, style overrides
Dark mode
Set dark_mode to true in the request array. Chrome flips its prefers-color-scheme media feature to dark before the page even starts loading. Sites with a dark palette (GitHub, Tailwind docs, Laravel.com) render that version without extra JavaScript:
$response = Http::withToken(config('services.screenshotrun.key'))
->get('https://api.screenshotrun.com/v1/screenshots/capture', [
'url' => 'https://github.com',
'dark_mode' => 'true',
'response_type' => 'image',
]);
Element screenshots
The selector parameter tells the API to find a specific DOM element and return only that portion of the page. Useful when you need a product card, a pricing table, or a hero section without the rest of the layout:
'selector' => '.pricing-table',
Style overrides
The css parameter lets you inject a stylesheet before the page renders. Remove sticky navbars, hide promotional banners, change background colors:
'css' => '.sticky-nav { display: none !important; } .hero { background: #f8f9fa; }',
You can mix all of them in a single request array. Dark mode, a selector target, custom CSS — the API stacks them in order and returns one cropped image. No chaining, no extra round trips.
Retry and error handling
Laravel's Http facade has built-in retry support. You do not need to write your own loop:
$response = Http::withToken(config('services.screenshotrun.key'))
->retry(3, 100, function ($exception, $request) {
// Retry on 429 (rate limit) and 5xx (server errors)
return $exception instanceof \Illuminate\Http\Client\RequestException
&& in_array($exception->response->status(), [429, 500, 502, 503]);
})
->timeout(60)
->get('https://api.screenshotrun.com/v1/screenshots/capture', [
'url' => $targetUrl,
'format' => 'png',
'response_type' => 'image',
]);
$response->throw();
The retry() method accepts the attempt count as its first argument, the pause between attempts (in milliseconds) as the second, plus a callback that decides whether to retry. Rate limit responses (429) and server errors (5xx) get another chance. A 401 (wrong key) or 422 (missing URL) exits the loop right away because the request itself is broken, and repeating it won't help.
Screenshot API vs. Spatie Browsershot
Spatie Browsershot is the most popular Laravel screenshot package. It wraps Puppeteer, which controls a real Chrome browser. That gives you full browser access: login flows, cookie injection, multi-step interactions. When the task is just converting a URL to an image, that entire Puppeteer stack becomes overhead.
| Spatie Browsershot | ScreenshotRun API | |
|---|---|---|
| Dependencies | Node.js + Puppeteer + Chromium (~400 MB) | Zero. Uses Laravel's built-in Http facade |
| Server requirements | Chrome system packages, shared memory config, matching Node version | Any server that runs Laravel (shared hosting included) |
| Laravel Vapor / serverless | Requires a Lambda layer for Chrome, complex setup | Works out of the box. No binary dependencies |
| Memory per capture | Chrome eats 300-500 MB per render on top of your PHP process | Your PHP process stays at its normal memory usage |
| Queue integration | Works but Chrome startup adds 2-3s overhead per job | One HTTP call per job, no startup cost |
| Cookie consent popups | Write Puppeteer scripts to dismiss them, update when sites change | Add block_cookies=true to the request array |
| Full-page scrolling | Built-in via fullPage() | Built-in via full_page=true |
| Cost | Free package, but Chrome server costs add up | Free plan covers 200 monthly captures, paid tiers scale from there |
Spatie Browsershot shines when you need to authenticate, navigate multiple pages, or manipulate the DOM before the capture — think Dusk-style testing or intranet screenshots that require an auth cookie. For the majority of Laravel screenshot work (turning public URLs into thumbnails, archiving pages on a schedule, generating PDF reports), the Http facade deploys on any host that runs PHP and keeps your Docker image hundreds of megabytes lighter.
Laravel teams wire this into daily monitoring jobs that archive competitor pages. It also powers link preview generators that show thumbnails in admin panels. Some teams run report pipelines that email dashboard snapshots to stakeholders every morning.
FAQ#
No. ScreenshotRun runs headless Chrome on its own servers. Your Laravel app sends an HTTP GET through the built-in Http facade and receives image bytes back. No binaries to install, no system packages to configure.
Add SCREENSHOTRUN_KEY=your_key to your .env file, then reference it in config/services.php under a screenshotrun key. Call it with config(services.screenshotrun.key) anywhere in your app. This keeps the secret out of version control.
Yes. Create a job class that implements ShouldQueue, inject the ScreenshotService, and dispatch the job from your controller. Laravel handles retries through the $tries and $backoff properties. The example on this page shows the full implementation.
It works on any platform that runs Laravel, including Vapor, shared hosting, and Docker containers. Since the Http facade only makes an outbound HTTP request, there are no binary dependencies or filesystem requirements beyond normal Laravel.
Browsershot wraps Puppeteer which wraps Chrome. That stack adds Node.js, Chromium (~400 MB), and system font packages to your server. The API approach replaces all of that with a single Http::get() call. Browsershot is better when you need to log into sites or run multi-step Puppeteer scripts before capturing.
PNG, JPEG, WebP, AVIF, and PDF. Pass the format parameter in the Http::get() request array. Laravel receives the raw bytes which you can store with Storage::put(), stream to the browser, or attach to a notification.