Need to capture website screenshots from your PHP application? Whether you're building link previews, generating social cards, archiving pages, or monitoring competitor sites, a screenshot API is the simplest path. No Puppeteer servers to manage, no Chrome binaries to install on your hosting.
This guide covers three approaches: vanilla PHP with cURL, a clean Laravel integration, and async batch processing for high-volume use cases. All examples use GrabShot's screenshot API, which returns images in under 3 seconds with a generous free tier.
PHP developers often try to run Puppeteer or Chrome via shell commands. This works locally but creates real problems in production:
A screenshot API handles all of this. You send a URL, you get an image back. Your PHP app stays lightweight.
The simplest approach uses PHP's built-in cURL functions. No packages, no dependencies:
<?php
// Capture a screenshot with GrabShot API
function captureScreenshot(string $url, string $apiKey, array $options = []): string|false
{
$params = array_merge([
'url' => $url,
'width' => 1280,
'height' => 800,
'format' => 'png',
], $options);
$endpoint = 'https://grabshot.dev/api/screenshot?' . http_build_query($params);
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-Key: $apiKey"],
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
error_log("Screenshot failed for $url: HTTP $httpCode");
return false;
}
return $response; // Raw image bytes
}
// Usage
$apiKey = getenv('GRABSHOT_API_KEY');
$image = captureScreenshot('https://example.com', $apiKey);
if ($image) {
file_put_contents('screenshot.png', $image);
echo "Screenshot saved!\n";
}
That's it. About 20 lines of actual code. The function returns raw image bytes that you can save to disk, store in S3, or serve directly to the browser.
If you're capturing screenshots of the same URLs repeatedly (like for link previews), add a simple file-based cache to avoid burning API credits:
<?php
function cachedScreenshot(string $url, string $apiKey, int $ttl = 3600): string|false
{
$cacheDir = __DIR__ . '/cache/screenshots';
$cacheFile = $cacheDir . '/' . md5($url) . '.png';
// Return cached version if fresh
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
return file_get_contents($cacheFile);
}
// Capture fresh screenshot
$image = captureScreenshot($url, $apiKey);
if ($image) {
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
file_put_contents($cacheFile, $image);
}
return $image;
}
// Screenshots cached for 1 hour by default
$image = cachedScreenshot('https://example.com', $apiKey);
For production apps, consider using Redis or your framework's cache instead of the filesystem. But for small projects, this works perfectly.
In Laravel, you can wrap the API in a clean service class with dependency injection, config management, and queue support.
Add to your .env:
GRABSHOT_API_KEY=your_api_key_here
GRABSHOT_BASE_URL=https://grabshot.dev/api
Create config/grabshot.php:
<?php
return [
'api_key' => env('GRABSHOT_API_KEY'),
'base_url' => env('GRABSHOT_BASE_URL', 'https://grabshot.dev/api'),
'defaults' => [
'width' => 1280,
'height' => 800,
'format' => 'png',
],
];
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
class ScreenshotService
{
public function capture(string $url, array $options = []): ?string
{
$params = array_merge(config('grabshot.defaults'), $options, [
'url' => $url,
]);
$response = Http::withHeaders([
'X-API-Key' => config('grabshot.api_key'),
])
->timeout(30)
->get(config('grabshot.base_url') . '/screenshot', $params);
if ($response->failed()) {
logger()->error("Screenshot failed: {$url}", [
'status' => $response->status(),
]);
return null;
}
return $response->body();
}
public function captureAndStore(
string $url,
string $path = null,
string $disk = 'public'
): ?string {
$image = $this->capture($url);
if (!$image) return null;
$path = $path ?? 'screenshots/' . md5($url) . '.png';
Storage::disk($disk)->put($path, $image);
return $path;
}
public function cached(string $url, int $ttl = 3600): ?string
{
$key = 'screenshot:' . md5($url);
return Cache::remember($key, $ttl, function () use ($url) {
return $this->captureAndStore($url);
});
}
}
<?php
// In a controller
class LinkPreviewController extends Controller
{
public function __construct(
private ScreenshotService $screenshots
) {}
public function show(Request $request)
{
$url = $request->validate(['url' => 'required|url'])['url'];
$path = $this->screenshots->cached($url);
return response()->json([
'preview_url' => Storage::url($path),
]);
}
}
// In an Artisan command
$service = app(ScreenshotService::class);
$service->captureAndStore('https://laravel.com', 'previews/laravel.png');
For bulk screenshot jobs (sitemap audits, batch social cards, competitor monitoring), dispatch the work to a queue so your web requests stay fast:
<?php
namespace App\Jobs;
use App\Services\ScreenshotService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class CaptureScreenshotJob implements ShouldQueue
{
use Dispatchable, Queueable;
public int $tries = 3;
public int $backoff = 10;
public function __construct(
public string $url,
public string $storagePath,
public array $options = [],
) {}
public function handle(ScreenshotService $service): void
{
$image = $service->capture($this->url, $this->options);
if (!$image) {
$this->fail(new \Exception("Screenshot capture failed: {$this->url}"));
return;
}
Storage::disk('public')->put($this->storagePath, $image);
}
}
// Dispatch from anywhere:
$urls = ['https://example.com', 'https://laravel.com', 'https://php.net'];
foreach ($urls as $i => $url) {
CaptureScreenshotJob::dispatch(
url: $url,
storagePath: "batch/screenshot-{$i}.png",
);
}
With Laravel Horizon or a simple queue worker, this processes screenshots in parallel without blocking your app. The built-in retry logic handles transient failures automatically.
25 free screenshots/month. No credit card required. Works with any PHP version 7.4+.
Try GrabShot Free →GrabShot supports parameters that handle common screenshot challenges:
| Parameter | Description | Example |
|---|---|---|
| width | Viewport width in pixels | 1440 |
| height | Viewport height | 900 |
| full_page | Capture entire scrollable page | true |
| format | Output format | png, jpeg, webp |
| delay | Wait before capture (ms) | 2000 |
| dark_mode | Force dark color scheme | true |
| block_ads | Block ads and trackers | true |
Use these in any of the examples above by passing them in the options array:
$image = captureScreenshot('https://example.com', $apiKey, [
'full_page' => 'true',
'format' => 'webp',
'delay' => 2000,
'dark_mode' => 'true',
]);
In production, you want to handle failures gracefully. Here's a robust version with retries:
<?php
function captureWithRetry(
string $url,
string $apiKey,
int $maxRetries = 3,
array $options = []
): string|false {
$lastError = '';
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
$image = captureScreenshot($url, $apiKey, $options);
if ($image !== false) {
return $image;
}
$lastError = "Attempt $attempt failed";
error_log("$lastError for $url");
if ($attempt < $maxRetries) {
// Exponential backoff: 1s, 2s, 4s
sleep(pow(2, $attempt - 1));
}
}
error_log("All $maxRetries attempts failed for $url: $lastError");
return false;
}
One of the most common needs: generating preview cards for URLs that users paste into your app (like Slack or Twitter do). Here's a minimal endpoint:
<?php
// GET /api/preview?url=https://example.com
header('Content-Type: application/json');
$url = filter_input(INPUT_GET, 'url', FILTER_VALIDATE_URL);
$apiKey = getenv('GRABSHOT_API_KEY');
if (!$url) {
http_response_code(400);
echo json_encode(['error' => 'Invalid URL']);
exit;
}
$image = cachedScreenshot($url, $apiKey, ttl: 7200);
if (!$image) {
http_response_code(502);
echo json_encode(['error' => 'Screenshot capture failed']);
exit;
}
// Return as base64 for easy embedding
echo json_encode([
'url' => $url,
'image' => 'data:image/png;base64,' . base64_encode($image),
'timestamp' => time(),
]);
On the frontend, render it in an <img> tag directly from the base64 data. No file storage needed for ephemeral previews.
Capturing website screenshots in PHP doesn't require running headless browsers on your server. A simple API call with cURL or Laravel's HTTP client gives you reliable screenshots with minimal code and zero infrastructure overhead.
The examples in this guide work with GrabShot's API, which includes 25 free screenshots per month on the free tier. For higher volumes, paid plans start at $9/month with no rate limits on individual requests.
Check out the full API documentation for additional parameters like custom CSS injection, cookie handling, and webhook callbacks.