How to Take Screenshots with Laravel Using a Screenshot API
Laravel's expressive syntax and rich ecosystem make integrating external APIs straightforward. Whether you're building a monitoring dashboard, an archiving tool, or a social preview generator, this guide covers the full range of Laravel patterns for screenshot API integration.
Setup
composer require guzzlehttp/guzzle
Laravel's HTTP client wraps Guzzle and is available out of the box in Laravel 7+. Add your API key to .env:
SCREENSHOT_API_KEY=your_api_key_here
Register it in config/services.php:
'screenshot' => [
'key' => env('SCREENSHOT_API_KEY'),
'base_url' => 'https://hermesforge.dev/api/screenshot',
],
Get a free API key at hermesforge.dev/screenshot.
Basic Controller
A minimal Laravel controller that proxies screenshot requests:
// app/Http/Controllers/ScreenshotController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
class ScreenshotController extends Controller
{
public function capture(Request $request): Response
{
$request->validate([
'url' => 'required|url',
'width' => 'integer|min:1|max:3840',
'height' => 'integer|min:1|max:2160',
'format' => 'in:webp,png,jpeg',
'full_page' => 'boolean',
'delay' => 'integer|min:0|max:10000',
]);
$response = Http::withHeaders([
'X-API-Key' => config('services.screenshot.key'),
])->get(config('services.screenshot.base_url'), $request->only([
'url', 'width', 'height', 'format', 'full_page', 'delay',
]));
if ($response->failed()) {
abort($response->status(), 'Screenshot failed');
}
return response($response->body(), 200, [
'Content-Type' => $response->header('Content-Type') ?? 'image/webp',
'Cache-Control' => 'public, max-age=300',
]);
}
}
Register the route in routes/api.php:
Route::get('/screenshot', [ScreenshotController::class, 'capture']);
Test it:
curl "http://localhost:8000/api/screenshot?url=https://example.com" --output example.webp
Service Class
Extract the API interaction into an injectable service:
// app/Services/ScreenshotService.php
<?php
namespace App\Services;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class ScreenshotService
{
private string $apiKey;
private string $baseUrl;
public function __construct()
{
$this->apiKey = config('services.screenshot.key');
$this->baseUrl = config('services.screenshot.base_url');
}
public function capture(
string $url,
int $width = 1280,
int $height = 800,
string $format = 'webp',
bool $fullPage = false,
?int $delay = null,
): string {
$params = array_filter([
'url' => $url,
'width' => $width,
'height' => $height,
'format' => $format,
'full_page' => $fullPage ? 'true' : 'false',
'delay' => $delay,
], fn($v) => $v !== null);
$response = Http::timeout(30)
->withHeaders(['X-API-Key' => $this->apiKey])
->get($this->baseUrl, $params);
if ($response->failed()) {
throw new RuntimeException(
"Screenshot API error: HTTP {$response->status()}",
$response->status()
);
}
return $response->body();
}
public function captureToBase64(string $url, array $options = []): string
{
$body = $this->capture($url, ...$options);
return base64_encode($body);
}
}
Bind it in AppServiceProvider or use auto-resolution:
// The service can be auto-resolved via Laravel's container
// Inject it directly in controllers:
public function capture(Request $request, ScreenshotService $screenshots): Response
{
$body = $screenshots->capture(
url: $request->input('url'),
width: $request->integer('width', 1280),
height: $request->integer('height', 800),
);
return response($body, 200, ['Content-Type' => 'image/webp']);
}
Response Caching
Cache screenshot results with Laravel's Cache facade:
use Illuminate\Support\Facades\Cache;
public function capture(Request $request, ScreenshotService $screenshots): Response
{
$request->validate(['url' => 'required|url']);
$cacheKey = 'screenshot:' . md5(http_build_query($request->only([
'url', 'width', 'height', 'format', 'full_page',
])));
$body = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($request, $screenshots) {
return $screenshots->capture(
url: $request->input('url'),
width: $request->integer('width', 1280),
height: $request->integer('height', 800),
format: $request->input('format', 'webp'),
fullPage: $request->boolean('full_page'),
);
});
return response($body, 200, [
'Content-Type' => 'image/webp',
'Cache-Control' => 'public, max-age=300',
]);
}
Switch cache drivers in .env — CACHE_DRIVER=redis for production.
Rate Limiting
Use Laravel's built-in rate limiting in RouteServiceProvider or routes/api.php:
// routes/api.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('screenshots', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
Route::middleware(['throttle:screenshots'])
->get('/screenshot', [ScreenshotController::class, 'capture']);
Laravel returns 429 Too Many Requests with a Retry-After header automatically.
Queued Job
For long-running captures or background processing, dispatch a Job:
// app/Jobs/CaptureScreenshot.php
<?php
namespace App\Jobs;
use App\Models\Screenshot;
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 = 30; // Seconds between retries
public function __construct(
private readonly string $url,
private readonly int $screenshotId,
private readonly array $options = [],
) {}
public function handle(ScreenshotService $service): void
{
$body = $service->capture($this->url, ...$this->options);
$path = "screenshots/{$this->screenshotId}.webp";
Storage::put($path, $body);
Screenshot::findOrFail($this->screenshotId)->update([
'status' => 'ready',
'path' => $path,
]);
}
public function failed(\Throwable $exception): void
{
Screenshot::findOrFail($this->screenshotId)->update([
'status' => 'failed',
'error' => $exception->getMessage(),
]);
}
}
Dispatch from a controller:
public function captureAsync(Request $request): \Illuminate\Http\JsonResponse
{
$request->validate(['url' => 'required|url']);
$screenshot = Screenshot::create([
'url' => $request->input('url'),
'status' => 'pending',
]);
CaptureScreenshot::dispatch($request->input('url'), $screenshot->id);
return response()->json(['id' => $screenshot->id, 'status' => 'pending']);
}
public function status(int $id): \Illuminate\Http\JsonResponse
{
$screenshot = Screenshot::findOrFail($id);
return response()->json($screenshot->only(['id', 'url', 'status', 'error']));
}
Batch Screenshots
Capture multiple URLs in a single request using concurrent HTTP:
use Illuminate\Support\Facades\Http;
public function batch(Request $request): \Illuminate\Http\JsonResponse
{
$request->validate([
'urls' => 'required|array|max:10',
'urls.*' => 'url',
'width' => 'integer|min:1|max:3840',
'height' => 'integer|min:1|max:2160',
]);
$apiKey = config('services.screenshot.key');
$baseUrl = config('services.screenshot.base_url');
$params = $request->only(['width', 'height']) + ['format' => 'webp'];
// Build concurrent requests
$promises = collect($request->input('urls'))->mapWithKeys(function ($url) use ($apiKey, $baseUrl, $params) {
return [$url => Http::withHeaders(['X-API-Key' => $apiKey])
->async()
->get($baseUrl, ['url' => $url] + $params)];
});
$results = Http::pool(fn ($pool) =>
$promises->map(fn ($_, $url) =>
$pool->as($url)->withHeaders(['X-API-Key' => $apiKey])
->get($baseUrl, ['url' => $url] + $params)
)->toArray()
);
$formatted = collect($results)->map(function ($response, $url) {
if ($response instanceof \Throwable || $response->failed()) {
return ['url' => $url, 'status' => 'error', 'error' => 'Capture failed'];
}
return [
'url' => $url,
'status' => 'ok',
'data' => base64_encode($response->body()),
];
})->values();
return response()->json(['results' => $formatted]);
}
Artisan Command
Add a CLI command for scheduled or manual captures:
// app/Console/Commands/CaptureScreenshotCommand.php
<?php
namespace App\Console\Commands;
use App\Services\ScreenshotService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CaptureScreenshotCommand extends Command
{
protected $signature = 'screenshot:capture
{url : URL to capture}
{--output= : Output file path}
{--width=1280 : Viewport width}
{--height=800 : Viewport height}
{--format=webp : Image format (webp, png, jpeg)}
{--full-page : Capture full page}';
protected $description = 'Capture a screenshot of a URL';
public function handle(ScreenshotService $service): int
{
$url = $this->argument('url');
$this->info("Capturing: {$url}");
try {
$body = $service->capture(
url: $url,
width: (int) $this->option('width'),
height: (int) $this->option('height'),
format: $this->option('format'),
fullPage: $this->option('full-page'),
);
$output = $this->option('output') ?? 'screenshot.' . $this->option('format');
file_put_contents($output, $body);
$this->info("Saved to: {$output} (" . strlen($body) . " bytes)");
return Command::SUCCESS;
} catch (\RuntimeException $e) {
$this->error("Failed: {$e->getMessage()}");
return Command::FAILURE;
}
}
}
Run it:
php artisan screenshot:capture https://example.com --output=example.webp --full-page
Register in app/Console/Kernel.php to run on a schedule:
$schedule->command('screenshot:capture https://yoursite.com --output=/var/screenshots/daily.webp')
->dailyAt('06:00');
Error Handling
Use Laravel's exception handler for consistent error responses:
// app/Exceptions/Handler.php
use App\Exceptions\ScreenshotException;
$this->renderable(function (ScreenshotException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json(['error' => $e->getMessage()], $e->getCode() ?: 500);
}
return response($e->getMessage(), $e->getCode() ?: 500);
});
Get Your API Key
Free API key at hermesforge.dev/screenshot. Full parameter reference at hermesforge.dev/api/docs.