How to Use ScreenshotOne with PHP and Laravel
Capture website screenshots with ScreenshotOne in PHP and Laravel. Install the SDK, configure credentials, save results, and queue captures safely.
Use ScreenshotOne’s PHP SDK to request a website capture and receive the resulting image bytes. In Laravel, put the access key in environment-backed configuration, wrap the SDK in a small service, and decide whether each capture should be stored immediately or processed by a queued job. The vendor documents a PHP SDK, but the Laravel configuration and container wiring below are application patterns, not a first-party Laravel package recipe.
The SDK can build a request URL or retrieve bytes directly. This guide uses the bytes approach for a Laravel workflow and also shows the generic HTTP alternative. Keep the access key private; the separate secret key is for signing public links or verifying signed webhook payloads, and should not be sent as an API request parameter. [ScreenshotOne API keys](https://screenshotone.com/docs/api-keys/) · [PHP SDK documentation](https://screenshotone.com/docs/code-examples/php/)
1. Install the PHP SDK
Install the SDK from Composer:
composer require screenshotone/sdk:^1.0
The package metadata declares PHP 7.4 or newer and Guzzle 7.15.2 or 8.0.1 or newer for the referenced package version. Check the requirements for the version Composer resolves and your Laravel application’s PHP version before deploying. [Packagist package metadata](https://packagist.org/packages/screenshotone/sdk)
2. Store the credentials in Laravel configuration
Add credentials to the local environment, which should not be committed:
SCREENSHOTONE_ACCESS_KEY=your_access_key
SCREENSHOTONE_SECRET_KEY=your_secret_key
Create config/screenshotone.php as application-owned configuration:
<?php
return [
'access_key' => env('SCREENSHOTONE_ACCESS_KEY'),
'secret_key' => env('SCREENSHOTONE_SECRET_KEY'),
];
Use the access key to authenticate API requests. Keep the secret key out of requests; it is used for signed public links and webhook verification. ScreenshotOne recommends HTTPS because an unencrypted request can expose keys, authorization headers, cookies, and other sensitive values in transit. [API keys](https://screenshotone.com/docs/api-keys/) · [Getting Started](https://screenshotone.com/docs/getting-started/)
After changing environment-backed configuration in a deployed app, rebuild Laravel’s configuration cache according to your normal deployment process. Avoid putting credentials in a controller, a committed config file, a browser URL, or logs.
3. Make a capture with the PHP SDK
This standalone example follows the vendor SDK’s documented pattern. It requests a full-page image, waits two seconds, passes geolocation options, then writes the returned bytes to a file:
<?php
require __DIR__ . '/vendor/autoload.php';
use ScreenshotOne\Sdk\Client;
use ScreenshotOne\Sdk\TakeOptions;
$client = new Client('YOUR_ACCESS_KEY', 'YOUR_SECRET_KEY');
$options = TakeOptions::url('https://example.com')
->fullPage(true)
->delay(2)
->geolocationLatitude(37.7749)
->geolocationLongitude(-122.4194)
->geolocationAccuracy(100);
$bytes = $client->take($options);
file_put_contents(__DIR__ . '/capture.png', $bytes);
The exact option methods available can vary by SDK release; use the current SDK documentation for the option names supported by the version you install. The example’s coordinates are a sample location and should be changed or removed if the page does not need location-specific rendering. The PHP SDK can also generate the request URL when you need to inspect or hand off the URL instead of fetching bytes directly. [PHP SDK documentation](https://screenshotone.com/docs/code-examples/php/) · [Screenshot options](https://screenshotone.com/docs/options/)
4. Wire the SDK into Laravel
A small wrapper gives controllers and jobs one place to construct capture options. This is a Laravel implementation pattern around the PHP SDK, not a vendor-prescribed service provider:
<?php
namespace App\Services;
use RuntimeException;
use ScreenshotOne\Sdk\Client;
use ScreenshotOne\Sdk\TakeOptions;
final class ScreenshotService
{
private Client $client;
public function __construct()
{
$accessKey = config('screenshotone.access_key');
$secretKey = config('screenshotone.secret_key');
if (! is_string($accessKey) || $accessKey === '' || ! is_string($secretKey) || $secretKey === '') {
throw new RuntimeException('ScreenshotOne credentials are not configured.');
}
$this->client = new Client($accessKey, $secretKey);
}
public function capture(string $url): string
{
$options = TakeOptions::url($url)
->fullPage(true)
->delay(2);
return $this->client->take($options);
}
}
Laravel can resolve this concrete class through its container without a custom binding. If you prefer an explicit binding, register it in an application service provider. For easier unit testing, put your own interface in front of the wrapper and mock that interface in tests.
Here is a controller example that validates a URL and stores the binary result using Laravel’s storage facade:
<?php
namespace App\Http\Controllers;
use App\Services\ScreenshotService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Throwable;
final class CaptureController
{
public function store(Request $request, ScreenshotService $screenshots)
{
$validated = $request->validate([
'url' => ['required', 'url', 'max:2048'],
]);
try {
$bytes = $screenshots->capture($validated['url']);
} catch (Throwable $e) {
report($e);
return response()->json(['message' => 'Screenshot capture failed.'], 502);
}
$path = 'screenshots/' . Str::uuid() . '.png';
Storage::disk('local')->put($path, $bytes);
return response()->json(['path' => $path], 201);
}
}
This stores bytes on Laravel’s configured local disk. To serve captures publicly, choose a public disk or a controlled download route deliberately; do not make captures public by default if the requested pages or their contents may be sensitive. Validate and authorize URL capture requests: accepting arbitrary URLs from untrusted users can turn your application into a proxy to internal services.
5. Use Laravel’s HTTP client instead
If you do not need the SDK’s option builder, Laravel’s HTTP client can call the generic API. ScreenshotOne accepts GET and POST; the access key can go in a query parameter, JSON body, or X-Access-Key header. Use HTTPS. This example requests an image and saves the binary response:
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
$response = Http::timeout(90)
->withHeaders(['X-Access-Key' => config('screenshotone.access_key')])
->get('https://api.screenshotone.com/take', [
'url' => 'https://example.com',
'full_page' => true,
'delay' => 2,
]);
if (! $response->successful()) {
throw new RuntimeException('ScreenshotOne returned HTTP ' . $response->status());
}
$path = 'screenshots/' . Str::uuid() . '.png';
Storage::disk('local')->put($path, $response->body());
Confirm the endpoint and option spelling against the current ScreenshotOne API documentation before using this generic example; SDK and API options are represented differently. API errors include a human-readable message, error code, and HTTP status. Do not assume every successful response is an image if you requested another output or response mode. [Getting Started](https://screenshotone.com/docs/getting-started/) · [Options](https://screenshotone.com/docs/options/)
6. Choose output and persistence intentionally
| Need | Approach | Keep in mind |
|---|---|---|
| Return an image from a request | Fetch bytes with take() or HTTP client, then return or store them |
Keep the response binary; avoid treating it as JSON or text. |
| Save a durable application copy | Write bytes to Laravel storage or configured object storage | Service-side caching is separate from your own storage lifecycle. |
| Avoid repeating an identical render | Use ScreenshotOne’s cache=true option |
Documented default cache lifetime is four hours and can be configured up to one month; cached results do not consume rendering quota. |
| Return a generated URL instead of bytes | Use the SDK URL-building capability or a documented API response mode | Do not expose credentials in a public URL. Use signed links where appropriate. |
| Need a document or rendered content | Select a documented output such as PDF, HTML, or Markdown | Choose based on downstream use and verify current format and plan terms. |
Documented output formats include PNG, JPEG/JPG, WebP, GIF, JP2, TIFF, AVIF, HEIF, PDF, HTML, and Markdown. Ordinary binary responses are not stored by ScreenshotOne by default unless caching, storage, or similar features are used. JSON responses may involve temporary storage to serve a content URL; configured S3-compatible storage is a separate service-side persistence choice. [Options](https://screenshotone.com/docs/options/) · [Caching](https://screenshotone.com/docs/caching/)
7. Queue captures for web requests
Rendering can take longer than a normal web request. For user-triggered captures that do not need to finish before responding, dispatch a Laravel job and return a job or resource identifier. A minimal job can call the same wrapper and store the result:
<?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;
use Illuminate\Support\Str;
use Throwable;
final class CaptureWebsite implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 120;
public function __construct(public string $url) {}
public function backoff(): array
{
return [5, 20, 60];
}
public function handle(ScreenshotService $screenshots): void
{
$bytes = $screenshots->capture($this->url);
$path = 'screenshots/' . Str::uuid() . '.png';
Storage::disk('local')->put($path, $bytes);
}
public function failed(Throwable $exception): void
{
report($exception);
}
}
Set the queue worker timeout and Laravel retry policy to match the render timeout and your queue backend. The retry/backoff behavior above is application design: ScreenshotOne does not automatically retry API requests. Make jobs safe to retry by using deterministic object names or recording a capture state so a retry does not create unintended duplicates.
For large queues, pace request starts. ScreenshotOne’s usage endpoint reports request counts and a concurrency object; concurrency.remaining and concurrency.reset refer to remaining request starts in the current minute bucket, not the number of active renders. Use that distinction when setting worker throughput. [Get Usage](https://screenshotone.com/docs/get-usage/) · [Bulk screenshots](https://screenshotone.com/docs/guides/bulk-screenshots/)
8. Performance, reliability, and cost
- Rendering time: set the Laravel HTTP or queue timeout to allow for remote page loading and the capture delay. Avoid adding fixed delays unless the target page needs them; a delay increases completion time.
- Repeat work: use the documented cache option when the same URL and options can reuse a recent capture. The default lifetime is four hours and can be set up to one month; cache hits do not consume rendering quota. Confirm cache behavior for the exact request options you use.
- Transient failures: set bounded retries with backoff in your queue. Since the API does not automatically retry requests, decide which failures are retryable and cap attempts to prevent a failing URL from looping.
- Throughput: monitor usage and pace job starts using the usage endpoint’s request bucket values. Do not interpret those values as active-render concurrency.
- Application storage: image bytes consume your own disk or object-storage capacity when you persist them. Define retention and cleanup policies for generated files.
- Quota and plans: the vendor’s PHP page listed 100 free screenshots per month when reviewed on 2026-10-03. Allowances and terms can change, so check the current plan details before estimating spend. [PHP Screenshot API](https://screenshotone.com/screenshot-api/php/)
9. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Composer cannot install the SDK | PHP or Guzzle constraints do not match the selected package version | Check php -v, the resolved package requirements, and the version constraints in Packagist; update the runtime or choose a compatible release. |
| Authentication failure | Missing, incorrect, or stale access key; configuration cache still contains an older value | Check the environment value and deployment config cache. Use the access key for authentication; do not substitute the secret key. |
| Timeout in a controller or job | Target page is slow, the chosen delay is long, or the client/worker timeout is too short | Increase the relevant bounded timeout, move the work to a queue, and remove unnecessary delay. Keep retries limited. |
| Saved file is unreadable | Binary response was mishandled, an error response was saved, or extension does not match requested format | Check HTTP status before writing, inspect the API error payload on failure, and use a filename extension matching the requested output. |
| Capture looks incomplete | Page content loads after the capture point, or content depends on location or other options | Use a suitable wait/delay and relevant rendering options; verify target behavior and avoid an arbitrary excessive delay. |
| Repeated API errors exhaust quota | Application retries are too frequent or failures are not classified | Use bounded backoff, stop retrying permanent errors, and monitor the usage endpoint. |
| Large request fails | Large HTML or Markdown was sent through a URL/query string or request body exceeds the documented limit | Use JSON POST for large content. The documented maximum POST body is 100 MiB; keep payloads below it. [Getting Started](https://screenshotone.com/docs/getting-started/) |
10. Or skip the browser setup
If your Laravel application needs a screenshot endpoint without managing a browser runtime, ScreenshotNeo provides a one-request screenshot API and an MCP server for AI agents. It removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. AI agents can use its MCP tools to take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Here is the cURL request; replace the sample URL with the page you need. See the ScreenshotNeo API documentation for options and formats.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
ScreenshotNeo also has PHP, Python, and Node.js options for integrating the same endpoint. Learn more at ScreenshotNeo. Sign up free for 1,000 screenshots a month with no card.
FAQ
Does ScreenshotOne provide an official Laravel package?
The reviewed sources document the PHP SDK and generic HTTP API, but not a Laravel-specific first-party package or service-provider recipe. Laravel configuration, dependency injection, controllers, storage, and jobs are application wiring.
Should I return bytes or a URL?
Use bytes when Laravel should store or immediately stream the result. Use a generated or signed URL when another system needs to fetch it, taking care not to expose credentials.
Does Laravel need a browser installed for this API flow?
The examples send a request to ScreenshotOne and receive the rendered result; they do not launch a browser process inside the Laravel application.
Can I send large HTML as a query parameter?
For large HTML or Markdown input, use JSON POST rather than a query string. The documented POST body limit is 100 MiB.


